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.
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=platformaws cloudformation wait stack-create-complete --stack-name orders-apiWhen 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.
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_IAMaws 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-1758012345ROLLBACK_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-apiIf 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.InnerResourceSkipped 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 â¦aws cloudformation update-termination-protection \
--stack-name orders-api --enable-termination-protectionTermination 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: RetainUpdateReplacePolicy 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"}
]
}'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 tableDetection 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.
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-idNo hard coupling, and the consumer picks up changes on its next update.
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=25With 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.
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.
| Task | Command |
|---|---|
| Create | aws cloudformation create-stack --capabilities CAPABILITY_IAM |
| Preview an update | create-change-set then describe-change-set |
| Apply | aws cloudformation execute-change-set |
| Why did it fail? | describe-stack-events filtered to *_FAILED |
| Unstick a rollback | continue-update-rollback [--resources-to-skip â¦] |
| Protect | update-termination-protection --enable-termination-protection |
| Detect drift | detect-stack-drift then describe-stack-resource-drifts |
| Read outputs | describe-stacks --query 'Stacks[0].Outputs' |
| Delete | aws cloudformation delete-stack --stack-name <s> |
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.DeletionPolicy and UpdateReplacePolicy to Retain on anything holding data.<MCQExercise exercise={{ id: 'aws-cfn-stacks-quiz1', title: 'A successful update that lost the data', description: '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?', options: [ { id: 'a', text: 'Creating a change set first and checking the Replacement column, which would have shown True for the table' }, { id: 'b', text: 'Enabling termination protection on the stack before updating' }, { id: 'c', text: 'Running drift detection, which reports resources that will be replaced' }, { id: 'd', text: 'Using --disable-rollback so the old table would have been preserved' }, ], correctId: 'a', explanation: 'Some properties cannot be modified in place, and a DynamoDB key schema is one of them, so CloudFormation deletes the table and creates a new one, which it counts as a successful update. A change set exposes this before anything happens: describe-change-set shows Replacement: True against that resource, which is the signal to stop. Setting DeletionPolicy and UpdateReplacePolicy to Retain on stateful resources is the belt-and-braces protection, since UpdateReplacePolicy covers exactly this case where the stack survives but the resource does not.', wrongExplanations: { b: 'Termination protection blocks stack deletion. It has no effect on resource replacement during an update.', c: 'Drift detection compares live resources against the template as it stands; it does not predict what a future update would do.', d: '--disable-rollback changes behaviour after a failure. This update did not fail.', }, }} />
<MCQExercise exercise={{ id: 'aws-cfn-stacks-quiz2', title: 'A networking stack that cannot be changed', description: '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?', options: [ { id: 'a', text: 'Fn::ImportValue creates a hard dependency that locks the exported value, so consumers must stop importing it before the exporter can change' }, { id: 'b', text: 'Exports may only be consumed by one stack at a time' }, { id: 'c', text: 'The networking stack needs termination protection disabled before any update' }, { id: 'd', text: 'Cross-stack references require all stacks to be updated in a single StackSet operation' }, ], correctId: 'a', explanation: 'CloudFormation protects exported values so a consumer cannot be broken by a change upstream. The exporting stack is effectively frozen for as long as anything imports it. That is genuine protection and also a real obstacle on shared foundational stacks. Unpicking it means updating each consumer to stop importing, changing the exporter, then reconnecting. Publishing the value to SSM Parameter Store instead and consuming it as an AWS::SSM::Parameter::Value parameter avoids the coupling entirely, at the cost of losing the guarantee.', wrongExplanations: { b: 'Any number of stacks may import an export; the lock applies regardless of how many.', c: 'Termination protection only blocks deletion of the stack itself.', d: 'StackSets replicate a stack across accounts and regions and do nothing about export dependencies.', }, }} />
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.
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.
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.
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.
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.
Write maintainable CloudFormation templates with typed parameters, intrinsic functions, conditions, DependsOn and custom resources, then lint with cfn-lint.
Deploy an app with Elastic Beanstalk without managing servers. Compare deployment policies and downtime tradeoffs, blue-green URL swaps and .ebextensions.
Use AWS KMS correctly: why key policies come before IAM, AWS managed vs customer managed keys, envelope encryption, encryption context, rotation and grants.