AWS CloudFormation Stacks

By Vishvesh Patel · DevOps EngineerUpdated September 16, 2026

Operate stacks in practice. Change sets before every update, recovering from UPDATE_ROLLBACK_FAILED and ROLLBACK_COMPLETE, drift and deletion policies.

CloudFormation Stacks

A stack is a set of AWS resources managed as one unit, created from a template. Create the stack and the resources appear in dependency order; delete it and they are removed in reverse. That atomicity is the point, and the failure modes below are what you spend your time on.

Create, and watch it properly

aws cloudformation create-stack --stack-name orders-api \
  --template-body file://template.yaml \
  --parameters ParameterKey=Environment,ParameterValue=prod \
  --capabilities CAPABILITY_IAM \
  --enable-termination-protection \
  --tags Key=Owner,Value=platform
aws cloudformation wait stack-create-complete --stack-name orders-api

When something fails, the stack events hold the answer, and the first failure is the one that matters, since everything after it is consequence:

aws cloudformation describe-stack-events --stack-name orders-api \
  --query 'StackEvents[?ResourceStatus==`CREATE_FAILED`].[LogicalResourceId,ResourceStatusReason]' \
  --output table
-----------------------------------------------------------------------
| OrdersTable | Resource of type 'AWS::DynamoDB::Table' with identifier
|             | 'Orders' already exists.                                |
-----------------------------------------------------------------------

--capabilities CAPABILITY_IAM is required for any template creating roles or policies, and CAPABILITY_NAMED_IAM when those have explicit names. It exists so a template cannot quietly grant itself permissions you did not review.

Never update without a change set

update-stack applies changes immediately. A change set shows you what it would do first, and the distinction that matters is Replacement. A resource being deleted and recreated rather than modified in place.

aws cloudformation create-change-set --stack-name orders-api \
  --change-set-name review-$(date +%s) \
  --template-body file://template.yaml \
  --parameters ParameterKey=Environment,ParameterValue=prod \
  --capabilities CAPABILITY_IAM
aws cloudformation describe-change-set --stack-name orders-api \
  --change-set-name review-1758012345 \
  --query 'Changes[].ResourceChange.[Action,LogicalResourceId,ResourceType,Replacement]' \
  --output table
------------------------------------------------------------------
| Modify | OrdersApi    | AWS::ApiGatewayV2::Api    | False       |
| Modify | OrdersTable  | AWS::DynamoDB::Table      | True        |  ← data loss
------------------------------------------------------------------

Replacement: True on a database, a table or a volume means the existing one is deleted and a new empty one created. The stack update reports success and the data is gone.

Changing a DynamoDB table's key schema, an RDS instance's identifier, or an EC2 instance's availability zone all trigger replacement. Reading this table before every production update is the single most valuable CloudFormation habit.

aws cloudformation execute-change-set --stack-name orders-api \
  --change-set-name review-1758012345

The two rollback states you will meet

ROLLBACK_COMPLETE: the stack's first creation failed and rolled back. It cannot be updated, only deleted:

An error occurred (ValidationError): Stack:arn:... is in ROLLBACK_COMPLETE
state and can not be updated.

Delete it and create again. This only happens on initial creation; a failed update produces UPDATE_ROLLBACK_COMPLETE, which is a healthy state.

UPDATE_ROLLBACK_FAILED: worse. An update failed, and the rollback failed too, so the stack is stuck and refuses everything.

aws cloudformation continue-update-rollback --stack-name orders-api

If a specific resource cannot roll back, typically one deleted outside CloudFormation, or a security group with a dependency that no longer exists. Skip it and reconcile by hand:

aws cloudformation continue-update-rollback --stack-name orders-api \
  --resources-to-skip OrdersTable NestedStack.InnerResource

Skipped resources leave the stack's view of reality wrong, so follow up with drift detection.

To avoid the state entirely, keep --on-failure ROLLBACK (the default) and disable rollback only when you genuinely want to inspect a half-built stack:

