AWS CloudFormation

By Vishvesh Patel · DevOps EngineerUpdated September 11, 2026

Define AWS infrastructure as code. Template anatomy, change sets, drift detection, rollback behaviour, deletion policies and how it compares to Terraform.

CloudFormation is AWS's native infrastructure-as-code service. You describe resources in a YAML or JSON template, and CloudFormation works out the order to create them, tracks what it created in a stack, and can tear the whole thing down again.

Its defining characteristic, for better and worse, is that the state lives in AWS. There's no state file to store, lock, or accidentally commit. In exchange, you get whatever recovery options AWS gives you when a stack wedges, and no more.

Template anatomy

AWSTemplateFormatVersion: '2010-09-09'
Description: Payments service networking

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, staging, prod]
    Description: Deployment environment

Mappings:
  EnvConfig:
    dev:     { InstanceType: t3.small }
    staging: { InstanceType: t3.medium }
    prod:    { InstanceType: m6i.large }

Conditions:
  IsProd: !Equals [!Ref Environment, prod]

Resources:
  AppBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Sub 'payments-${Environment}-${AWS::AccountId}'
      VersioningConfiguration:
        Status: !If [IsProd, Enabled, Suspended]
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

Outputs:
  BucketName:
    Value: !Ref AppBucket
    Export:
      Name: !Sub '${AWS::StackName}-BucketName'

Only Resources is required. The pieces that earn their place quickly:

  • Parameters with AllowedValues turn a typo into a rejection instead of a broken deploy.
  • Conditions let one template serve every environment, which is the main reason to prefer them over copy-pasted templates that drift apart.
  • Outputs with Export publish values other stacks can import, though see the warning below.
  • !Sub with ${AWS::AccountId} keeps globally-unique names unique without hardcoding.

DeletionPolicy: Retain on stateful resources is the single most valuable line in that template. Without it, deleting a stack deletes the bucket and everything in it. UpdateReplacePolicy covers the sneakier case: a property change that forces replacement rather than an in-place update.

Deploying

aws cloudformation deploy is the command to use. It creates the stack if it doesn't exist and updates it if it does:

aws cloudformation deploy \
  --template-file template.yaml \
  --stack-name payments-network-prod \
  --parameter-overrides Environment=prod \
  --capabilities CAPABILITY_NAMED_IAM \
  --tags Service=payments Environment=prod

--capabilities is mandatory whenever a template creates IAM resources. It's an explicit acknowledgement that you're about to grant permissions, CAPABILITY_IAM for unnamed roles, CAPABILITY_NAMED_IAM when you set a role name yourself.

Validate before you deploy:

aws cloudformation validate-template --template-body file://template.yaml

That only checks syntax. For real linting, deprecated properties, invalid values, security issues, use cfn-lint, which catches far more:

pip install cfn-lint && cfn-lint template.yaml

Change sets

Never update a production stack without previewing the plan. A change set is CloudFormation's equivalent of terraform plan:

aws cloudformation create-change-set \
  --stack-name payments-network-prod \
  --change-set-name review-$(date +%s) \
  --template-body file://template.yaml \
  --parameters ParameterKey=Environment,ParameterValue=prod \
  --capabilities CAPABILITY_NAMED_IAM

aws cloudformation describe-change-set \
  --stack-name payments-network-prod \
  --change-set-name review-1757600000 \
  --query 'Changes[].ResourceChange.{Action:Action,Resource:LogicalResourceId,Replacement:Replacement}' \
  --output table
-----------------------------------------------------
|                 DescribeChangeSet                 |
+----------+------------------------+---------------+
|  Action  |       Resource         |  Replacement  |
+----------+------------------------+---------------+
|  Modify  |  AppBucket             |  False        |
|  Modify  |  AppDatabase           |  True         |
+----------+------------------------+---------------+

Replacement: True is the column that matters. It means CloudFormation will create a new resource and delete the old one. On a database or an EBS volume, that is data loss delivered by an apparently routine property change. Read every change set for that column before executing.

aws cloudformation deploy --no-execute-changeset produces the same preview in one step.

When a stack gets stuck

This is where CloudFormation's managed state cuts both ways.

UPDATE_ROLLBACK_FAILED is the one that hurts. An update failed, the rollback also failed, and the stack now accepts no further updates. Recovery:

# Skip the resources that cannot roll back
aws cloudformation continue-update-rollback \
  --stack-name payments-network-prod \
  --resources-to-skip AppDatabase

Skipped resources drift out of CloudFormation's view, so you must reconcile them by hand afterwards.

DELETE_FAILED is usually a dependency CloudFormation didn't create: a non-empty S3 bucket, an ENI still attached, a security group referenced by something outside the stack. Empty or detach it, then retry the delete.

Rollback on create failure deletes everything, which makes debugging awkward. Disable it while iterating:

aws cloudformation create-stack --stack-name test --template-body file://template.yaml \
  --on-failure DO_NOTHING

Drift detection

Someone will change something in the console. Drift detection finds it:

aws cloudformation detect-stack-drift --stack-name payments-network-prod

aws cloudformation describe-stack-resource-drifts \
  --stack-name payments-network-prod \
  --stack-resource-drift-status-filters MODIFIED DELETED

Run it on a schedule via EventBridge rather than only during incidents. Be aware that drift detection doesn't support every resource type, so a clean report is reassuring rather than conclusive.

Structuring larger systems

Don't put everything in one template. A 3,000-line template is slow to deploy, dangerous to change, and impossible to review.

Nested stacks compose child templates into a parent, sharing a lifecycle:

Resources:
  Network:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.amazonaws.com/my-templates/network.yaml
      Parameters:
        Environment: !Ref Environment

