Skip to main content

CloudFormation — Write Infrastructure as YAML, Deploy in Minutes

· 7 min read
Goel Academy
DevOps & Cloud Learning Hub

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:

SectionRequiredPurpose
AWSTemplateFormatVersionNoTemplate format (always 2010-09-09)
DescriptionNoHuman-readable description of the stack
ParametersNoInput values provided at deploy time
MappingsNoStatic lookup tables (region-to-AMI, etc.)
ConditionsNoConditional resource creation
ResourcesYesThe actual AWS resources to create
OutputsNoValues 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

FeatureCloudFormationTerraform
ProviderAWS onlyMulti-cloud (AWS, Azure, GCP, 1000+)
LanguageYAML/JSONHCL (HashiCorp Configuration Language)
StateManaged by AWS (free)Self-managed (S3 + DynamoDB)
Drift detectionBuilt-interraform plan shows drift
RollbackAutomatic on failureManual (no auto-rollback)
ModulesNested stacks (S3-hosted)Registry modules (versioned)
Preview changesChange setsterraform plan
Learning curveModerateModerate
Community modulesLimitedMassive ecosystem
CostFreeFree (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.