aws cloudformation create-stack --stack-name debug-me --disable-rollback …

Protecting what matters

aws cloudformation update-termination-protection \
  --stack-name orders-api --enable-termination-protection

Termination protection blocks delete-stack entirely. Enable it on anything stateful.

DeletionPolicy protects individual resources even when the stack goes:

  OrdersTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain

UpdateReplacePolicy is the one people omit, and it is the one that saves you. It covers the replacement case above, where the stack is not being deleted but the resource is.

A stack policy can deny updates to specific resources:

aws cloudformation set-stack-policy --stack-name orders-api --stack-policy-body '{
  "Statement": [
    {"Effect": "Allow", "Action": "Update:*", "Principal": "*", "Resource": "*"},
    {"Effect": "Deny", "Action": "Update:Replace", "Principal": "*",
     "Resource": "LogicalResourceId/OrdersTable"}
  ]
}'

Drift

Resources changed outside CloudFormation, a console edit during an incident, another tool, are drift, and CloudFormation neither notices nor corrects it until the next update, which may overwrite the fix or fail confusingly.

aws cloudformation detect-stack-drift --stack-name orders-api
aws cloudformation describe-stack-resource-drifts --stack-name orders-api \
  --stack-resource-drift-status-filters MODIFIED DELETED \
  --query 'StackResourceDrifts[].[LogicalResourceId,StackResourceDriftStatus]' --output table

Detection is asynchronous, so poll describe-stack-drift-detection-status with the returned id. Run it before any significant update to a stack you do not fully control, and on a schedule for production.

Cross-stack references

Two ways to share values, with very different consequences.

Exports and Fn::ImportValue create a hard dependency:

# networking stack
Outputs:
  VpcId:
    Value: !Ref Vpc
    Export:
      Name: !Sub '${AWS::StackName}-VpcId'
# application stack
  Subnet:
    Properties:
      VpcId: !ImportValue 'networking-VpcId'

An exported value cannot be changed or deleted while anything imports it, so the networking stack becomes effectively frozen. That is sometimes protection and often an obstacle.

SSM Parameter Store is looser and usually better:

Parameters:
  VpcId:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /platform/vpc-id

No hard coupling, and the consumer picks up changes on its next update.

Nested stacks or StackSets

Different problems, frequently confused.

Nested stacks break one large template into reusable components deployed together as a parent and children. They keep templates under the 500-resource and 1 MB limits and allow reuse. The cost is worse error messages. A failure inside a child surfaces as a generic failure on the parent, so you follow the event chain down.

StackSets deploy the same stack across many accounts and regions:

aws cloudformation create-stack-instances --stack-set-name baseline-guardrails \
  --deployment-targets OrganizationalUnitIds=ou-abc1-12345678 \
  --regions eu-west-1 us-east-1 \
  --operation-preferences FailureTolerancePercentage=10,MaxConcurrentPercentage=25

With Organizations integration, service-managed StackSets deploy automatically to new accounts as they are created, which is how a baseline of logging, guardrails and IAM roles lands everywhere without anyone remembering to apply it.

Common errors

Stack ... is in ROLLBACK_COMPLETE state and can not be updated.

Failed initial creation. Delete and recreate.

No updates are to be performed.

CloudFormation sees no difference. Real when nothing changed; misleading when you edited a file you are not actually submitting.

Export networking-VpcId cannot be updated as it is in use by app-stack

Remove the import first, or use SSM parameters instead.

Resource of type 'AWS::S3::Bucket' with identifier 'my-bucket' already exists

Something outside the stack owns that name. Use import-stacks-to-stack-set or create-change-set --import-existing-resources to adopt it rather than renaming.

Quick reference