Cross-stack references with Export/ImportValue link independent stacks, but the coupling is rigid: you cannot delete or modify an exported value while another stack imports it, and untangling that in production is genuinely painful. Prefer passing values as parameters, or reading them from Parameter Store, unless you specifically want that hard dependency.

StackSets deploy one template across many accounts and regions, which is how you roll out baseline guardrails to an entire AWS Organization.

CloudFormation or Terraform?

CloudFormationTerraform
StateManaged by AWSYou store and lock it
ProvidersAWS onlyAWS plus hundreds of others
New AWS featuresSometimes lags the APIUsually fast, occasionally lags
PreviewChange setsterraform plan
LanguageYAML/JSON, or CDK to generate itHCL
Recovery from bad stateLimited to what AWS offersFull control via state commands
CostFreeFree (Terraform Cloud is paid)

Choose CloudFormation if you're AWS-only, want no state to manage, or need StackSets for organisation-wide rollouts. Choose Terraform if you touch more than one provider, or you want the ability to surgically repair state. The UPDATE_ROLLBACK_FAILED situation has no real Terraform equivalent because you can always edit state directly.

If YAML is the sticking point rather than the engine, AWS CDK lets you write TypeScript or Python that synthesises CloudFormation templates, giving you loops, types, and testing while keeping managed state.

Quick reference

TaskCommand
Create or updateaws cloudformation deploy --template-file <f> --stack-name <s>
Preview changesaws cloudformation deploy --no-execute-changeset ...
Stack statusaws cloudformation describe-stacks --stack-name <s>
Watch eventsaws cloudformation describe-stack-events --stack-name <s> --max-items 20
Read outputsaws cloudformation describe-stacks --stack-name <s> --query 'Stacks[0].Outputs'
Detect driftaws cloudformation detect-stack-drift --stack-name <s>
Unstick a rollbackaws cloudformation continue-update-rollback --stack-name <s> --resources-to-skip <r>
Deleteaws cloudformation delete-stack --stack-name <s>

Key takeaways

  • DeletionPolicy: Retain and UpdateReplacePolicy: Retain on stateful resources are what stand between a template change and permanent data loss.
  • Always read the change set for Replacement: True: that column is the difference between an update and a rebuild.
  • --capabilities is required for IAM resources, deliberately, as an acknowledgement that permissions are being granted.
  • UPDATE_ROLLBACK_FAILED needs continue-update-rollback --resources-to-skip, and leaves you to reconcile the skipped resources manually.
  • Export/ImportValue creates a hard dependency that blocks changes to the exporting stack. Prefer parameters or Parameter Store.
  • cfn-lint catches what validate-template misses: the latter only checks syntax.

Check your understanding

Exercise

Reading a change set

A change set for a production stack shows Modify on an RDS instance with Replacement: True. What will happen if you execute it?

Exercise

A stack that will not update

A networking stack exports SubnetId. Updating it to rename that export fails with "Export cannot be updated as it is in use". Why?


Frequently Asked Questions

How do I fix a stack stuck in UPDATE_ROLLBACK_FAILED?

Run aws cloudformation continue-update-rollback --stack-name <name>. If it fails again, add --resources-to-skip with the logical IDs of the resources blocking the rollback, which you can find in describe-stack-events. Skipping means CloudFormation stops tracking their real state, so afterwards you must inspect those resources and either bring them back in line with the template or import them again. The usual root causes are a resource deleted outside CloudFormation and an IAM permission that was revoked mid-update.

What is the difference between a change set and just deploying?

A change set computes what would change and shows it without applying anything, the same role terraform plan plays. It tells you which resources are added, modified or deleted and, critically, which will be replaced rather than updated in place. Deploying without one means finding out at execution time, which on a stateful resource can mean discovering the replacement after the original is gone.

Does deleting a CloudFormation stack delete my data?

Yes, by default. Deleting a stack deletes every resource it created, including S3 buckets, RDS instances and EBS volumes. Protect anything stateful with DeletionPolicy: Retain (keep the resource, orphaned from the stack) or DeletionPolicy: Snapshot where supported. Add UpdateReplacePolicy too, since a replacement during an update deletes the original by a different code path that DeletionPolicy alone does not cover. Enabling termination protection on the stack adds another safeguard.

CloudFormation or Terraform, which should I learn?

Terraform if you only learn one, because it works across AWS, Azure, GCP, Kubernetes, Datadog and hundreds of other providers, and the skill transfers between employers. CloudFormation is worth knowing if you are in an AWS-only environment, particularly for StackSets, which deploy a template across every account in an Organization with no equivalent in open-source Terraform. Many AWS teams run both: CloudFormation or CDK for account baselines, Terraform for application infrastructure.

Why does my template fail with "Requires capabilities: [CAPABILITY_IAM]"?

The template creates IAM resources, a role, policy, user or instance profile, and CloudFormation requires you to acknowledge that explicitly rather than granting permissions as an invisible side effect. Add --capabilities CAPABILITY_IAM, or CAPABILITY_NAMED_IAM if any IAM resource has an explicit name. A third value, CAPABILITY_AUTO_EXPAND, is needed for templates using macros or transforms such as AWS SAM.

How do I split a large template without creating a mess?

Nested stacks are the tighter option: a parent template declares AWS::CloudFormation::Stack children, they deploy and delete as one unit, and values pass down as parameters. Independent stacks linked by Export/ImportValue are more loosely coupled on the surface but create a dependency that blocks the exporting stack from changing while consumers exist. For most teams the cleanest pattern is independent stacks that pass values as parameters or read them from Parameter Store, keeping stacks genuinely separable.

Continue Learning

Explore Related Topics

When it breaks

Related Resources