Skip to main content
AWS · lesson 8 of 10
AWS intermediate Lesson 8 of 10

CloudFormation Basics

Deploy a minimal CloudFormation stack, break it on purpose with a bad parameter, and read exactly what a failed rollback prints in the events log.

By SkillByExample · Published

aws cloudformation iac

CloudFormation turns a set of resources into one named, versioned unit: a template describes the desired state, and CloudFormation works out how to get there, including undoing itself cleanly if something goes wrong partway through.

This lesson runs on LocalStack via awslocal — CloudFormation with S3 and IAM resources is well-supported in the open-source Community edition, so everything here, including the failed rollback, needs no AWS account.

A minimal template

# stack.yaml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  BucketName:
    Type: String

Resources:
  ReportsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      VersioningConfiguration:
        Status: Enabled

Outputs:
  BucketArn:
    Value: !GetAtt ReportsBucket.Arn

Deploying it

awslocal cloudformation deploy \
  --template-file stack.yaml \
  --stack-name reports-stack \
  --parameter-overrides BucketName=acme-reports-2026-a91f
$ awslocal cloudformation deploy --template-file stack.yaml --stack-name reports-stack --parameter-overrides BucketName=acme-reports-2026-a91f

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - reports-stack
awslocal cloudformation describe-stacks --stack-name reports-stack \
  --query 'Stacks[0].{Status:StackStatus,Outputs:Outputs}'
{
    "Status": "CREATE_COMPLETE",
    "Outputs": [
        {
            "OutputKey": "BucketArn",
            "OutputValue": "arn:aws:s3:::acme-reports-2026-a91f"
        }
    ]
}

Re-running the deploy: it’s idempotent

awslocal cloudformation deploy \
  --template-file stack.yaml \
  --stack-name reports-stack \
  --parameter-overrides BucketName=acme-reports-2026-a91f
$ awslocal cloudformation deploy --template-file stack.yaml --stack-name reports-stack --parameter-overrides BucketName=acme-reports-2026-a91f

Waiting for changeset to be created..
No changes to deploy. Stack reports-stack is up to date

Nothing was recreated — deploy diffed the current template against the running stack and found no drift.

Breaking it on purpose

Give the bucket a name that violates S3 naming rules (uppercase letters), forcing a real failure:

# stack-broken.yaml — same template, BucketName parameter will be invalid
awslocal cloudformation deploy \
  --template-file stack.yaml \
  --stack-name reports-stack-broken \
  --parameter-overrides BucketName=Acme_Reports_INVALID
$ awslocal cloudformation deploy --template-file stack.yaml --stack-name reports-stack-broken --parameter-overrides BucketName=Acme_Reports_INVALID

Waiting for changeset to be created..
Waiting for stack create/update to complete

Failed to create/update the stack. Run the following command
to fetch the list of events leading up to the failure
aws cloudformation describe-stack-events --stack-name reports-stack-broken

Reading the rollback

awslocal cloudformation describe-stack-events --stack-name reports-stack-broken \
  --query 'StackEvents[].{Resource:LogicalResourceId,Status:ResourceStatus,Reason:ResourceStatusReason}' \
  --output table
$ awslocal cloudformation describe-stack-events --stack-name reports-stack-broken --query 'StackEvents[].{Resource:LogicalResourceId,Status:ResourceStatus,Reason:ResourceStatusReason}' --output table
----------------------------------------------------------------------------------------------------------
|                                     DescribeStackEvents                                                  |
+--------------------+-------------------------------+---------------------------------------------------+
|  reports-stack-broken | ROLLBACK_COMPLETE            |  The following resource(s) failed to create        |
|  ReportsBucket        | DELETE_COMPLETE              |  -                                                  |
|  ReportsBucket        | DELETE_IN_PROGRESS           |  -                                                  |
|  reports-stack-broken | ROLLBACK_IN_PROGRESS         |  The following resource(s) failed to create:        |
|                        |                               |  [ReportsBucket]                                    |
|  ReportsBucket         | CREATE_FAILED                |  Invalid bucket name "Acme_Reports_INVALID":         |
|                        |                               |  Bucket name must not contain uppercase characters   |
|  ReportsBucket         | CREATE_IN_PROGRESS           |  -                                                  |
|  reports-stack-broken | CREATE_IN_PROGRESS           |  User Initiated                                    |
+--------------------+-------------------------------+---------------------------------------------------+

Reading bottom-to-top (events list newest first): the stack started CREATE_IN_PROGRESS, the bucket failed with the exact S3 naming error, CloudFormation immediately began ROLLBACK_IN_PROGRESS, deleted the bucket it had not actually finished creating, and the stack settled at ROLLBACK_COMPLETE — not deleted, but not created either. A stack in ROLLBACK_COMPLETE can only be deleted, never updated in place; the fix requires deleting it and deploying fresh.

