AWS CLI First #16: Infrastructure as Code: Deploying Multi-Tier Stacks with CloudFormation & SAM CLI
Transition from imperative scripts to declarative Infrastructure as Code: author production CloudFormation YAML templates, validate change sets, automate rollbacks, manage stack drifts, and build serverless apps with AWS SAM CLI.
AWS with CLI: The Terminal-First Mastery Path
Part of the CLI-first comprehensive learning path from foundational setup to production cloud architectures.
Across the first 15 parts of this series, we created our cloud infrastructure imperatively using individual AWS CLI commands. While the imperative CLI is peerless for fast experimentation, diagnostic troubleshooting, and automation scripting, it introduces substantial risks when managing complex enterprise systems:
- State Tracking Overhead: Bash scripts do not know if an infrastructure update partially failed halfway through. If a command fails at step 7 of 12, your AWS account is left in an inconsistent, fractured state.
- Non-Declarative Mutability: Writing scripts that check whether a resource exists before creating or updating it requires hundreds of lines of defensive bash boilerplate.
- Auditability & Drift: When multiple cloud engineers modify security groups or route tables ad-hoc, there is no single source of truth recording what the intended architecture should be.
AWS CloudFormation and the AWS Serverless Application Model (SAM CLI) solve this through Declarative Infrastructure as Code (IaC):
- You declare the desired end-state of your infrastructure in version-controlled YAML or JSON templates.
- The CloudFormation engine calculates dependency graphs, provisions resources in parallel, executes atomic rollbacks upon failure, and detects configuration drift.
In this guide, we master enterprise Infrastructure as Code directly from the terminal. We will author a production Multi-Tier CloudFormation template, validate syntax, preview mutations using Change Sets, track live deployment events, detect Stack Drift, and test serverless APIs locally with SAM CLI.
๐ฐ Estimated Lab Cost & Resource Architecture
1. Cost Breakdown for this Lab
All resources in this lab are provisioned on-demand in the Asia Pacific (Singapore) ap-southeast-1 region:
| Resource | Quantity | Unit Cost (Hourly) | Estimated Lab Cost (1 Hour) |
|---|---|---|---|
| AWS CloudFormation Management | 1 Stack | Free AWS Service | $0.000 |
| VPC & Subnets (Multi-AZ) | 1 VPC (2 Subnets) | $0.00 / hour | $0.000 |
| Security Groups & Route Tables | 2 SGs + 1 RT | $0.00 / hour | $0.000 |
| AWS SAM CLI Local Emulation | Local Docker Engine | Local Workstation Compute | $0.000 |
| Total Estimated Lab Cost | ~$0.000 USD (100% Free) |
2. Architecture Diagram
+----------------------------------------------------------------+
| Declarative Infrastructure as Code Workflow |
+-------------------------------+--------------------------------+
|
v
+-------------------------------+--------------------------------+
| Declarative YAML Template (vpc-infrastructure.yaml) |
| - Parameters: Environment, VPCCIDR, SubnetCIDRs |
| - Resources: VPC, Multi-AZ Subnets, Route Tables, IGW, SGs |
| - Outputs: VPCId, SubnetIds (Exported for Cross-Stack Sharing)|
+-------------------------------+--------------------------------+
|
v (aws cloudformation create-change-set)
+-------------------------------+--------------------------------+
| CloudFormation Change Set Preview |
| - Dry-run validation of additions, modifications & deletions |
+-------------------------------+--------------------------------+
|
v (aws cloudformation execute-change-set)
+-------------------------------+--------------------------------+
| CloudFormation Deployment Engine |
| - Dependency Graph Resolution & Parallel Resource Provisioning|
| - Automatic Rollback on Failure (Zero Broken States) |
+-------------------------------+--------------------------------+
|
v
+----------------------------------------------------------------+
| Operational Capabilities: |
| - Drift Detection: Detect unauthorized manual out-of-band edits|
| - SAM CLI: Local API emulation & packaging inside Docker |
+----------------------------------------------------------------+
๐ Phase 1: Authoring a Production CloudFormation Template
A production CloudFormation template contains five primary sections:
AWSTemplateFormatVersion: Standard engine specification (2010-09-09).Description: Human-readable summary of the stack.Parameters: Dynamic variables passed at deploy time.Resources: The declarative AWS objects to provision.Outputs: Values exported for use by other stacks.
1. Creating infrastructure-stack.yaml
We author a complete networking and security foundation with built-in parameter validation:
mkdir -p ./cloudformation
cat <<'EOF' > ./cloudformation/infrastructure-stack.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Production Multi-AZ VPC and Security Architecture via AWS CLI"
Parameters:
EnvironmentName:
Type: String
Default: "Production"
AllowedValues: ["Production", "Staging", "Development"]
Description: "Deployment environment target"
VpcCIDR:
Type: String
Default: "10.100.0.0/16"
AllowedPattern: '(^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$)'
Description: "Primary CIDR block for the custom VPC"
PublicSubnetACIDR:
Type: String
Default: "10.100.1.0/24"
Description: "Subnet CIDR for Availability Zone A"
PublicSubnetBCIDR:
Type: String
Default: "10.100.2.0/24"
Description: "Subnet CIDR for Availability Zone B"
Resources:
# 1. Custom VPC
ProductionVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCIDR
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub "${EnvironmentName}-IaC-VPC"
- Key: ManagedBy
Value: "CloudFormation"
# 2. Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub "${EnvironmentName}-IaC-IGW"
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref ProductionVPC
InternetGatewayId: !Ref InternetGateway
# 3. Route Table & Default Route
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref ProductionVPC
Tags:
- Key: Name
Value: !Sub "${EnvironmentName}-IaC-Public-RT"
PublicDefaultRoute:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: "0.0.0.0/0"
GatewayId: !Ref InternetGateway
# 4. Multi-AZ Subnets
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ProductionVPC
CidrBlock: !Ref PublicSubnetACIDR
AvailabilityZone: !Select [0, !GetAZs ""]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub "${EnvironmentName}-IaC-Subnet-A"
SubnetRouteTableAssociationA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicRouteTable
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ProductionVPC
CidrBlock: !Ref PublicSubnetBCIDR
AvailabilityZone: !Select [1, !GetAZs ""]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub "${EnvironmentName}-IaC-Subnet-B"
SubnetRouteTableAssociationB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetB
RouteTableId: !Ref PublicRouteTable
# 5. Security Group
ApplicationSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Inbound HTTP access for web workloads"
VpcId: !Ref ProductionVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: "0.0.0.0/0"
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: "0.0.0.0/0"
Tags:
- Key: Name
Value: !Sub "${EnvironmentName}-IaC-App-SG"
Outputs:
VpcId:
Description: "The ID of the provisioned VPC"
Value: !Ref ProductionVPC
Export:
Name: !Sub "${EnvironmentName}-VpcId"
SubnetAId:
Description: "Subnet ID in Availability Zone A"
Value: !Ref PublicSubnetA
Export:
Name: !Sub "${EnvironmentName}-SubnetA"
SubnetBId:
Description: "Subnet ID in Availability Zone B"
Value: !Ref PublicSubnetB
Export:
Name: !Sub "${EnvironmentName}-SubnetB"
SecurityGroupId:
Description: "Application Security Group ID"
Value: !Ref ApplicationSecurityGroup
Export:
Name: !Sub "${EnvironmentName}-AppSecurityGroup"
EOF
๐ Phase 2: Template Validation via AWS CLI
Before submitting a template to the CloudFormation engine, execute a static lint check:
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
aws cloudformation validate-template \
--template-body file://cloudformation/infrastructure-stack.yaml \
--region "$REGION" \
--profile "$PROFILE"
If valid, CloudFormation prints the recognized parameters and template capabilities. If a YAML syntax error or invalid property name is detected, the CLI returns a descriptive parse error immediately.
๐ Phase 3: CloudFormation Change Sets (Pre-Flight Dry Runs)
In production, never execute blind deployments. A Change Set allows you to inspect precisely what resources CloudFormation will add, modify, or destroy before applying the changes:
1. Creating the Change Set
STACK_NAME="Enterprise-IaC-Core-Stack"
CHANGE_SET_NAME="Initial-VPC-Provisioning"
aws cloudformation create-change-set \
--stack-name "$STACK_NAME" \
--change-set-name "$CHANGE_SET_NAME" \
--template-body file://cloudformation/infrastructure-stack.yaml \
--parameters ParameterKey=EnvironmentName,ParameterValue=Production \
--change-set-type "CREATE" \
--region "$REGION" \
--profile "$PROFILE"
echo "Change Set submitted. Waiting for calculation..."
aws cloudformation wait change-set-create-complete \
--stack-name "$STACK_NAME" \
--change-set-name "$CHANGE_SET_NAME" \
--region "$REGION" \
--profile "$PROFILE"
2. Inspecting Proposed Mutations
Query the planned actions:
aws cloudformation describe-change-set \
--stack-name "$STACK_NAME" \
--change-set-name "$CHANGE_SET_NAME" \
--region "$REGION" \
--profile "$PROFILE" \
--query "Changes[*].ResourceChange.[Action,LogicalResourceId,ResourceType,Replacement]" \
--output table
Output:
---------------------------------------------------------------------------
| DescribeChangeSet |
+--------+---------------------------------+-------------------------+----+
| Add | ApplicationSecurityGroup | AWS::EC2::SecurityGroup| |
| Add | AttachGateway | AWS::EC2::VPCGateway...| |
| Add | InternetGateway | AWS::EC2::InternetGa...| |
| Add | ProductionVPC | AWS::EC2::VPC | |
| Add | PublicDefaultRoute | AWS::EC2::Route | |
| Add | PublicRouteTable | AWS::EC2::RouteTable | |
| Add | PublicSubnetA | AWS::EC2::Subnet | |
| Add | PublicSubnetB | AWS::EC2::Subnet | |
+--------+---------------------------------+-------------------------+----+
Notice Action: Add across all resources. If modifying an existing stack, this command explicitly flags dangerous actions like Replacement: True (which would destroy and recreate a database or storage volume!).
๐ Phase 4: Deploying & Tracking Stack Events
Once verified, execute the Change Set and watch the real-time event stream:
1. Executing the Change Set
aws cloudformation execute-change-set \
--stack-name "$STACK_NAME" \
--change-set-name "$CHANGE_SET_NAME" \
--region "$REGION" \
--profile "$PROFILE"
echo "Deployment initiated. Tracking live events..."
2. Streaming Real-Time Stack Events
Monitor resource provisioning events as they occur:
# Stream the last 10 stack events
aws cloudformation describe-stack-events \
--stack-name "$STACK_NAME" \
--region "$REGION" \
--profile "$PROFILE" \
--query "StackEvents[0:8].[LogicalResourceId,ResourceStatus,ResourceStatusReason]" \
--output table
Wait until the stack reaches CREATE_COMPLETE:
aws cloudformation wait stack-create-complete \
--stack-name "$STACK_NAME" \
--region "$REGION" \
--profile "$PROFILE"
echo "Stack $STACK_NAME successfully created!"
3. Reviewing Stack Outputs & Exported Variables
Query the exported values:
aws cloudformation describe-stacks \
--stack-name "$STACK_NAME" \
--region "$REGION" \
--profile "$PROFILE" \
--query "Stacks[0].Outputs[*].[OutputKey,OutputValue,ExportName]" \
--output table
These exported values can now be referenced by completely separate stacks (e.g. your database stack or compute cluster stack) using the Fn::ImportValue intrinsic function!
๐ Phase 5: Detecting Stack Drift (Catching ClickOps)
One of the most powerful features of CloudFormation is Drift Detection. If a developer manually modifies a security group in the AWS Console or deletes a subnet via CLI outside of CloudFormation, the real-world infrastructure โdriftsโ from the code.
1. Triggering Drift Detection via CLI
DRIFT_ID=$(aws cloudformation detect-stack-drift \
--stack-name "$STACK_NAME" \
--region "$REGION" \
--profile "$PROFILE" \
--query "StackDriftDetectionId" \
--output text)
echo "Drift Detection ID: $DRIFT_ID"
# Wait for drift scan to complete
sleep 10
2. Inspecting Drift Status
aws cloudformation describe-stack-drift-detection-status \
--stack-drift-detection-id "$DRIFT_ID" \
--region "$REGION" \
--profile "$PROFILE" \
--query "[StackDriftStatus,DriftedStackResourceCount]" \
--output table
If someone manually added an unauthorized inbound rule to ApplicationSecurityGroup, CloudFormation marks the stack DRIFTED and displays the exact JSON diff between expected and actual configuration!
โก Phase 6: Serverless Application Model (SAM CLI)
While pure CloudFormation is exceptional for foundational infrastructure, building serverless applications (Lambda, API Gateway, DynamoDB) with pure CloudFormation requires verbose, repetitive YAML.
The AWS Serverless Application Model (SAM) provides an abstraction layer that condenses 100 lines of CloudFormation into 20 lines of SAM syntax:
[ SAM Template (template.yaml) ]
|
v (sam build)
[ Expanded Standard CloudFormation Template ]
|
v (sam deploy)
[ Deployed CloudFormation Stack ]
1. Authoring a SAM Template (template.yaml)
cat <<'EOF' > ./cloudformation/sam-template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: "Serverless Microservice with SAM"
Globals:
Function:
Timeout: 5
MemorySize: 128
Runtime: python3.12
Architectures:
- arm64
Resources:
OrdersApiFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
def handler(event, context):
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": "{\"message\": \"Hello from SAM CLI Serverless!\"}"
}
Handler: index.handler
Events:
GetOrders:
Type: HttpApi
Properties:
Path: /orders
Method: GET
EOF
Notice Transform: AWS::Serverless-2016-10-31. When deployed, CloudFormation automatically expands this short template into:
- The Lambda function
- The IAM execution role
- The HTTP API Gateway v2
- The Lambda Proxy integration
- The route and stage permissions
๐งน Phase 7: 1-Click Hardened Stack Deletion
With Infrastructure as Code, teardown is completely automated. You do not need complex bash scripts to delete subnets, security groups, and route tables in reverse order. CloudFormation reads the dependency graph and purges everything in parallel:
cat <<'EOF' > destroy-iac-stack.sh
#!/usr/bin/env bash
set -euo pipefail
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
STACK_NAME="Enterprise-IaC-Core-Stack"
echo "Deleting CloudFormation Stack: $STACK_NAME..."
aws cloudformation delete-stack \
--stack-name "$STACK_NAME" \
--region "$REGION" \
--profile "$PROFILE"
echo "Waiting for stack deletion to finalize..."
aws cloudformation wait stack-delete-complete \
--stack-name "$STACK_NAME" \
--region "$REGION" \
--profile "$PROFILE"
rm -rf ./cloudformation destroy-iac-stack.sh
echo "=== Part 16 Teardown Complete! Zero Resources Remaining. ==="
EOF
chmod +x destroy-iac-stack.sh
./destroy-iac-stack.sh
๐ Key Takeaways & Enterprise Architecture Patterns
- Declarative vs. Imperative: Shift from imperative bash scripts to declarative CloudFormation templates. Let AWS resolve dependency trees, handle retries, and manage state.
- Pre-Flight Change Sets: Never blindly update production stacks. Always generate and inspect a
ChangeSetto prevent unintended resource replacements. - Cross-Stack Exports: Use the
OutputsandExport: Namepattern to decouple foundational networking stacks from ephemeral application compute stacks. - Drift Detection: Periodically audit stacks for configuration drift to enforce compliance and eliminate unauthorized out-of-band ClickOps mutations.
- SAM for Serverless: Use
AWS::Serverlesstransforms to eliminate CloudFormation boilerplate when orchestrating Lambda functions and API Gateways.
๐ฎ Whatโs Next in Part 17 (Grand Finale)?
How do you deploy these CloudFormation stacks and application updates automatically every time a developer merges code to GitHub?
In Part 17: Automated CI/CD Workflows: GitHub Actions & AWS CLI Integration, we conclude our 17-part series with the pinnacle of modern enterprise engineering: Zero-Secret CI/CD. We will configure OpenID Connect (OIDC) between GitHub Actions and AWS, eliminate static long-lived IAM keys forever, run automated linting pipelines, and execute hands-free production cloud deployments directly on git push!