# AWS CloudFormation Cheatsheet

- URL: https://karandeepsingh.ca/cheatsheets/devops/cloudformation/
- Author: Karandeep Singh
- Published: 2025-07-15
- Tags: aws, automation
- Summary: Quick-reference CloudFormation guide. Template sections, intrinsic functions, stack lifecycle CLI commands, change sets, drift detection, and error fixes.

CloudFormation is still my default for AWS-native infrastructure. No state file to babysit, and rollbacks come free. This is the reference I wish I had when I started: template anatomy, the intrinsic functions I actually use, and the CLI commands for the full stack lifecycle. For imperative AWS scripting I reach for [boto3](https://karandeepsingh.ca/cheatsheets/python/python-boto3/) instead, and config management after provisioning goes to [Ansible](https://karandeepsingh.ca/cheatsheets/devops/ansible/).

## Template Anatomy

> **Docs:** [Template anatomy, CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-anatomy.html)

```yaml
AWSTemplateFormatVersion: '2010-09-09'   # only valid value
Description: App network layer

Parameters:        # runtime inputs
  EnvName:
    Type: String
    Default: dev
    AllowedValues: [dev, staging, prod]
  VpcCidr:
    Type: String
    Default: 10.0.0.0/16
    AllowedPattern: ^\d+\.\d+\.\d+\.\d+/\d+$

Mappings:          # static lookup tables
  RegionAmi:
    us-east-1: { Ami: ami-0abc1234 }
    ca-central-1: { Ami: ami-0def5678 }

Conditions:        # booleans evaluated at deploy time
  IsProd: !Equals [!Ref EnvName, prod]

Resources:         # the only REQUIRED section
  MyVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr

Outputs:           # values to display/export
  VpcId:
    Value: !Ref MyVpc
    Export:
      Name: !Sub '${EnvName}-vpc-id'
```

Other top-level sections: `Metadata`, `Rules` (parameter validation), `Transform` (`AWS::Serverless-2016-10-31` for SAM, `AWS::LanguageExtensions` for `Fn::ForEach`).

## Intrinsic Functions

> **Docs:** [Intrinsic function reference, CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html)

| Function | Short form | What it does |
|---|---|---|
| `Ref` | `!Ref` | Parameter value or resource's default ID (e.g. VPC ID, bucket name) |
| `Fn::GetAtt` | `!GetAtt Res.Attr` | Named attribute of a resource (`!GetAtt Db.Endpoint.Address`) |
| `Fn::Sub` | `!Sub '${Var}-x'` | String interpolation of params, resources, pseudo params |
| `Fn::Join` | `!Join [':', [a, b]]` | Join list with delimiter |
| `Fn::If` | `!If [Cond, valTrue, valFalse]` | Conditional value (condition from `Conditions`) |
| `Fn::ImportValue` | `!ImportValue name` | Read another stack's export |
| `Fn::Select` | `!Select [0, !GetAZs '']` | Pick item from a list (`!GetAZs` = region's AZs) |
| `Fn::Split` | `!Split [',', !Ref Csv]` | String to list |
| `Fn::FindInMap` | `!FindInMap [Map, Key, SubKey]` | Lookup in `Mappings` |

`!Sub` beats `!Join` for readability in almost every case:

```yaml
BucketArn: !Sub 'arn:${AWS::Partition}:s3:::${MyBucket}/*'
```

## Sample Template: S3 + IAM

> **Docs:** [AWS::S3::Bucket, CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html)

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Versioned artifact bucket with a scoped writer role

Parameters:
  EnvName: { Type: String, Default: dev }

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

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

  WriterRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - { Effect: Allow, Principal: { Service: ec2.amazonaws.com }, Action: 'sts:AssumeRole' }
      Policies:
        - PolicyName: write-artifacts
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: ['s3:PutObject', 's3:GetObject']
                Resource: !Sub '${ArtifactBucket.Arn}/*'

Outputs:
  BucketName:
    Value: !Ref ArtifactBucket
    Export:
      Name: !Sub '${EnvName}-artifact-bucket'
```

## CLI: Deploy & Stack Lifecycle

> **Docs:** [cloudformation, AWS CLI Command Reference](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/)

```bash
# One command, idempotent - creates or updates (my default)
aws cloudformation deploy \
  --stack-name app-dev \
  --template-file template.yaml \
  --parameter-overrides EnvName=dev VpcCidr=10.1.0.0/16 \
  --capabilities CAPABILITY_NAMED_IAM \
  --no-fail-on-empty-changeset

# Explicit create / update
aws cloudformation create-stack --stack-name app-dev \
  --template-body file://template.yaml \
  --parameters ParameterKey=EnvName,ParameterValue=dev \
  --capabilities CAPABILITY_IAM
aws cloudformation update-stack --stack-name app-dev \
  --template-body file://template.yaml \
  --parameters ParameterKey=EnvName,UsePreviousValue=true

# Wait + inspect
aws cloudformation wait stack-create-complete --stack-name app-dev
aws cloudformation describe-stacks --stack-name app-dev --query 'Stacks[0].Outputs'
aws cloudformation describe-stack-events --stack-name app-dev \
  --query 'StackEvents[?ResourceStatus==`CREATE_FAILED`]'

# Delete
aws cloudformation delete-stack --stack-name app-dev
```

## Change Sets

> **Docs:** [Updating stacks using change sets, CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html)

Preview exactly what an update will touch before executing. Mandatory habit for prod:

```bash
aws cloudformation create-change-set \
  --stack-name app-prod --change-set-name add-alarms \
  --template-body file://template.yaml \
  --capabilities CAPABILITY_NAMED_IAM

aws cloudformation describe-change-set \
  --stack-name app-prod --change-set-name add-alarms \
  --query 'Changes[].ResourceChange.{Action:Action,Resource:LogicalResourceId,Replacement:Replacement}'

aws cloudformation execute-change-set \
  --stack-name app-prod --change-set-name add-alarms
```

`Replacement: True` means the resource gets destroyed and recreated. Check this column every time.

## Drift Detection

> **Docs:** [Detecting stack drift, CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html)

```bash
aws cloudformation detect-stack-drift --stack-name app-prod
aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id <id>
aws cloudformation describe-stack-resource-drifts --stack-name app-prod \
  --stack-resource-drift-status-filters MODIFIED DELETED
```

## Stack Policies & Termination Protection

> **Docs:** [Stack policies, CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html)

```bash
# Termination protection - flip on for anything prod
aws cloudformation update-termination-protection \
  --stack-name app-prod --enable-termination-protection
```

```json
{
  "Statement": [
    { "Effect": "Allow", "Action": "Update:*", "Principal": "*", "Resource": "*" },
    { "Effect": "Deny",  "Action": ["Update:Replace", "Update:Delete"],
      "Principal": "*", "Resource": "LogicalResourceId/ProdDatabase" }
  ]
}
```

```bash
aws cloudformation set-stack-policy --stack-name app-prod \
  --stack-policy-body file://policy.json
```

Also set `DeletionPolicy: Retain` (and `UpdateReplacePolicy: Retain`) on stateful resources: databases, buckets, EFS.

## Nested Stacks & Exports

> **Docs:** [Working with nested stacks, CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html)

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

  App:
    Type: AWS::EC2::Instance
    Properties:
      SubnetId: !GetAtt NetworkStack.Outputs.PublicSubnetId
```

Cross-stack (not nested): export in one stack's `Outputs`, then `!ImportValue prod-vpc-id` elsewhere. You cannot delete or change an export while another stack imports it. Nested stacks avoid that coupling; exports make it explicit.

## Pseudo Parameters

> **Docs:** [Pseudo parameters reference, CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html)

| Parameter | Example value |
|---|---|
| `AWS::AccountId` | `123456789012` |
| `AWS::Region` | `ca-central-1` |
| `AWS::StackName` | `app-prod` |
| `AWS::Partition` | `aws` (or `aws-cn`, `aws-us-gov`) |
| `AWS::NoValue` | Removes a property when used with `!If` |

## Validation & Linting

> **Docs:** [validate-template, AWS CLI Command Reference](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/validate-template.html)

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

pip install cfn-lint
cfn-lint templates/**/*.yaml           # catches type errors, bad refs, region issues
```

`validate-template` only checks syntax; `cfn-lint` checks semantics against the real resource specs. Run both in CI.

## Rollback Behavior

**danger:**
`UPDATE_ROLLBACK_FAILED` is the state that strands a stack: it will accept no
further updates until you resolve it. `continue-update-rollback` with
`--resources-to-skip` is the way out, and the resources you skip are left
drifted — note them before you run it.

**Getting a stack out of UPDATE_ROLLBACK_FAILED**
```bash
# What actually failed, oldest first
aws cloudformation describe-stack-events --stack-name api-prod \
  --query 'StackEvents[?ResourceStatus==`UPDATE_FAILED`].[LogicalResourceId,ResourceStatusReason]' \
  --output table

# Retry the rollback; skip only what you have inspected
aws cloudformation continue-update-rollback --stack-name api-prod \
  --resources-to-skip MyBrokenResource

aws cloudformation describe-stacks --stack-name api-prod \
  --query 'Stacks[0].StackStatus'
```

Then reconcile the skipped resources by hand and run drift detection, because
the template no longer describes them.

> **Docs:** [Continue rolling back an update, CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-continueupdaterollback.html)

- Create fails → `ROLLBACK_COMPLETE`: stack is dead, delete it and recreate.
- Update fails → automatic rollback to previous working state (`UPDATE_ROLLBACK_COMPLETE`); if that rollback fails, `continue-update-rollback` (optionally `--resources-to-skip`).
- `--disable-rollback` / `--on-failure DO_NOTHING` on create keeps failed resources up for debugging.

**warning:**
A stack stuck in `ROLLBACK_COMPLETE` after a failed create can never be updated, only deleted. Delete it, fix the template, and create again. This surprises everyone exactly once.

## Common Errors

> **Docs:** [Troubleshooting CloudFormation, CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/troubleshooting.html)

| Error | Cause | Fix |
|---|---|---|
| `No updates are to be performed` | Template unchanged | Use `deploy` with `--no-fail-on-empty-changeset` |
| `Requires capabilities: [CAPABILITY_IAM]` | Template creates IAM resources | Add `--capabilities CAPABILITY_IAM` (or `CAPABILITY_NAMED_IAM` for named ones) |
| `Export ... cannot be deleted as it is in use` | Another stack imports the value | Update/delete importing stacks first |
| `Resource ... already exists` | Name collision with unmanaged resource | Remove hardcoded name or import the resource (`import` change set type) |
| `Template format error: Unresolved resource dependencies` | `!Ref`/`!GetAtt` to a typo'd logical ID | Fix the logical ID; `cfn-lint` catches this |
| Stuck in `UPDATE_ROLLBACK_FAILED` | Rollback blocked (e.g. deleted resource) | `continue-update-rollback --resources-to-skip` |
| `Rate exceeded` | API throttling on big stacks | Retry with backoff; split mega-stacks into nested stacks |
| `Circular dependency between resources` | Mutual `!Ref`s (common with SGs) | Split with `AWS::EC2::SecurityGroupIngress` standalone resources |

**Question:** Does your deploy pipeline create a change set and show it before applying, or does it run `deploy` straight at production and find out afterwards?

## Related Cheatsheets

- [Ansible](https://karandeepsingh.ca/cheatsheets/devops/ansible/): configuration management for the instances these stacks provision, with modules, handlers, roles, and Vault
- [Python Boto3](https://karandeepsingh.ca/cheatsheets/python/python-boto3/): the imperative side, with clients, paginators, waiters, and ClientError handling for anything a template can't express
- [Kubernetes](https://karandeepsingh.ca/cheatsheets/containers/kubernetes/): manifests, rollouts, and kubectl triage for the workloads that land on the clusters these stacks create

The rest of the [DevOps Tooling group](https://karandeepsingh.ca/cheatsheets/devops/) is one click away, or start from the top at the [cheatsheet library](https://karandeepsingh.ca/cheatsheets/).
