CloudFormation examples
Worked template fragments. Everything below is written for copying, so the defaults are the safe ones: no open SSH, encryption on by default, AMI IDs resolved rather than pinned, and current instance and storage generations.
Mappings and !FindInMap
Section titled “Mappings and !FindInMap”Mappings declares a lookup table of fixed values keyed by something known at deploy time —
usually Region or environment — and !FindInMap reads from it.
Mappings: EnvironmentConfig: dev: InstanceType: t3.micro DbInstanceClass: db.t4g.micro MultiAZ: false prod: InstanceType: t3.small DbInstanceClass: db.t4g.medium MultiAZ: true
Resources: WebServerLaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateData: InstanceType: !FindInMap [EnvironmentConfig, !Ref EnvironmentName, InstanceType]Use Mappings for values that genuinely vary by Region or environment and have no
authoritative source to look up: instance sizing per environment, CIDR allocations, a
Region-to-partner-account table.
Do not use it for AMI IDs. A RegionMap of hardcoded AMIs was the standard pattern for years
and is now an anti-pattern: the IDs go stale within weeks and the map has to be edited every
time a Region is added. Resolve them from the SSM public parameters instead:
LaunchTemplateData: ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'or, if the ID should be resolved as a template parameter so the change set shows it:
Parameters: LatestAmiId: Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>' Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64A complete web tier
Section titled “A complete web tier”The template below deploys a VPC with public and private subnets across two Availability Zones, an internet-facing Application Load Balancer terminating TLS, an Auto Scaling group of private instances reachable only from the load balancer, and an encrypted Multi-AZ database. It is a single template; it is split here only for readability.
Network
Section titled “Network”AWSTemplateFormatVersion: '2010-09-09'Description: Example web tier - VPC, ALB, Auto Scaling group and database
Parameters: EnvironmentName: Type: String Default: dev AllowedValues: [dev, prod] CertificateArn: Type: String Description: ACM certificate ARN for the load balancer HTTPS listener
Resources: Vpc: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsHostnames: true EnableDnsSupport: true Tags: - Key: Name Value: !Sub '${EnvironmentName}-vpc'
InternetGateway: Type: AWS::EC2::InternetGateway
AttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref Vpc InternetGatewayId: !Ref InternetGateway
PublicSubnetA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref Vpc CidrBlock: 10.0.0.0/24 AvailabilityZone: !Select [0, !GetAZs ''] MapPublicIpOnLaunch: true
PublicSubnetB: Type: AWS::EC2::Subnet Properties: VpcId: !Ref Vpc CidrBlock: 10.0.1.0/24 AvailabilityZone: !Select [1, !GetAZs ''] MapPublicIpOnLaunch: true
PrivateSubnetA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref Vpc CidrBlock: 10.0.10.0/24 AvailabilityZone: !Select [0, !GetAZs '']
PrivateSubnetB: Type: AWS::EC2::Subnet Properties: VpcId: !Ref Vpc CidrBlock: 10.0.11.0/24 AvailabilityZone: !Select [1, !GetAZs '']
PublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc
DefaultPublicRoute: Type: AWS::EC2::Route DependsOn: AttachGateway Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway
PublicSubnetARouteAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnetA
PublicSubnetBRouteAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnetBSecurity groups
Section titled “Security groups”Each tier accepts traffic only from the tier in front of it. There is no inbound SSH rule
anywhere in this template — administrative access goes through
Systems Manager Session Manager,
which needs no inbound port at all. An ingress rule of 0.0.0.0/0 on port 22 is flagged as a
finding by AWS Config, Security Hub and Trusted Advisor, and it is the first thing an automated
scanner looks for.
LoadBalancerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Public HTTPS to the load balancer VpcId: !Ref Vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 Description: HTTPS from the internet - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: 0.0.0.0/0 Description: HTTP, redirected to HTTPS by the listener
WebServerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Application traffic from the load balancer only VpcId: !Ref Vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 8080 ToPort: 8080 SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup Description: From the load balancer
DatabaseSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Database access from the application tier only VpcId: !Ref Vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 3306 ToPort: 3306 SourceSecurityGroupId: !Ref WebServerSecurityGroup Description: MySQL from the application tierInstance role, launch template and Auto Scaling group
Section titled “Instance role, launch template and Auto Scaling group”The instance role carries AmazonSSMManagedInstanceCore, which is what makes Session Manager,
Run Command and Patch Manager work on the instance.
InstanceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: ec2.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Roles: - !Ref InstanceRole
WebServerLaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateData: ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}' InstanceType: !FindInMap [EnvironmentConfig, !Ref EnvironmentName, InstanceType] IamInstanceProfile: Arn: !GetAtt InstanceProfile.Arn SecurityGroupIds: - !Ref WebServerSecurityGroup MetadataOptions: HttpTokens: required BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeType: gp3 VolumeSize: 20 Encrypted: true
WebServerAsg: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: - !Ref PrivateSubnetA - !Ref PrivateSubnetB LaunchTemplate: LaunchTemplateId: !Ref WebServerLaunchTemplate Version: !GetAtt WebServerLaunchTemplate.LatestVersionNumber MinSize: 2 MaxSize: 6 DesiredCapacity: 2 HealthCheckType: ELB HealthCheckGracePeriod: 120 TargetGroupARNs: - !Ref AlbTargetGroupMetadataOptions: HttpTokens: required enforces IMDSv2, which closes the SSRF-to-credentials
path that IMDSv1 leaves open.
Load balancer
Section titled “Load balancer” ApplicationLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internet-facing Subnets: - !Ref PublicSubnetA - !Ref PublicSubnetB SecurityGroups: - !Ref LoadBalancerSecurityGroup
AlbTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: VpcId: !Ref Vpc Port: 8080 Protocol: HTTP TargetType: instance HealthCheckPath: /health HealthCheckIntervalSeconds: 15 HealthyThresholdCount: 2 UnhealthyThresholdCount: 3
HttpsListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: LoadBalancerArn: !Ref ApplicationLoadBalancer Port: 443 Protocol: HTTPS SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06 Certificates: - CertificateArn: !Ref CertificateArn DefaultActions: - Type: forward TargetGroupArn: !Ref AlbTargetGroup
HttpRedirectListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: LoadBalancerArn: !Ref ApplicationLoadBalancer Port: 80 Protocol: HTTP DefaultActions: - Type: redirect RedirectConfig: Protocol: HTTPS Port: '443' StatusCode: HTTP_301Database
Section titled “Database” DbSubnetGroup: Type: AWS::RDS::DBSubnetGroup Properties: DBSubnetGroupDescription: Private subnets for the database SubnetIds: - !Ref PrivateSubnetA - !Ref PrivateSubnetB
DatabaseInstance: Type: AWS::RDS::DBInstance DeletionPolicy: Snapshot UpdateReplacePolicy: Snapshot Properties: Engine: mysql DBInstanceClass: !FindInMap [EnvironmentConfig, !Ref EnvironmentName, DbInstanceClass] MultiAZ: !FindInMap [EnvironmentConfig, !Ref EnvironmentName, MultiAZ] AllocatedStorage: 20 StorageType: gp3 StorageEncrypted: true BackupRetentionPeriod: 7 MasterUsername: dbadmin ManageMasterUserPassword: true DBSubnetGroupName: !Ref DbSubnetGroup VPCSecurityGroups: - !Ref DatabaseSecurityGroup PubliclyAccessible: falseManageMasterUserPassword: true has RDS generate the password and store it in AWS Secrets
Manager, with rotation available. This is preferable to a NoEcho parameter, because a
NoEcho parameter still has to be supplied by whoever runs the deployment and therefore exists
somewhere in plaintext.
DeletionPolicy: Snapshot means deleting the stack takes a final snapshot rather than
destroying the data. On any stateful resource this line is worth more than the rest of the
template.
Storage with a lifecycle policy
Section titled “Storage with a lifecycle policy” DataBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub '${EnvironmentName}-${AWS::AccountId}-data' VersioningConfiguration: Status: Enabled PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: aws:kms LifecycleConfiguration: Rules: - Id: TransitionToInfrequentAccess Status: Enabled Transitions: - TransitionInDays: 90 StorageClass: STANDARD_IAS3 bucket names are globally unique, so naming a bucket after a bare environment string
(dev) collides on the first attempt. Including the account ID makes the name unique without
making it unpredictable.
Outputs
Section titled “Outputs”Outputs: VpcId: Description: VPC ID Value: !Ref Vpc Export: Name: !Sub '${AWS::StackName}-VpcId'
LoadBalancerDns: Description: DNS name of the Application Load Balancer Value: !GetAtt ApplicationLoadBalancer.DNSName
DatabaseEndpoint: Description: Database endpoint address Value: !GetAtt DatabaseInstance.Endpoint.AddressBefore deploying anything from this page
Section titled “Before deploying anything from this page”Run cfn-lint and aws cloudformation validate-template over the assembled template, and
generate a change set rather than updating a stack directly. Undefined logical IDs and
properties that do not exist on a resource type are the two most common defects in a template
copied from documentation, and both are caught in seconds.