TaskCommand
Createaws cloudformation create-stack --capabilities CAPABILITY_IAM
Preview an updatecreate-change-set then describe-change-set
Applyaws cloudformation execute-change-set
Why did it fail?describe-stack-events filtered to *_FAILED
Unstick a rollbackcontinue-update-rollback [--resources-to-skip …]
Protectupdate-termination-protection --enable-termination-protection
Detect driftdetect-stack-drift then describe-stack-resource-drifts
Read outputsdescribe-stacks --query 'Stacks[0].Outputs'
Deleteaws cloudformation delete-stack --stack-name <s>

Key takeaways

  • Always create a change set and read the Replacement column before a production update.
  • Replacement: True on a stateful resource means data loss, reported as a successful update.
  • ROLLBACK_COMPLETE cannot be updated: delete and recreate; it only affects first creation.
  • UPDATE_ROLLBACK_FAILED needs continue-update-rollback, skipping resources if necessary.
  • Set DeletionPolicy and UpdateReplacePolicy to Retain on anything holding data.
  • Enable termination protection on stateful stacks.
  • Exports freeze the exporting stack. Prefer SSM parameters for loose coupling.
  • Nested stacks compose one deployment; StackSets replicate across accounts.

Check your understanding

Exercise

A successful update that lost the data

A CloudFormation update changed a DynamoDB table's partition key. The stack reported UPDATE_COMPLETE and the table is now empty. What would have prevented this?

Exercise

A networking stack that cannot be changed

A networking stack exports its VPC ID, and three application stacks import it. Attempts to update the networking stack fail with "Export cannot be updated as it is in use". What is the underlying issue?


Frequently Asked Questions

What is a change set and why should I always use one?

A change set is a preview: CloudFormation compares your new template against the deployed stack and lists what it would add, modify or delete, without doing any of it. The column that matters is Replacement, True means a resource will be deleted and recreated rather than modified, so a database or table would come back empty while the update reports success. Reading that list before every production update is the single most valuable CloudFormation habit, and it costs one extra command.

How do I fix a stack stuck in UPDATE_ROLLBACK_FAILED?

Run aws cloudformation continue-update-rollback. If a particular resource is what blocks it, commonly one deleted outside CloudFormation, or a security group whose dependency has gone. Add --resources-to-skip naming those logical IDs, which lets the rollback finish while leaving those resources unmanaged. Afterwards run drift detection, because skipping resources means the stack's view of reality is now wrong and the next update may behave unexpectedly.

Why can't I update a stack in ROLLBACK_COMPLETE?

Because that state means the stack's very first creation failed and was rolled back, so there is no previously working configuration to update from. CloudFormation only allows deleting it. Delete the stack and create it again with the corrected template. A stack that has succeeded at least once never enters this state; a failed update leaves UPDATE_ROLLBACK_COMPLETE, which is perfectly updatable.

What is CloudFormation drift and how do I handle it?

Drift is a difference between what the template says and what actually exists, caused by changes made outside CloudFormation. A console edit during an incident, or another tool touching the same resources. CloudFormation does not notice or correct it automatically, so the next update may overwrite the manual fix or fail in a confusing way. Run detect-stack-drift before significant updates and on a schedule for production, then either bring the change into the template or revert it.

When should I use nested stacks versus StackSets?

They solve unrelated problems. Nested stacks split one large deployment into reusable child templates deployed together, keeping you under the resource and template size limits and allowing components to be shared between stacks. StackSets deploy the same stack to many accounts and regions at once, which is how organisation-wide baselines are applied, and with Organizations integration, new accounts receive them automatically as they are created.

How do I stop CloudFormation deleting my database?

Two settings, both needed. DeletionPolicy: Retain keeps the resource when the stack is deleted. UpdateReplacePolicy: Retain keeps it when an update would otherwise replace it. The case people forget, because the stack is not being deleted and the loss still happens. Add termination protection on the stack itself, and a stack policy denying Update:Replace on the specific resource if you want a hard block rather than a retained orphan.

Continue Learning

Explore Related Topics

When it breaks

Related Resources