AWS CloudFormation Best Practices for Infrastructure as Code: Complete 2024 Guide
Master AWS CloudFormation with this comprehensive 3000+ word guide covering template anatomy, intrinsic functions, nested stacks, StackSets for multi-account deployment, drift detection, change sets workflow, cfn-lint validation, security best practices, and CI/CD integration with 6 production-ready code examples.
AWS CloudFormation is the native Infrastructure as Code (IaC) service that enables you to model, provision, and manage AWS resources through declarative templates. As organizations scale their cloud infrastructure, mastering CloudFormation becomes essential for maintaining consistency, security, and operational efficiency across environments. Unlike imperative scripting approaches, CloudFormation's declarative model allows you to define the desired state of your infrastructure, and AWS handles the complex orchestration of resource creation, updates, and deletions.
This comprehensive guide provides actionable best practices, real-world implementation patterns, and production-ready code examples to help you build robust, maintainable, and secure CloudFormation templates. Whether you're deploying a simple application stack or orchestrating complex multi-account environments across multiple AWS regions, these practices will help you leverage CloudFormation to its full potential.
Understanding CloudFormation Template Anatomy and Fundamentals
Before diving into advanced patterns, it's essential to understand the anatomy of a CloudFormation template. Every template consists of several sections, each serving a specific purpose in defining your infrastructure. Understanding these sections is the foundation for writing maintainable, scalable templates.
The Seven Core Template Sections
A well-structured CloudFormation template can include up to seven main sections. While only the Resources section is required, using all sections appropriately creates templates that are easier to understand, maintain, and reuse across different environments and teams.
- AWSTemplateFormatVersion: Identifies the template format version (currently only "2010-09-09")
- Description: A text string describing the template's purpose
- Metadata: Additional information about the template, including Console interface configuration
- Parameters: Values to pass at runtime for template customization
- Mappings: Static key-value pairs for lookup tables (region-specific AMIs, environment configs)
- Conditions: Logical statements controlling resource creation
- Resources: The AWS resources to create (the only required section)
- Outputs: Values returned after stack creation for cross-stack references
Production-Ready Template Structure Example
Here's a comprehensive example demonstrating all major template sections with best practices applied:
AWSTemplateFormatVersion: '2010-09-09'
Description: >
Production-ready CloudFormation template demonstrating
all major template sections and best practices for
deploying a secure, scalable web application infrastructure
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Network Configuration"
Parameters:
- VpcCidr
- PublicSubnetCidrs
- PrivateSubnetCidrs
- Label:
default: "Environment Settings"
Parameters:
- Environment
- ApplicationName
- Label:
default: "Compute Configuration"
Parameters:
- InstanceType
- KeyPairName
ParameterLabels:
VpcCidr:
default: "VPC CIDR Block"
Environment:
default: "Deployment Environment"
Parameters:
Environment:
Type: String
AllowedValues:
- development
- staging
- production
Default: development
Description: Deployment environment for resource tagging and configuration
VpcCidr:
Type: String
Default: 10.0.0.0/16
AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
ConstraintDescription: Must be a valid CIDR block (e.g., 10.0.0.0/16)
Description: CIDR block for the VPC
InstanceType:
Type: String
Default: t3.medium
AllowedValues:
- t3.micro
- t3.small
- t3.medium
- t3.large
- t3.xlarge
Description: EC2 instance type for application servers
ApplicationName:
Type: String
MinLength: 3
MaxLength: 50
AllowedPattern: ^[a-zA-Z][a-zA-Z0-9-]*$
ConstraintDescription: Must begin with a letter and contain only alphanumeric characters and hyphens
Description: Name of the application for resource naming conventions
KeyPairName:
Type: AWS::EC2::KeyPair::KeyName
Description: Name of an existing EC2 KeyPair for SSH access
Mappings:
RegionMap:
us-east-1:
AMI: ami-0c55b159cbfafe1f0
AZ1: us-east-1a
AZ2: us-east-1b
us-west-2:
AMI: ami-0892d3c7ee96c0bf7
AZ1: us-west-2a
AZ2: us-west-2b
eu-west-1:
AMI: ami-0d8e27447ec2c8410
AZ1: eu-west-1a
AZ2: eu-west-1b
EnvironmentConfig:
development:
InstanceCount: 1
MultiAZ: 'false'
BackupRetention: 1
DeletionProtection: 'false'
staging:
InstanceCount: 2
MultiAZ: 'false'
BackupRetention: 7
DeletionProtection: 'false'
production:
InstanceCount: 3
MultiAZ: 'true'
BackupRetention: 35
DeletionProtection: 'true'
Conditions:
IsProduction: !Equals [!Ref Environment, production]
IsNotDevelopment: !Not [!Equals [!Ref Environment, development]]
EnableMultiAZ: !Equals
- !FindInMap [EnvironmentConfig, !Ref Environment, MultiAZ]
- 'true'
EnableDeletionProtection: !Equals
- !FindInMap [EnvironmentConfig, !Ref Environment, DeletionProtection]
- 'true'
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub '${ApplicationName}-${Environment}-vpc'
- Key: Environment
Value: !Ref Environment
- Key: ManagedBy
Value: CloudFormation
Outputs:
VPCId:
Description: VPC Identifier for cross-stack references
Value: !Ref VPC
Export:
Name: !Sub '${AWS::StackName}-VPCId'
VPCCidr:
Description: VPC CIDR Block
Value: !GetAtt VPC.CidrBlock
Export:
Name: !Sub '${AWS::StackName}-VPCCidr'
StackEnvironment:
Description: The deployment environment of this stack
Value: !Ref Environment
This template structure demonstrates several critical best practices:
- Metadata for Console Experience: The
AWS::CloudFormation::Interfacesection groups parameters logically and provides friendly labels, making the template much easier to use through the AWS Console - Comprehensive Parameter Validation: Every parameter includes validation through AllowedValues, AllowedPattern, or AWS-specific types
- Environment-Aware Mappings: Configuration values vary based on environment, enabling the same template to deploy appropriately sized resources for development, staging, and production
- Meaningful Conditions: Conditions control not just resource creation but also property values, allowing dynamic configuration
- Consistent Tagging Strategy: All resources include standardized tags for cost allocation, environment identification, and management tracking
Template Organization and Modularization Strategies
As your infrastructure grows, single monolithic templates become unwieldy and difficult to maintain. Effective template organization separates concerns, promotes reusability, and enables independent team ownership of different infrastructure components.
Layered Architecture Approach
Organize templates into logical layers that reflect your infrastructure dependencies:
Layer 1 - Foundation: VPC, subnets, route tables, NAT gateways, VPN connections Layer 2 - Security: Security groups, NACLs, IAM roles, KMS keys Layer 3 - Shared Services: RDS databases, ElastiCache clusters, S3 buckets Layer 4 - Applications: EC2 instances, ECS services, Lambda functions, API Gateway
This layered approach ensures that foundational resources are deployed first and that application-layer templates can reference shared resources through exports.
File Organization Best Practices
Structure your template repository for clarity and maintainability:
cloudformation/
├── foundations/
│ ├── vpc.yaml
│ ├── vpc-endpoints.yaml
│ └── transit-gateway.yaml
├── security/
│ ├── security-groups.yaml
│ ├── iam-roles.yaml
│ └── kms-keys.yaml
├── databases/
│ ├── rds-postgres.yaml
│ ├── dynamodb-tables.yaml
│ └── elasticache-redis.yaml
├── applications/
│ ├── web-tier.yaml
│ ├── api-tier.yaml
│ └── worker-tier.yaml
├── monitoring/
│ ├── cloudwatch-alarms.yaml
│ └── dashboard.yaml
└── modules/
├── s3-bucket-encrypted.yaml
└── lambda-function.yaml
Nested Stacks and Cross-Stack References
CloudFormation provides two primary mechanisms for template composition: nested stacks and cross-stack references. Understanding when to use each approach is crucial for building maintainable infrastructure.
Nested Stacks for Template Reuse
Nested stacks allow you to create reusable template components that can be embedded within parent stacks. This pattern is ideal for standardized resources that need consistent configuration across multiple deployments.
AWSTemplateFormatVersion: '2010-09-09'
Description: Parent stack demonstrating nested stack pattern
Parameters:
Environment:
Type: String
AllowedValues: [development, staging, production]
Resources:
NetworkStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/my-templates/vpc-template.yaml
Parameters:
Environment: !Ref Environment
VpcCidr: 10.0.0.0/16
Tags:
- Key: StackType
Value: Network
TimeoutInMinutes: 30
SecurityStack:
Type: AWS::CloudFormation::Stack
DependsOn: NetworkStack
Properties:
TemplateURL: https://s3.amazonaws.com/my-templates/security-groups.yaml
Parameters:
Environment: !Ref Environment
VpcId: !GetAtt NetworkStack.Outputs.VPCId
Tags:
- Key: StackType
Value: Security
DatabaseStack:
Type: AWS::CloudFormation::Stack
DependsOn: SecurityStack
Properties:
TemplateURL: https://s3.amazonaws.com/my-templates/rds-postgres.yaml
Parameters:
Environment: !Ref Environment
VpcId: !GetAtt NetworkStack.Outputs.VPCId
SubnetIds: !GetAtt NetworkStack.Outputs.PrivateSubnetIds
SecurityGroupId: !GetAtt SecurityStack.Outputs.DatabaseSecurityGroupId
Tags:
- Key: StackType
Value: Database
Outputs:
DatabaseEndpoint:
Description: RDS database endpoint
Value: !GetAtt DatabaseStack.Outputs.Endpoint
Cross-Stack References with Exports and Imports
For resources that need to be shared across independently managed stacks, use exports and the !ImportValue function:
# In the network stack (vpc-stack.yaml)
Outputs:
VPCId:
Description: VPC ID for reference by other stacks
Value: !Ref VPC
Export:
Name: !Sub '${AWS::StackName}-VPCId'
PrivateSubnetIds:
Description: Comma-separated list of private subnet IDs
Value: !Join
- ','
- - !Ref PrivateSubnet1
- !Ref PrivateSubnet2
- !Ref PrivateSubnet3
Export:
Name: !Sub '${AWS::StackName}-PrivateSubnetIds'
# In an application stack (app-stack.yaml)
Parameters:
NetworkStackName:
Type: String
Default: production-network
Description: Name of the network stack to import values from
Resources:
ApplicationSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for application instances
VpcId: !ImportValue
Fn::Sub: '${NetworkStackName}-VPCId'
ApplicationInstance:
Type: AWS::EC2::Instance
Properties:
InstanceType: t3.medium
SubnetId: !Select
- 0
- !Split
- ','
- !ImportValue
Fn::Sub: '${NetworkStackName}-PrivateSubnetIds'
SecurityGroupIds:
- !Ref ApplicationSecurityGroup
When to Use Each Approach:
| Use Case | Nested Stacks | Cross-Stack References |
|---|---|---|
| Reusable components | Preferred | Limited |
| Independent lifecycle | Not suitable | Preferred |
| Different teams | Challenging | Preferred |
| Tight coupling | Preferred | Not suitable |
| Template size limits | Helps avoid | No impact |
Intrinsic Functions Deep Dive
CloudFormation intrinsic functions are the building blocks for dynamic template logic. Mastering these functions enables you to create flexible, environment-aware templates that adapt to different deployment contexts.
Essential Intrinsic Functions
!Ref - Returns the value of a parameter or the physical ID of a resource:
SecurityGroupId: !Ref ApplicationSecurityGroup
EnvironmentName: !Ref Environment
!Sub - Substitutes variables in strings. This is one of the most powerful functions for constructing dynamic values:
# Simple substitution
BucketName: !Sub '${ApplicationName}-${Environment}-assets'
# With explicit variable mapping
RoleArn: !Sub
- 'arn:aws:iam::${AccountId}:role/${RoleName}'
- AccountId: !Ref AWS::AccountId
RoleName: !Ref ApplicationRole
!GetAtt - Retrieves attribute values from resources:
# Get the ARN of an S3 bucket
BucketArn: !GetAtt AssetsBucket.Arn
# Get the DNS name of an ALB
LoadBalancerDns: !GetAtt ApplicationLoadBalancer.DNSName
# Get the hosted zone ID (useful for Route 53)
HostedZoneId: !GetAtt ApplicationLoadBalancer.CanonicalHostedZoneID
!If - Conditional value selection based on a condition:
Resources:
ApplicationInstance:
Type: AWS::EC2::Instance
Properties:
InstanceType: !If
- IsProduction
- t3.xlarge
- t3.medium
# Use AWS::NoValue to conditionally omit properties
KeyName: !If
- IsProduction
- !Ref AWS::NoValue
- !Ref KeyPairName
!Join - Concatenates values with a delimiter:
SubnetIds: !Join
- ','
- - !Ref PrivateSubnet1
- !Ref PrivateSubnet2
- !Ref PrivateSubnet3
!Select - Selects an item from a list by index:
# Select the first availability zone
AvailabilityZone: !Select
- 0
- !GetAZs ''
# Select from a split string
FirstSubnet: !Select
- 0
- !Split [',', !ImportValue NetworkStack-SubnetIds]
!FindInMap - Retrieves values from the Mappings section:
Mappings:
RegionConfig:
us-east-1:
AMI: ami-12345678
InstanceType: t3.medium
eu-west-1:
AMI: ami-87654321
InstanceType: t3.large
Resources:
Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: !FindInMap
- RegionConfig
- !Ref AWS::Region
- AMI
InstanceType: !FindInMap
- RegionConfig
- !Ref AWS::Region
- InstanceType
Security Best Practices: Protecting Your Infrastructure
Security in CloudFormation extends beyond the resources you create—it includes how you manage secrets, configure IAM policies, and protect against accidental data loss.
Never Hardcode Secrets
One of the most critical CloudFormation security practices is never embedding secrets directly in templates. Instead, use AWS Secrets Manager or Systems Manager Parameter Store:
Parameters:
DatabasePasswordSecretArn:
Type: String
Description: ARN of the Secrets Manager secret containing the database password
AllowedPattern: ^arn:aws:secretsmanager:[a-z0-9-]+:[0-9]+:secret:.+$
Resources:
DatabaseInstance:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: !Sub '${ApplicationName}-${Environment}-db'
Engine: postgres
EngineVersion: '15.4'
DBInstanceClass: db.t3.medium
MasterUsername: '{{resolve:secretsmanager:DatabaseCredentials:SecretString:username}}'
MasterUserPassword: '{{resolve:secretsmanager:DatabaseCredentials:SecretString:password}}'
Using Dynamic References for Secrets
CloudFormation supports dynamic references that resolve values at deployment time:
# Secrets Manager reference
Password: '{{resolve:secretsmanager:my-secret:SecretString:password}}'
# SSM Parameter Store reference (standard parameters)
AMIId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2}}'
# SSM Parameter Store reference (SecureString)
ApiKey: '{{resolve:ssm-secure:/myapp/api-key}}'
IAM Least Privilege in Templates
When creating IAM roles in CloudFormation, always follow the principle of least privilege:
Resources:
ApplicationRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ApplicationName}-${Environment}-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
Condition:
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Policies:
- PolicyName: ApplicationS3Access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: ReadApplicationBucket
Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
- s3:ListBucket
Resource:
- !GetAtt AssetsBucket.Arn
- !Sub '${AssetsBucket.Arn}/*'
- Sid: WriteLogsBucket
Effect: Allow
Action:
- s3:PutObject
Resource:
- !Sub '${LogsBucket.Arn}/application-logs/*'
Tags:
- Key: Environment
Value: !Ref Environment
DeletionPolicy and UpdateReplacePolicy
Protecting critical resources from accidental deletion or replacement is essential for production environments. CloudFormation provides two policies for this purpose.
DeletionPolicy Options
Resources:
ProductionDatabase:
Type: AWS::RDS::DBInstance
DeletionPolicy: Retain
UpdateReplacePolicy: Snapshot
Properties:
DBInstanceIdentifier: !Sub '${ApplicationName}-production-db'
Engine: postgres
DeletionProtection: true
CriticalS3Bucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
BucketName: !Sub '${ApplicationName}-critical-data'
VersioningConfiguration:
Status: Enabled
LogBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Delete
Properties:
BucketName: !Sub '${ApplicationName}-logs'
LifecycleConfiguration:
Rules:
- Id: DeleteOldLogs
Status: Enabled
ExpirationInDays: 90
DeletionPolicy Values:
- Delete (default): Resource is deleted when removed from template or stack is deleted
- Retain: Resource is preserved (you become responsible for managing it)
- Snapshot: Creates a snapshot before deletion (RDS, ElastiCache, Redshift, Neptune, DocumentDB)
UpdateReplacePolicy for Safe Updates
When CloudFormation needs to replace a resource (rather than update in-place), the UpdateReplacePolicy controls what happens to the old resource:
Resources:
Database:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Properties:
DBInstanceIdentifier: !Sub '${ApplicationName}-db'
Change Sets Workflow: Safe Deployments
Change sets are essential for production deployments. They allow you to preview exactly what CloudFormation will modify before executing any changes.
Creating and Reviewing Change Sets
# Create a change set for an existing stack
aws cloudformation create-change-set \
--stack-name production-application \
--change-set-name update-instance-type-20241220 \
--template-body file://template.yaml \
--parameters ParameterKey=InstanceType,ParameterValue=t3.large \
--capabilities CAPABILITY_IAM \
--description "Update instance type from t3.medium to t3.large"
# Wait for change set creation to complete
aws cloudformation wait change-set-create-complete \
--stack-name production-application \
--change-set-name update-instance-type-20241220
# Describe the change set to review changes
aws cloudformation describe-change-set \
--stack-name production-application \
--change-set-name update-instance-type-20241220
# If changes look correct, execute
aws cloudformation execute-change-set \
--stack-name production-application \
--change-set-name update-instance-type-20241220
# If changes are not desired, delete the change set
aws cloudformation delete-change-set \
--stack-name production-application \
--change-set-name update-instance-type-20241220
Understanding Change Set Output
Change sets show three types of changes:
- Add: New resources will be created
- Modify: Existing resources will be updated (in-place or replacement)
- Remove: Resources will be deleted
Pay special attention to the Replacement field in modifications—if true, the resource will be deleted and recreated, potentially causing downtime or data loss.
StackSets for Multi-Account and Multi-Region Deployments
AWS CloudFormation StackSets extend CloudFormation capabilities to deploy stacks across multiple AWS accounts and regions from a single template. This is essential for enterprise organizations managing infrastructure at scale.
Service-Managed StackSets with AWS Organizations
# Create a StackSet with service-managed permissions
aws cloudformation create-stack-set \
--stack-set-name security-baseline \
--template-body file://security-baseline.yaml \
--description "Organization-wide security baseline configuration" \
--permission-model SERVICE_MANAGED \
--auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \
--capabilities CAPABILITY_NAMED_IAM \
--managed-execution Active=true
# Deploy to organizational units
aws cloudformation create-stack-instances \
--stack-set-name security-baseline \
--deployment-targets OrganizationalUnitIds=ou-xxxx-yyyyyyyy \
--regions us-east-1 us-west-2 eu-west-1 \
--operation-preferences \
FailureTolerancePercentage=10,\
MaxConcurrentPercentage=25,\
RegionConcurrencyType=PARALLEL
StackSet Template Example
AWSTemplateFormatVersion: '2010-09-09'
Description: Security baseline deployed via StackSets to all accounts
Resources:
CloudTrailBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'cloudtrail-logs-${AWS::AccountId}-${AWS::Region}'
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled
SecurityNotificationTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: security-notifications
KmsMasterKeyId: alias/aws/sns
GuardDutyDetector:
Type: AWS::GuardDuty::Detector
Properties:
Enable: true
FindingPublishingFrequency: FIFTEEN_MINUTES
DataSources:
S3Logs:
Enable: true
Kubernetes:
AuditLogs:
Enable: true
Outputs:
CloudTrailBucketArn:
Value: !GetAtt CloudTrailBucket.Arn
Description: ARN of the CloudTrail logs bucket
Template Validation with cfn-lint
The cfn-lint tool validates CloudFormation templates against AWS resource specifications and best practices before deployment, catching errors early in the development process.
Installing and Running cfn-lint
# Install cfn-lint
pip install cfn-lint
# Validate a single template
cfn-lint template.yaml
# Validate all templates in a directory
cfn-lint templates/*.yaml
# Use with specific rules
cfn-lint -r W3002 template.yaml
# Ignore specific rules
cfn-lint -i W3002 W2001 template.yaml
# Output in different formats
cfn-lint -f json template.yaml
cfn-lint -f parseable template.yaml
Common cfn-lint Warnings and How to Fix Them
W3002 - Resource names should use !Sub or !Ref:
# Bad
BucketName: my-application-bucket
# Good
BucketName: !Sub '${ApplicationName}-${Environment}-bucket'
E3012 - Property value does not match type:
# Bad (Port expects Integer, not String)
FromPort: "443"
# Good
FromPort: 443
Pre-commit Hook Integration
# .pre-commit-config.yaml
repos:
- repo: https://github.com/aws-cloudformation/cfn-lint
rev: v0.83.0
hooks:
- id: cfn-lint
files: cloudformation/.*\.(yaml|yml|json)$
CI/CD Pipeline Integration
Integrating CloudFormation with CI/CD pipelines ensures consistent, automated deployments with proper validation and approval workflows.
GitHub Actions Workflow Example
name: CloudFormation Deployment
on:
push:
branches: [main]
paths:
- 'cloudformation/**'
pull_request:
branches: [main]
paths:
- 'cloudformation/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install cfn-lint
run: pip install cfn-lint
- name: Lint CloudFormation templates
run: cfn-lint cloudformation/**/*.yaml
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Validate template with AWS
run: |
aws cloudformation validate-template \
--template-body file://cloudformation/main.yaml
deploy-staging:
needs: validate
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to staging
run: |
aws cloudformation deploy \
--stack-name myapp-staging \
--template-file cloudformation/main.yaml \
--parameter-overrides Environment=staging \
--capabilities CAPABILITY_IAM \
--no-fail-on-empty-changeset
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::PRODUCTION_ACCOUNT:role/CloudFormationDeployRole
aws-region: us-east-1
- name: Create change set
run: |
aws cloudformation create-change-set \
--stack-name myapp-production \
--change-set-name deploy-${{ github.sha }} \
--template-body file://cloudformation/main.yaml \
--parameter-overrides Environment=production \
--capabilities CAPABILITY_IAM
- name: Execute change set
run: |
aws cloudformation execute-change-set \
--stack-name myapp-production \
--change-set-name deploy-${{ github.sha }}
Drift Detection and Remediation
Configuration drift occurs when the actual state of resources differs from the expected state defined in your CloudFormation template. Regular drift detection is essential for maintaining infrastructure compliance.
Detecting Drift
# Initiate drift detection for a stack
aws cloudformation detect-stack-drift \
--stack-name production-application
# Check drift detection status
aws cloudformation describe-stack-drift-detection-status \
--stack-drift-detection-id aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
# Get detailed drift results
aws cloudformation describe-stack-resource-drifts \
--stack-name production-application \
--stack-resource-drift-status-filters MODIFIED DELETED
# Detect drift for specific resource
aws cloudformation detect-stack-resource-drift \
--stack-name production-application \
--logical-resource-id WebServerInstance
Automated Drift Detection Schedule
Resources:
DriftDetectionFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: drift-detection-scheduler
Runtime: python3.11
Handler: index.handler
Timeout: 300
Role: !GetAtt DriftDetectionRole.Arn
Code:
ZipFile: |
import boto3
import json
def handler(event, context):
cfn = boto3.client('cloudformation')
sns = boto3.client('sns')
stacks = cfn.list_stacks(
StackStatusFilter=['CREATE_COMPLETE', 'UPDATE_COMPLETE']
)['StackSummaries']
for stack in stacks:
detection_id = cfn.detect_stack_drift(
StackName=stack['StackName']
)['StackDriftDetectionId']
print(f"Started drift detection for {stack['StackName']}: {detection_id}")
return {'statusCode': 200, 'body': json.dumps('Drift detection initiated')}
DriftDetectionSchedule:
Type: AWS::Events::Rule
Properties:
Description: Run drift detection daily
ScheduleExpression: cron(0 6 * * ? *)
State: ENABLED
Targets:
- Id: DriftDetectionTarget
Arn: !GetAtt DriftDetectionFunction.Arn
Remediating Drift
When drift is detected, you have several options:
- Update the template to match the current resource state (if the changes are intentional)
- Update the stack to revert resources to the template-defined state
- Import the drifted resource into a different stack
- Replace the resource by forcing a stack update
Working with Warqline
We are a cloud engineering consultancy and an official AWS and Google Cloud partner. If you are running this in production and want a second pair of eyes, we scope work in a free 45-minute technical call: you describe what you are running and what worries you, and we tell you what we would look at first.
Conclusion: Building Enterprise-Grade Infrastructure with CloudFormation
AWS CloudFormation remains the most powerful native tool for managing AWS infrastructure as code. By following the best practices outlined in this comprehensive guide, you can build infrastructure that is:
Maintainable: Well-organized templates with clear naming conventions, proper documentation, and modular design enable teams to understand and modify infrastructure with confidence.
Secure: Never hardcoding secrets, using dynamic references, implementing least-privilege IAM policies, and protecting critical resources with DeletionPolicy ensures your infrastructure meets enterprise security requirements.
Scalable: Nested stacks, cross-stack references, and StackSets enable you to manage infrastructure across multiple environments, accounts, and regions from centralized, reusable templates.
Reliable: Change sets, drift detection, and CI/CD integration ensure that infrastructure changes are reviewed, tested, and applied consistently across all environments.
Key Takeaways
- Structure templates logically with all seven sections used appropriately
- Use parameters with validation to create flexible, reusable templates
- Master intrinsic functions for dynamic configuration
- Never hardcode secrets - use Secrets Manager or Parameter Store
- Protect critical resources with DeletionPolicy and UpdateReplacePolicy
- Always use change sets for production deployments
- Validate templates with cfn-lint before deployment
- Monitor for drift and remediate promptly
- Integrate with CI/CD for automated, consistent deployments
- Leverage StackSets for multi-account and multi-region management
Combine these CloudFormation best practices with a review by our engineers to maintain continuous visibility into your AWS infrastructure's security and compliance posture.