CoursesAWS CloudFormationDeploying: stacks & events

Deploying: stacks & events

create, update, rollback, delete.

Beginner12 min · lesson 4 of 12

Hand a blueprint to a general contractor and you don't pour the foundation, wire the panel, and hang the doors yourself. You submit one order. The contractor works out what has to happen first, builds in the right sequence, and keeps a signed record of everything installed. A CloudFormation stack works the same way. CloudFormation is the infrastructure-as-code service from Amazon Web Services (AWS): it builds cloud resources from a written template instead of by hand. You hand it one template and it becomes the contractor for every resource inside. You submit the template once, and the stack works out the dependency order, provisions in sequence, and keeps a receipt of exactly what it built.

That receipt is the part a defender cares about. Every create, update, and delete is a single transaction recorded against it, and the stack's event log is the running narration of the build, timestamped line by line. When a deploy hangs, a resource shows up that nobody remembers adding, or a pager goes off at 3 a.m., that log is your first piece of evidence. This lesson is about driving the stack lifecycle from the command line (the CLI, or command-line interface, the text commands you type into a shell) and reading what it tells you. Previewing a change before you commit it is the job of change sets, and reconciling what is really running against the template is drift. Both get their own lessons.

Create: submit the template, watch it build

There are two front doors. create-stack is the primitive. It submits the template once, hands you back a stack ID, and returns immediately while the real work happens in the background. That ID is an ARN (Amazon Resource Name, the globally unique address AWS stamps on every resource). deploy is the higher-level command most pipelines reach for. It creates the stack if it is missing and updates it if it already exists, running a change set underneath (a change set is a preview of the exact changes an update would make, covered in its own lesson), so your scripts never have to branch on whether the stack is there yet.

If your template creates any IAM resources (IAM is Identity and Access Management, the AWS system that decides who is allowed to do what), CloudFormation stops and makes you sign for it. It will not quietly mint permissions on your behalf. That refusal is a security control, not red tape. Creating a role or a policy is exactly the kind of change you want a human to approve on purpose. You clear it with a --capabilities flag: CAPABILITY_IAM for IAM resources with auto-generated names, and CAPABILITY_NAMED_IAM when any of them carry a custom name you picked. Templates that pull in macros or the Serverless transform (features that expand shorthand into full resources) also need CAPABILITY_AUTO_EXPAND.

The --on-failure flag decides what happens when a create goes wrong. ROLLBACK, the default, tears the half-built stack back down to nothing. DELETE removes the stack outright. DO_NOTHING freezes everything in place, the resources that succeeded and the one that failed, so you can open them up and read the wreckage before anything gets cleaned away.

terminal
# Primitive: create only, fails if the stack already exists
aws cloudformation create-stack \
--stack-name my-app \
--template-body file://template.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--on-failure ROLLBACK
# Block until it settles: exit 0 on success, non-zero on failure
aws cloudformation wait stack-create-complete --stack-name my-app
output
{
"StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/my-app/8f4e2a10-6d5c-11f0-b3a1-0e8c4d2f9a71"
}

The wait prints nothing and exits 0 when the stack settles cleanly. A non-zero exit is your failure signal, which is what a pipeline step keys on. Most pipelines skip the create-versus-update branching entirely and let one command decide.

terminal
# One command that creates or updates, whichever the stack needs
aws cloudformation deploy \
--template-file template.yaml \
--stack-name my-app \
--capabilities CAPABILITY_NAMED_IAM
output
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - my-app

Events: the stack's flight recorder

Now suppose that create did not go clean. When a deploy stalls or dies, the event stream is your black box recorder. describe-stack-events returns one entry per resource state change, newest at the top. The ResourceStatusReason on a failed row is usually the literal error the underlying service threw back: a permission denial, a name collision, a quota you have hit. There is a trick to reading it. Start at the bottom and read upward. The first CREATE_FAILED you meet going up is the real cause. Everything above it is CloudFormation dutifully deleting the resources that did succeed as it unwinds the whole stack.

terminal
# Newest first; keep only the fields that matter
aws cloudformation describe-stack-events \
--stack-name my-app \
--query 'StackEvents[*].[Timestamp,ResourceStatus,LogicalResourceId,ResourceStatusReason]' \
--output text
output
2026-07-21T10:36:44.102Z ROLLBACK_COMPLETE my-app None
2026-07-21T10:36:43.550Z DELETE_COMPLETE AppRole None
2026-07-21T10:36:41.017Z DELETE_IN_PROGRESS AppRole None
2026-07-21T10:36:40.233Z ROLLBACK_IN_PROGRESS my-app The following resource(s) failed to create: [AppBucket].
2026-07-21T10:36:39.802Z CREATE_FAILED AppBucket my-app-assets already exists
2026-07-21T10:36:37.145Z CREATE_IN_PROGRESS AppBucket None
2026-07-21T10:36:35.688Z CREATE_COMPLETE AppRole None
2026-07-21T10:36:33.291Z CREATE_IN_PROGRESS AppRole Resource creation Initiated
2026-07-21T10:36:31.774Z CREATE_IN_PROGRESS AppRole None
2026-07-21T10:36:29.402Z CREATE_IN_PROGRESS my-app User Initiated

