AWS CloudFormation: Infrastructure as Code Guide
Learn AWS CloudFormation from scratch. Build, update, and manage AWS infrastructure with YAML templates, parameters, outputs, and nested stacks.
What you'll learn
- ✓Write CloudFormation templates with parameters, mappings, and outputs
- ✓Deploy and update stacks safely with change sets
- ✓Organize large infrastructures with nested stacks and cross-stack references
- ✓Handle errors with rollback triggers and drift detection
Prerequisites
- •AWS account with admin access
- •Basic understanding of at least one AWS service like EC2 or S3
AWS CloudFormation lets you define your entire AWS infrastructure in text files called templates. Instead of clicking through the console or running CLI commands, you declare what resources you want, and CloudFormation creates, configures, and connects them for you. When you need to make changes, you update the template, and CloudFormation figures out what to modify, replace, or delete.
This approach, called Infrastructure as Code (IaC), gives you version control, repeatability, and consistency across environments. This guide covers everything you need to go from your first template to production-ready stacks.
Template Anatomy
A CloudFormation template is a YAML or JSON file with several sections. Here is the structure with the most important sections:
AWSTemplateFormatVersion: "2010-09-09"
Description: "A description of what this stack creates"
Parameters:
# Input values provided at stack creation
Mappings:
# Static lookup tables
Conditions:
# Conditional logic for resource creation
Resources:
# The AWS resources to create (required)
Outputs:
# Values to export or display after creation
Only Resources is required. Everything else is optional but becomes essential as templates grow.
Your First Template: S3 Bucket with Versioning
Start with something simple to understand the workflow:
AWSTemplateFormatVersion: "2010-09-09"
Description: "S3 bucket with versioning and encryption"
Parameters:
Environment:
Type: String
AllowedValues:
- dev
- staging
- prod
Default: dev
Description: "Deployment environment"
BucketPrefix:
Type: String
Default: myapp
Description: "Prefix for the bucket name"
Resources:
AppBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${BucketPrefix}-${Environment}-${AWS::AccountId}"
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Tags:
- Key: Environment
Value: !Ref Environment
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref AppBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: DenyUnencryptedUploads
Effect: Deny
Principal: "*"
Action: s3:PutObject
Resource: !Sub "${AppBucket.Arn}/*"
Condition:
StringNotEquals:
s3:x-amz-server-side-encryption: AES256
Outputs:
BucketName:
Description: "Name of the created bucket"
Value: !Ref AppBucket
BucketArn:
Description: "ARN of the created bucket"
Value: !GetAtt AppBucket.Arn
Export:
Name: !Sub "${AWS::StackName}-BucketArn"
Deploy this template with the CLI:
# Create the stack
aws cloudformation create-stack \
--stack-name my-s3-stack \
--template-body file://s3-bucket.yaml \
--parameters ParameterKey=Environment,ParameterValue=dev \
ParameterKey=BucketPrefix,ParameterValue=myapp
# Wait for completion
aws cloudformation wait stack-create-complete --stack-name my-s3-stack
# Check outputs
aws cloudformation describe-stacks \
--stack-name my-s3-stack \
--query 'Stacks[0].Outputs'
Intrinsic Functions You Will Use Constantly
CloudFormation provides built-in functions for dynamic values:
Resources:
Example:
Type: AWS::EC2::Instance
Properties:
# !Ref returns the resource ID or parameter value
SubnetId: !Ref PrivateSubnet
# !Sub performs string substitution
UserData:
Fn::Base64: !Sub |
#!/bin/bash
echo "Deploying to ${Environment}"
aws s3 cp s3://${AppBucket}/config.yml /etc/app/
# !GetAtt gets a resource attribute
IamInstanceProfile: !GetAtt InstanceProfile.Arn
# !Select picks an item from a list
AvailabilityZone: !Select [0, !GetAZs '']
# !If for conditional values
InstanceType: !If [IsProduction, m5.large, t3.micro]
# !Join concatenates strings
Tags:
- Key: Name
Value: !Join ['-', [!Ref Environment, 'web', 'server']]
The !Sub function is the most versatile. It handles variable substitution inline and supports both parameter references and resource attributes.
Parameters and Conditions for Multi-Environment Templates
Instead of maintaining separate templates for dev and prod, use parameters and conditions:
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
InstanceType:
Type: String
Default: t3.micro
AllowedValues: [t3.micro, t3.small, m5.large, m5.xlarge]
Conditions:
IsProduction: !Equals [!Ref Environment, prod]
CreateReadReplica: !Equals [!Ref Environment, prod]
Resources:
WebServer:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref InstanceType
Monitoring: !If [IsProduction, true, false]
DatabasePrimary:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceClass: !If [IsProduction, db.r5.large, db.t3.medium]
MultiAZ: !If [IsProduction, true, false]
BackupRetentionPeriod: !If [IsProduction, 30, 7]
DeletionProtection: !If [IsProduction, true, false]
DatabaseReplica:
Type: AWS::RDS::DBInstance
Condition: CreateReadReplica
Properties:
SourceDBInstanceIdentifier: !Ref DatabasePrimary
DBInstanceClass: db.r5.large
The Condition key on DatabaseReplica means the read replica is only created in production. This single template serves all environments while keeping costs low in dev.
Mappings for Static Configuration
Mappings provide lookup tables that avoid long lists of conditions:
Mappings:
RegionConfig:
us-east-1:
AMI: ami-0abcdef1234567890
AZCount: "3"
us-west-2:
AMI: ami-0fedcba9876543210
AZCount: "3"
eu-west-1:
AMI: ami-0123456789abcdef0
AZCount: "3"
EnvironmentConfig:
dev:
InstanceType: t3.micro
MinCapacity: "1"
MaxCapacity: "2"
prod:
InstanceType: m5.large
MinCapacity: "2"
MaxCapacity: "10"
Resources:
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
ImageId: !FindInMap [RegionConfig, !Ref "AWS::Region", AMI]
InstanceType: !FindInMap [EnvironmentConfig, !Ref Environment, InstanceType]
Change Sets: Safe Updates
Never update production stacks directly. Use change sets to preview what CloudFormation will do before it does it:
# Create a change set
aws cloudformation create-change-set \
--stack-name my-stack \
--template-body file://updated-template.yaml \
--change-set-name update-instance-type \
--parameters ParameterKey=InstanceType,ParameterValue=m5.xlarge
# Review the changes
aws cloudformation describe-change-set \
--stack-name my-stack \
--change-set-name update-instance-type \
--query 'Changes[].ResourceChange.{Action:Action,Resource:LogicalResourceId,Replacement:Replacement}'
# Execute if changes look correct
aws cloudformation execute-change-set \
--stack-name my-stack \
--change-set-name update-instance-type
The output shows whether each resource will be Added, Modified, or Removed, and critically, whether a modification requires Replacement. A replacement means the resource is destroyed and recreated, which can cause downtime or data loss.
Nested Stacks for Large Infrastructures
When templates exceed 200-300 lines, split them into nested stacks. Each nested stack manages one concern:
# root-stack.yaml
Resources:
NetworkStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/my-templates/network.yaml
Parameters:
Environment: !Ref Environment
VpcCidr: "10.0.0.0/16"
DatabaseStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/my-templates/database.yaml
Parameters:
Environment: !Ref Environment
VpcId: !GetAtt NetworkStack.Outputs.VpcId
PrivateSubnetIds: !GetAtt NetworkStack.Outputs.PrivateSubnetIds
ApplicationStack:
Type: AWS::CloudFormation::Stack
DependsOn: DatabaseStack
Properties:
TemplateURL: https://s3.amazonaws.com/my-templates/application.yaml
Parameters:
Environment: !Ref Environment
VpcId: !GetAtt NetworkStack.Outputs.VpcId
DatabaseEndpoint: !GetAtt DatabaseStack.Outputs.DatabaseEndpoint
Upload child templates to S3 before deploying:
# Upload all templates
aws s3 sync ./templates/ s3://my-templates/ --delete
# Deploy the root stack
aws cloudformation create-stack \
--stack-name production \
--template-body file://root-stack.yaml \
--parameters ParameterKey=Environment,ParameterValue=prod \
--capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND
Cross-Stack References with Exports
An alternative to nested stacks is cross-stack references. One stack exports values, and another stack imports them:
# network-stack.yaml
Outputs:
VpcId:
Value: !Ref VPC
Export:
Name: !Sub "${AWS::StackName}-VpcId"
PrivateSubnetA:
Value: !Ref PrivateSubnetA
Export:
Name: !Sub "${AWS::StackName}-PrivateSubnetA"
# app-stack.yaml
Resources:
AppServer:
Type: AWS::EC2::Instance
Properties:
SubnetId: !ImportValue network-stack-PrivateSubnetA
VpcId: !ImportValue network-stack-VpcId
Cross-stack references create a dependency. You cannot delete the exporting stack while another stack imports its values.
Handling Failures and Rollbacks
CloudFormation rolls back automatically when resource creation fails, but you can add more control:
Resources:
Database:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Properties:
# ...
S3Bucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
# ...
- DeletionPolicy: Snapshot creates a final snapshot before deleting the resource
- DeletionPolicy: Retain keeps the resource even when the stack is deleted
- UpdateReplacePolicy: Snapshot creates a snapshot when a resource must be replaced during an update
For stack-level protection:
# Enable termination protection
aws cloudformation update-termination-protection \
--enable-termination-protection \
--stack-name production
# Set a stack policy to prevent updates to critical resources
aws cloudformation set-stack-policy \
--stack-name production \
--stack-policy-body '{
"Statement": [
{
"Effect": "Deny",
"Action": "Update:Replace",
"Principal": "*",
"Resource": "LogicalResourceId/Database"
},
{
"Effect": "Allow",
"Action": "Update:*",
"Principal": "*",
"Resource": "*"
}
]
}'
Drift Detection
Over time, manual changes can cause resources to drift from their template-defined configuration. CloudFormation can detect this:
# Start drift detection
aws cloudformation detect-stack-drift --stack-name my-stack
# Check drift status
aws cloudformation describe-stack-drift-detection-status \
--stack-drift-detection-id <detection-id>
# View drifted resources
aws cloudformation describe-stack-resource-drifts \
--stack-name my-stack \
--stack-resource-drift-status-filters MODIFIED DELETED
Run drift detection regularly, especially before stack updates. Updating a drifted stack can produce unexpected results.
Template Validation and Linting
Catch errors before deploying with these tools:
# Built-in validation (checks syntax only)
aws cloudformation validate-template --template-body file://template.yaml
# cfn-lint checks for common mistakes and best practices
pip install cfn-lint
cfn-lint template.yaml
# cfn-nag checks for security issues
gem install cfn-nag
cfn_nag_scan --input-path template.yaml
Add these to your CI/CD pipeline so every template change is validated before it reaches AWS.
A Complete Production Example
Here is a template that creates a VPC, Application Load Balancer, and Auto Scaling Group:
AWSTemplateFormatVersion: "2010-09-09"
Description: "Production web application infrastructure"
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
AMIId:
Type: AWS::EC2::Image::Id
Description: "AMI ID for the web servers"
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Subnets:
- !Ref PublicSubnetA
- !Ref PublicSubnetB
SecurityGroups:
- !Ref ALBSecurityGroup
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Port: 80
Protocol: HTTP
VpcId: !Ref VPC
HealthCheckPath: /health
HealthCheckIntervalSeconds: 30
ASG:
Type: AWS::AutoScaling::AutoScalingGroup
UpdatePolicy:
AutoScalingRollingUpdate:
MinInstancesInService: 1
MaxBatchSize: 1
PauseTime: PT5M
WaitOnResourceSignals: true
Properties:
LaunchTemplate:
LaunchTemplateId: !Ref LaunchTemplate
Version: !GetAtt LaunchTemplate.LatestVersionNumber
MinSize: 2
MaxSize: 6
TargetGroupARNs:
- !Ref TargetGroup
VPCZoneIdentifier:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
Outputs:
LoadBalancerDNS:
Value: !GetAtt ALB.DNSName
Description: "ALB DNS name"
The UpdatePolicy on the ASG ensures rolling deployments update one instance at a time, waiting for health checks to pass before proceeding.
Wrapping Up
CloudFormation turns your AWS infrastructure into versioned, reviewable, repeatable code. Start small with individual resources, add parameters and conditions to support multiple environments, and use nested stacks or cross-stack references as your infrastructure grows. Always use change sets for production updates, enable termination protection on critical stacks, and run drift detection regularly to catch manual changes before they cause problems.
Related articles
- AWS AWS CDK: Infrastructure in TypeScript Getting Started
Get started with AWS CDK using TypeScript. Define cloud infrastructure with constructs, stacks, and deploy real AWS resources with familiar programming patterns.
- DevOps Infrastructure as Code: Terraform Getting Started Guide
Learn Infrastructure as Code with Terraform. This beginner guide covers HCL syntax, providers, state management, modules, and deploying your first cloud resources step by step.
- DevOps Terraform State Management Basics
Why Terraform state exists, how it maps configuration to real infrastructure, and how to set up remote backends, locking, and workspaces without losing your mind.
- DevOps Ansible vs Terraform: Configuration vs Infrastructure
Compare Ansible and Terraform for DevOps automation. Learn when to use each tool, how they differ in approach, and how to combine them effectively.