awslocal cloudformation describe-stacks --stack-name reports-stack-broken \
  --query 'Stacks[0].StackStatus'
"ROLLBACK_COMPLETE"

Cleaning up

awslocal cloudformation delete-stack --stack-name reports-stack-broken
awslocal cloudformation delete-stack --stack-name reports-stack
$ awslocal cloudformation describe-stacks --stack-name reports-stack
An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with
id reports-stack does not exist

Deletion removes the bucket too, in dependency order — the whole point of tracking resources as one stack rather than issuing S3 and IAM commands independently.

Practice

1. Deploy the stack a second time with a different BucketName and confirm it updates rather than recreating the stack.
awslocal cloudformation deploy --template-file stack.yaml \
  --stack-name reports-stack --parameter-overrides BucketName=acme-reports-2026-b22c
awslocal cloudformation describe-stack-events --stack-name reports-stack \
  --query 'StackEvents[0].ResourceStatus'
"UPDATE_COMPLETE"

UPDATE_COMPLETE, not CREATE_COMPLETE — same stack, new resource properties.

2. Generate a change set without applying it, and read what it would do.
awslocal cloudformation create-change-set --stack-name reports-stack \
  --template-body file://stack.yaml \
  --parameters ParameterKey=BucketName,ParameterValue=acme-reports-2026-c33d \
  --change-set-name preview
awslocal cloudformation describe-change-set --change-set-name preview \
  --stack-name reports-stack --query 'Changes[].ResourceChange.{Action:Action,Resource:LogicalResourceId,Replacement:Replacement}'
[
    {
        "Action": "Modify",
        "Resource": "ReportsBucket",
        "Replacement": "True"
    }
]

Replacement: "True" is the important field — renaming an S3 bucket cannot be done in-place, so this change would delete the old bucket and create a new one, losing its contents. A change set surfaces that before anything runs.

3. Add a second resource that depends on the bucket and watch dependency-ordered rollback.
Resources:
  ReportsBucket:
    Type: AWS::S3::Bucket
  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref ReportsBucket
      PolicyDocument: { "Version": "2012-10-17", "Statement": [] }

An empty Statement list is invalid and fails; the events log shows BucketPolicy fail first, then ReportsBucket roll back second — CloudFormation unwinds dependents before their dependencies.

4. List all stacks and their statuses in the account.
awslocal cloudformation list-stacks \
  --query 'StackSummaries[].{Name:StackName,Status:StackStatus}'
[
    {
        "Name": "reports-stack",
        "Status": "UPDATE_COMPLETE"
    }
]

list-stacks includes deleted stacks by default for a while (DELETE_COMPLETE) unless filtered — useful for confirming a cleanup actually finished.

5. Add a Deletion Policy so the bucket survives a stack delete.
ReportsBucket:
  Type: AWS::S3::Bucket
  DeletionPolicy: Retain
awslocal cloudformation delete-stack --stack-name reports-stack
awslocal s3 ls s3://acme-reports-2026-b22c
2026-09-13 12:10:44          0

The bucket is still there after the stack is gone — DeletionPolicy: Retain is the standard guard for anything holding data you cannot regenerate, like a production database or a bucket full of user uploads.

Next: CloudWatch — metrics, log groups, and an alarm that actually fires.

Frequently Asked Questions

Can I run this lesson on LocalStack?
Yes. CloudFormation is one of the services LocalStack's open-source Community edition supports well for the resource types used here (S3, IAM), so this entire lesson runs through awslocal with no AWS account and no billing.
What does CloudFormation actually do that running the CLI commands by hand doesn't?
It tracks a stack as one unit with a defined desired state, so re-running the same template is idempotent (create-or-update, not duplicate), a failed deploy rolls back automatically to the last known-good state, and deleting the stack deletes everything it created in dependency order. Running equivalent aws s3, aws iam commands by hand gives you none of that bookkeeping.
Why does a stack automatically roll back on a failed create?
CloudFormation's default behavior treats a stack as all-or-nothing: if any resource fails to create, it deletes everything already created in that same stack, in reverse dependency order, so you never end up with an untracked partial deployment. This is why the events log always shows a mix of CREATE_FAILED, then DELETE_IN_PROGRESS entries for resources that had actually succeeded moments earlier.
What is the difference between aws cloudformation deploy and create-stack?
create-stack only creates a new stack and fails if one already exists; it also requires manually polling for completion. deploy is a higher-level command that creates the stack if it does not exist or updates it via a change set if it does, and it blocks until the operation finishes — which is why it is the command actually used in day-to-day work and in this lesson.