Read this one from the bottom. The stack starts (User Initiated), the role builds fine, then AppBucket hits CREATE_FAILED with the reason my-app-assets already exists. That is the root cause. Everything above it is cleanup: CloudFormation deletes the role it just built and parks the stack in ROLLBACK_COMPLETE. The wait subcommands are the scriptable version of watching this by eye. They poll for you and set an exit code, which is what a CI job (CI is continuous integration, the automated pipeline that builds and tests your changes) needs instead of a hardcoded sleep 120 and a prayer.

When you want only the one-word verdict, ask for it directly.

terminal
# The whole stack's status, one word
aws cloudformation describe-stacks \
--stack-name my-app \
--query 'Stacks[0].StackStatus' --output text
output
ROLLBACK_COMPLETE
A failed create: where does the stack land?
--on-failure, when the first create fails
ROLLBACK (default)
ROLLBACK_COMPLETE
One-way door. delete-stack, then create again from a fixed template.
DELETE
Stack removed
Clean slate. Nothing left in your account to inspect.
DO_NOTHING
CREATE_FAILED
Half-built stack kept for forensics. Still must delete before retrying.

Update: only what changed, all or nothing

update-stack (or another deploy) compares the running stack against the new template and applies only the difference. Some resources change in place with no disruption. Others force a replacement, where CloudFormation builds a brand-new physical resource, cuts over to it, and deletes the old one, all behind a stable logical name so the rest of your template still points at the right thing. Which properties trigger a replacement is the difference between a quiet patch and an unplanned database swap, so read the change set before you run a production update.

The automatic rollback is the safety net. If any resource in the update fails, CloudFormation rolls the entire stack back to the last known-good configuration on its own. You get all of the change or none of it, never a stack left stranded halfway between two versions.

terminal
# Apply template changes; auto-rolls back if any resource fails
aws cloudformation update-stack \
--stack-name my-app \
--template-body file://template.yaml \
--capabilities CAPABILITY_NAMED_IAM
aws cloudformation wait stack-update-complete --stack-name my-app
output
{
"StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/my-app/8f4e2a10-6d5c-11f0-b3a1-0e8c4d2f9a71"
}

One rough edge. If the template is byte-for-byte what is already deployed, update-stack does not shrug it off, it errors.

terminal
# Same template that's already deployed
aws cloudformation update-stack \
--stack-name my-app \
--template-body file://template.yaml
output
An error occurred (ValidationError) when calling the UpdateStack operation: No updates are to be performed.

deploy handles the same case gracefully, printing No changes to deploy and exiting 0, which is one more reason pipelines prefer it. And if you would rather inspect a broken update than watch it vanish, pass --disable-rollback and the stack parks in UPDATE_FAILED with the failed resources left intact for you to examine.

Protecting and tearing down

delete-stack runs the build in reverse. CloudFormation walks the dependency graph backward and removes resources in the opposite order it created them, so nothing gets pulled out from under something that still needs it. That is a lot of power behind one command, which is why production stacks get a deadbolt: termination protection. With it enabled, delete-stack is refused until someone deliberately turns the protection off.

terminal
# Put a deadbolt on a stack you can't afford to lose
aws cloudformation update-termination-protection \
--stack-name my-app --enable-termination-protection
# A delete is now refused until someone turns the deadbolt off
aws cloudformation delete-stack --stack-name my-app
output
{
"StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/my-app/8f4e2a10-6d5c-11f0-b3a1-0e8c4d2f9a71"
}
An error occurred (ValidationError) when calling the DeleteStack operation: Stack [arn:aws:cloudformation:us-east-1:123456789012:stack/my-app/8f4e2a10-6d5c-11f0-b3a1-0e8c4d2f9a71] cannot be deleted while TerminationProtection is enabled

To actually remove the stack, disable protection with --no-enable-termination-protection, then run delete-stack followed by wait stack-delete-complete. Both of those are silent on success.

