Skip to content

CloudFormation templates, stacks and change sets

CloudFormation provisions AWS resources from a declarative template in JSON or YAML. It reconciles the described state against what exists, creates and deletes in dependency order, and rolls the whole change back if any part of it fails. It is what makes an environment reproducible: the same template applied to a different account or Region produces the same infrastructure, which is the precondition for immutable architecture.

A template is a text file describing the resources that make up an environment. Only one section is required.

AWSTemplateFormatVersion: '2010-09-09'
Resources:
# At least one resource is required.

The optional sections carry everything that makes a template reusable:

Description: 'What this stack provides'
Parameters:
EnvironmentType:
Type: String
AllowedValues: [dev, prod]
Mappings:
EnvironmentConfig:
dev:
InstanceType: t3.micro
prod:
InstanceType: t3.large
Conditions:
IsProduction: !Equals [!Ref EnvironmentType, prod]
Outputs:
VpcId:
Description: 'VPC ID'
Value: !Ref MyVPC
Transform:
- AWS::Serverless-2016-10-31

Transform is how SAM works: the SAM transform expands AWS::Serverless::* resources into ordinary CloudFormation resources before the stack is created.

A stack is the environment a template describes, managed as a single unit — created, updated and deleted together, with a single consistent state. Deleting the stack deletes what it created, which is the property that makes ephemeral environments practical.

A StackSet extends one template across many accounts and Regions from an administrator account, with drift detection to report where a target has diverged. See cross-account infrastructure deployment.

Nested stacks (AWS::CloudFormation::Stack) let a parent template compose child templates, which is how a large environment is kept modular and how a common component — a VPC, a standard security group set — is reused rather than copied.

A change set is a preview of what an update would do before it does it: which resources would be added, modified or removed, and critically which modifications would replace a resource rather than update it in place. A replacement means the resource is destroyed and recreated with a new physical ID, which for a database or an EBS volume is a very different event from an update.

Generating a change set and reading it is the cheapest safety measure available in CloudFormation, and it should be a step in any pipeline that updates a stack holding stateful resources.

A stack policy is a JSON document that protects resources in a stack from being unintentionally updated or deleted by a stack update.

{
"Statement": [
{ "Effect": "Allow", "Action": "Update:*", "Principal": "*", "Resource": "*" },
{
"Effect": "Deny",
"Action": "Update:*",
"Principal": "*",
"Resource": "LogicalResourceId/ProductionDatabase"
}
]
}

Points that matter in practice:

  • A policy can be attached during stack creation through the console or the CLI; attaching one to an existing stack is a CLI or API operation only.
  • Once a stack has a policy it cannot be removed, though it can be replaced with a different one.
  • When a policy is present, protection is the default: everything is protected unless an explicit Allow covers it.
  • Deny overrides Allow.
  • A stack policy is not an IAM policy. It restricts what a stack update may do; it says nothing about who may call CloudFormation. Both are needed.

Hardcoded AMI IDs, snapshot IDs and account IDs are the single most common reason a template that works in one Region fails in another. Resolve them instead:

  • SSM public parameters for AMIs: ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
  • SSM Parameter Store for your own configuration: '{{resolve:ssm:/example-service/prod/log-level}}'
  • Secrets Manager for credentials: '{{resolve:secretsmanager:prod/db;SecretString:password}}', so the secret never appears in the template, in the change set or in the console.
  • Pseudo parametersAWS::Region, AWS::AccountId, AWS::StackName, AWS::Partition — for anything derived from where the stack is being deployed.
  • !Ref and !GetAtt for values produced by other resources in the same template; see !Ref versus !GetAtt.

A Mappings section is still appropriate for values that genuinely vary by Region or environment and have no authoritative source to resolve from.

Templates can be written by hand, generated by the AWS CDK, or composed visually in AWS Infrastructure Composer, which AWS offers in CloudFormation console mode as the successor to the older CloudFormation Designer.

Validation, cheapest first:

Terminal window
# Syntax and structure, server-side
aws cloudformation validate-template --template-body file://template.yaml
# Linting: unknown properties, bad references, Region-specific problems
cfn-lint template.yaml
# Policy checks: does this template comply with our rules?
cfn-guard validate --rules rules.guard --data template.yaml

validate-template only checks that the document is well formed. cfn-lint catches undefined logical IDs, misspelled properties and resources unavailable in the target Region — the class of error that otherwise surfaces halfway through a deployment. cfn-guard expresses organisational rules (“every S3 bucket must have encryption enabled”) as code that runs in the pipeline.

Where CloudFormation has no resource type for something, a custom resource backed by a Lambda function or an SNS topic extends it: CloudFormation calls the backing function on create, update and delete, and waits for a signal. This is how a template can register a DNS record with a third-party provider, seed a database, or call an API CloudFormation does not model. The function must respond on every path, including failure, or the stack hangs until it times out.

A resource with a block device mapping:

Resources:
EC2Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
InstanceType: t3.micro
IamInstanceProfile: !Ref InstanceProfile
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeType: gp3
VolumeSize: 50
Encrypted: true

Note what is absent: no KeyName. Administrative access should go through Systems Manager Session Manager rather than an SSH key and an open port.

A scaling policy driven by a CloudWatch alarm:

Resources:
ScaleUpPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AdjustmentType: ChangeInCapacity
AutoScalingGroupName: !Ref AutoScalingGroup
Cooldown: '300'
ScalingAdjustment: 1
CpuHighAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmDescription: Scale up if average CPU exceeds 70% for 10 minutes
MetricName: CPUUtilization
Namespace: AWS/EC2
Statistic: Average
Period: 300
EvaluationPeriods: 2
Threshold: 70
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: AutoScalingGroupName
Value: !Ref AutoScalingGroup
AlarmActions:
- !Ref ScaleUpPolicy

For most workloads a target-tracking policy is simpler and better behaved than a step or simple policy driven by an alarm; see Auto Scaling groups.

Change the template, not the resource. A resource modified in the console is drift: the next stack update may revert it or fail on it. Where drift is unavoidable, detect it deliberately with drift detection rather than discovering it during an incident.

Keep templates modular. One template per bounded concern, composed with nested stacks or cross-stack references, rather than one template that describes an entire estate.

Version-control templates like application code. Reviewed pull requests, a changelog, and the pipeline as the only route to production.

Protect stateful resources. DeletionPolicy: Retain or Snapshot on databases and buckets, plus a stack policy denying updates to them, so a template mistake cannot destroy data.

Use helper scripts sparingly. cfn-init, cfn-signal and cfn-hup configure software on an EC2 instance after it launches and signal readiness back to CloudFormation. They still work, but a pre-baked image or a container is usually a better answer than configuring a machine at boot.

Tag from the template. Cost allocation and ownership tags applied by CloudFormation are the only ones that will be applied consistently, and they make stack-level cost tracking possible.