CloudFormation — Write Infrastructure as YAML, Deploy in Minutes
You've been clicking through the AWS Console for weeks, creating VPCs, subnets, security groups, and EC2 instances by hand. Then someone says "recreate this in the staging account." Suddenly, your 47-step runbook doesn't feel so clever. CloudFormation fixes this: you describe your infrastructure in a YAML file, and AWS builds it exactly the same way every single time.
What Is CloudFormation?
CloudFormation is AWS's native Infrastructure as Code (IaC) service. You write a template (YAML or JSON) that describes the AWS resources you want, submit it, and CloudFormation creates a stack — a collection of resources managed as a single unit.
The key principle: declarative, not imperative. You say what you want, not how to build it. CloudFormation figures out the dependency order, creates resources in parallel where possible, and rolls back if anything fails.
Template Anatomy
Every CloudFormation template has a defined structure. Here's the full skeleton:
AWSTemplateFormatVersion: '2010-09-09'
Description: >
Production VPC with public and private subnets,
NAT gateway, and bastion host.
Parameters:
EnvironmentName:
Type: String
Default: production
AllowedValues: [development, staging, production]
Description: Environment name for tagging
VpcCidr:
Type: String
Default: 10.0.0.0/16
AllowedPattern: '(\d{1,3}\.){3}\d{1,3}/\d{1,2}'
Description: CIDR block for the VPC
Mappings:
RegionAMI:
us-east-1:
HVM64: ami-0c02fb55956c7d316
us-west-2:
HVM64: ami-0892d3c7ee96c0bf7
Conditions:
IsProduction: !Equals [!Ref EnvironmentName, production]
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-vpc'
Outputs:
VpcId:
Description: VPC ID
Value: !Ref VPC
Export:
Name: !Sub '${EnvironmentName}-VpcId'
The sections break down like this:
| Section | Required | Purpose |
|---|---|---|
| AWSTemplateFormatVersion | No | Template format (always 2010-09-09) |
| Description | No | Human-readable description of the stack |
| Parameters | No | Input values provided at deploy time |
| Mappings | No | Static lookup tables (region-to-AMI, etc.) |
| Conditions | No | Conditional resource creation |
| Resources | Yes | The actual AWS resources to create |
| Outputs | No | Values to export or display after creation |
Creating a Stack with the CLI
Let's deploy a real template that creates a VPC with public and private subnets:
# vpc-stack.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: VPC with public and private subnets
Parameters:
EnvironmentName:
Type: String
Default: dev
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-vpc'
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-public-subnet'
PrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-private-subnet'
InternetGateway:
Type: AWS::EC2::InternetGateway
GatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
Outputs:
VpcId:
Value: !Ref VPC
Export:
Name: !Sub '${EnvironmentName}-VpcId'
PublicSubnetId:
Value: !Ref PublicSubnet
Export:
Name: !Sub '${EnvironmentName}-PublicSubnetId'
# Validate the template before deploying
aws cloudformation validate-template \
--template-body file://vpc-stack.yaml
# Create the stack
aws cloudformation create-stack \
--stack-name dev-vpc \
--template-body file://vpc-stack.yaml \
--parameters ParameterKey=EnvironmentName,ParameterValue=dev
# Watch the creation progress
aws cloudformation wait stack-create-complete --stack-name dev-vpc
# Check the outputs
aws cloudformation describe-stacks \
--stack-name dev-vpc \
--query 'Stacks[0].Outputs' --output table
Stack Updates and Change Sets
Never update a production stack blindly. Change sets let you preview exactly what will happen before you commit:
# Create a change set (preview only, no changes yet)
aws cloudformation create-change-set \
--stack-name dev-vpc \
--change-set-name add-nat-gateway \
--template-body file://vpc-stack-v2.yaml \
--parameters ParameterKey=EnvironmentName,ParameterValue=dev
# Review what will change
aws cloudformation describe-change-set \
--stack-name dev-vpc \
--change-set-name add-nat-gateway \
--query 'Changes[].ResourceChange.{Action:Action,Resource:LogicalResourceId,Type:ResourceType,Replacement:Replacement}' \
--output table
# If it looks good, execute the change set
aws cloudformation execute-change-set \
--stack-name dev-vpc \
--change-set-name add-nat-gateway
Changes come in three flavors: Add (new resource), Modify (update in-place or replace), and Remove. The Replacement field tells you if a resource will be destroyed and recreated — critical for databases and stateful resources.
Intrinsic Functions — The Power Tools
CloudFormation's intrinsic functions let you build dynamic templates:
Resources:
WebServer:
Type: AWS::EC2::Instance
Condition: IsProduction
Properties:
# !Ref returns the resource ID or parameter value
SubnetId: !Ref PublicSubnet
# !Sub substitutes variables into a string
UserData: !Base64
!Sub |
#!/bin/bash
echo "Deploying to ${EnvironmentName}"
echo "Region: ${AWS::Region}"
echo "Stack: ${AWS::StackName}"
# !GetAtt gets an attribute from another resource
SecurityGroupIds:
- !GetAtt WebSecurityGroup.GroupId
# !Select picks an item from a list
AvailabilityZone: !Select [0, !GetAZs '']
# !If chooses based on a condition
InstanceType: !If [IsProduction, t3.large, t3.micro]
# !FindInMap looks up a value in Mappings
ImageId: !FindInMap [RegionAMI, !Ref 'AWS::Region', HVM64]
Tags:
- Key: Name
# !Join concatenates strings
Value: !Join ['-', [!Ref EnvironmentName, web, server]]
Nested Stacks and Cross-Stack References
As your infrastructure grows, a single template becomes unmanageable. Nested stacks let you break templates into reusable modules:
# parent-stack.yaml
Resources:
NetworkStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/my-templates/vpc-stack.yaml
Parameters:
EnvironmentName: !Ref EnvironmentName
AppStack:
Type: AWS::CloudFormation::Stack
DependsOn: NetworkStack
Properties:
TemplateURL: https://s3.amazonaws.com/my-templates/app-stack.yaml
Parameters:
VpcId: !GetAtt NetworkStack.Outputs.VpcId
SubnetId: !GetAtt NetworkStack.Outputs.PublicSubnetId
For independent stacks that need to share values, use cross-stack references via Export and Fn::ImportValue:
# In the consuming stack
Resources:
AppInstance:
Type: AWS::EC2::Instance
Properties:
SubnetId: !ImportValue dev-PublicSubnetId
Rollback Behaviors
CloudFormation's default behavior on failure is to roll back everything. This keeps your stack in a consistent state, but sometimes you need to debug a failed resource.
# Create with rollback disabled (for debugging)
aws cloudformation create-stack \
--stack-name debug-stack \
--template-body file://template.yaml \
--disable-rollback
# Continue a rolled-back update
aws cloudformation continue-update-rollback \
--stack-name broken-stack \
--resources-to-skip FailedResource
Drift Detection — Find Manual Changes
Someone clicked around in the Console and changed a security group rule. Drift detection catches that:
# Start drift detection
aws cloudformation detect-stack-drift --stack-name dev-vpc
# Check drift status
aws cloudformation describe-stack-drift-detection-status \
--stack-drift-detection-id <detection-id>
# See which resources drifted
aws cloudformation describe-stack-resource-drifts \
--stack-name dev-vpc \
--stack-resource-drift-status-filters MODIFIED DELETED \
--query 'StackResourceDrifts[].{Resource:LogicalResourceId,Status:StackResourceDriftStatus}' \
--output table
CloudFormation vs Terraform
| Feature | CloudFormation | Terraform |
|---|---|---|
| Provider | AWS only | Multi-cloud (AWS, Azure, GCP, 1000+) |
| Language | YAML/JSON | HCL (HashiCorp Configuration Language) |
| State | Managed by AWS (free) | Self-managed (S3 + DynamoDB) |
| Drift detection | Built-in | terraform plan shows drift |
| Rollback | Automatic on failure | Manual (no auto-rollback) |
| Modules | Nested stacks (S3-hosted) | Registry modules (versioned) |
| Preview changes | Change sets | terraform plan |
| Learning curve | Moderate | Moderate |
| Community modules | Limited | Massive ecosystem |
| Cost | Free | Free (OSS), paid (Terraform Cloud) |
When to pick CloudFormation: You're all-in on AWS, want zero state management overhead, and value automatic rollbacks. When to pick Terraform: You're multi-cloud, want a richer module ecosystem, or your team already knows HCL.
What's Next?
You can now define entire environments as code and deploy them repeatably. Next up, we'll look at ECS and Fargate — running containers on AWS without managing EC2 instances, and how CloudFormation templates can define your entire container infrastructure.
This is Part 9 of our AWS series. Once you start writing infrastructure as code, clicking through the Console starts to feel reckless.