Termination protection is narrower than it sounds
It only stops delete-stack on that one stack. It does nothing about an update that replaces a resource (a single changed property can still swap your database out from under you), and it does nothing if someone deletes the underlying resource directly through its own service, say emptying and deleting the S3 bucket (S3 is Simple Storage Service, Amazon's object storage) from the console. To block updates to specific resources, add a stack policy. To catch an out-of-band delete you need drift detection and CloudTrail (AWS's log of every API call, every programmatic request, made in your account), not this flag.

When the rollback itself gets stuck

Two states will trap you, and both come from a rollback that could not finish. The first you have already seen. A stack whose very first create fails lands in ROLLBACK_COMPLETE, and that is a one-way door. You cannot update it. The CLI rejects every update-stack and deploy you throw at it. The only way out is delete-stack, then create again from a fixed template.

The nastier cousin is UPDATE_ROLLBACK_FAILED. This is the lock jamming mid-turn. An update failed, CloudFormation tried to roll back to the last good state, and the rollback itself could not complete. The usual reason is that something changed out from under it: a resource the rollback needed was modified or deleted out-of-band, by a console click, another tool, or a person poking at production during an incident. CloudFormation cannot put the old state back, so the stack freezes and refuses new updates.

For a defender that cuts two ways. It is a reliability trap, and it is also a tell. If a stack wedges in UPDATE_ROLLBACK_FAILED and nobody on your team touched those resources by hand, something outside your pipeline changed your infrastructure, and that is worth investigating before you force the stack back into shape. CloudTrail will show you which identity made the change and when.

You recover with continue-update-rollback. Fix the underlying resource first if you can, then let CloudFormation finish the rollback it already started.

terminal
# Fix the blocker, then resume the rollback CloudFormation started
aws cloudformation continue-update-rollback --stack-name my-app
aws cloudformation wait stack-rollback-complete --stack-name my-app
# Confirm it landed somewhere you can deploy to again
aws cloudformation describe-stacks --stack-name my-app \
--query 'Stacks[0].StackStatus' --output text
output
UPDATE_ROLLBACK_COMPLETE

If a single resource is the blocker and you cannot restore it, add --resources-to-skip AppRole to that first command. CloudFormation abandons only that one resource and finishes the rollback for everything else.

A skipped resource does not vanish, it goes dark. --resources-to-skip drops the resource from the stack's bookkeeping, but the real thing keeps running in your account, live and billable, with nothing tracking it anymore. An orphaned IAM role or security group that no pipeline watches is exactly the forgotten door an attacker goes hunting for. Fix it, delete it, or re-import it into a stack the same day. Never leave one hanging.

Quick check
01Your first create-stack failed and the stack is sitting in ROLLBACK_COMPLETE. You have fixed the template. What actually gets you to a working stack?
Incorrect — Rejected. CloudFormation will not update a stack in ROLLBACK_COMPLETE; the CLI errors out every time.
Correct — ROLLBACK_COMPLETE is a one-way door; deleting and recreating is the only exit.
Incorrect — Wrong state. That command is for UPDATE_ROLLBACK_FAILED, not a failed first-ever create.
Incorrect — No. deploy runs an update under the hood, and updates on ROLLBACK_COMPLETE are refused too.
02Your template creates an IAM role (IAM is Identity and Access Management, which controls who may do what in AWS) and you give that role a custom name you chose. Which capability must the deploy pass?
Incorrect — CAPABILITY_IAM covers IAM resources with auto-generated names; a custom name needs the stricter flag.
Correct — when any IAM resource carries a custom name you picked, CloudFormation requires CAPABILITY_NAMED_IAM.
Incorrect — that capability is for templates that use macros or the Serverless transform, not for named IAM resources.
Incorrect — creating IAM resources always requires an explicit capability; CloudFormation will not mint permissions unsigned.
03A create failed and the event stream (newest first) reads: ROLLBACK_COMPLETE my-app; DELETE_COMPLETE AppRole; DELETE_IN_PROGRESS AppRole; ROLLBACK_IN_PROGRESS my-app; CREATE_FAILED AppBucket 'my-app-assets already exists'; CREATE_IN_PROGRESS AppBucket; CREATE_COMPLETE AppRole. Which line is the root cause?
Incorrect — that top line is just where the stack ended up after unwinding, not why it failed.
Incorrect — AppRole was deleted only as cleanup during rollback; it had built successfully.
Correct — you read the stream from the bottom up, and the first CREATE_FAILED is the real cause; everything above it is CloudFormation unwinding the resources that had succeeded.
Incorrect — a CREATE_COMPLETE is a success, not the failure; the bucket's CREATE_FAILED is the culprit.

Try this

Run aws cloudformation wait stack-create-complete --stack-name my-app on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.

Takeaway

The trap worth remembering here: termination protection is narrower than it sounds. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related