CloudFormation Mappings let you define region-specific or environment-specific configurations that your templates can use, abstracting away the need to hardcode values.

Let’s say you’re deploying an EC2 instance and need to use a specific AMI ID. These IDs are different for each AWS region. Instead of maintaining a massive if/else structure or separate templates for each region, you can use a mapping.

AWSTemplateFormatVersion: '2010-09-09'
Description: Example of using CloudFormation Mappings

Mappings:
  # This mapping holds AMI IDs for Amazon Linux 2, varying by region.
  AMIRegionMap:
    us-east-1:
      AMI: ami-0fc5d77e42a21170c # Amazon Linux 2 AMI in us-east-1
    us-west-2:
      AMI: ami-08b4173575791903b # Amazon Linux 2 AMI in us-west-2
    eu-west-1:
      AMI: ami-0e83a2282641974f7 # Amazon Linux 2 AMI in eu-west-1

Resources:
  MyEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !FindInMap [AMIRegionMap, !Ref "AWS::Region", AMI] # Dynamically find the AMI ID
      InstanceType: t2.micro
      Tags:
        - Key: Name
          Value: MyMappedInstance

Outputs:
  InstanceId:
    Description: The Instance ID of the created EC2 instance
    Value: !Ref MyEC2Instance
  AMISold:
    Description: The AMI ID used for this instance
    Value: !FindInMap [AMIRegionMap, !Ref "AWS::Region", AMI]

When you deploy this template, CloudFormation automatically determines which region you’re deploying into using the AWS::Region pseudo-parameter. It then looks up the correct AMI ID from the AMIRegionMap based on that region and the key AMI.

This same pattern applies to environment-specific configurations. Imagine you have different database instance sizes or security group names for your development, staging, and production environments.

AWSTemplateFormatVersion: '2010-09-09'
Description: Environment-specific configurations with Mappings

Mappings:
  # This mapping holds database instance sizes for different environments.
  EnvironmentDBConfig:
    dev:
      DBInstanceType: db.t3.small
      DBAllocatedStorage: 20
    staging:
      DBInstanceType: db.t3.medium
      DBAllocatedStorage: 50
    prod:
      DBInstanceType: db.r5.large
      DBAllocatedStorage: 100

Parameters:
  Environment:
    Type: String
    AllowedValues:
      - dev
      - staging
      - prod
    Description: The deployment environment (dev, staging, or prod).

Resources:
  MyRDSInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceClass: !FindInMap [EnvironmentDBConfig, !Ref Environment, DBInstanceType]
      AllocatedStorage: !FindInMap [EnvironmentDBConfig, !Ref Environment, DBAllocatedStorage]
      Engine: "mysql"
      MasterUsername: "admin"
      MasterUserPassword: "password123" # In production, use Secrets Manager!
      DBName: "mydatabase"

Outputs:
  DBInstanceEndpoint:
    Description: The endpoint of the RDS instance
    Value: !GetAtt MyRDSInstance.Endpoint.Address

Here, the Environment parameter allows the user to select dev, staging, or prod. The !FindInMap function then uses this selected environment to pull the correct DBInstanceType and DBAllocatedStorage from the EnvironmentDBConfig mapping.

The primary benefit is maintainability. If AWS introduces a new AMI in a region, you only update the mapping in one place, not across multiple templates or within complex conditional logic. Similarly, if your environment configurations change, you update the mapping. This significantly reduces the chances of errors and makes your templates more readable and reusable.

You can also nest mappings. For example, you might have a top-level mapping for regions, and within each region, a nested mapping for environments. This allows for even more granular control.

Mappings:
  RegionEnvConfig:
    us-east-1:
      dev:
        InstanceType: t3.micro
        AMI: ami-0fc5d77e42a21170c
      prod:
        InstanceType: m5.large
        AMI: ami-0fc5d77e42a21170c # Same AMI, but could be different
    us-west-2:
      dev:
        InstanceType: t3.small
        AMI: ami-08b4173575791903b
      prod:
        InstanceType: m5.xlarge
        AMI: ami-08b4173575791903b

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, prod]
    Default: dev

Resources:
  MyNestedMappedInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !FindInMap [RegionEnvConfig, !Ref "AWS::Region", !Ref Environment, AMI]
      InstanceType: !FindInMap [RegionEnvConfig, !Ref "AWS::Region", !Ref Environment, InstanceType]
      # ... other properties

In this nested example, !FindInMap [RegionEnvConfig, !Ref "AWS::Region", !Ref Environment, AMI] first looks up the region, then the environment within that region, and finally the AMI key. This provides a powerful way to organize and access complex, multi-dimensional configuration data directly within your CloudFormation templates.

The !Ref "AWS::Region" pseudo-parameter is a key enabler here; without it, you’d have to pass the region as a parameter, which is less automatic and more prone to user error. The !FindInMap intrinsic function is what actually performs the lookup, taking the mapping name, the first-level key (like the region), and the second-level key (like the environment or AMI) as arguments.

What trips people up is thinking of mappings as just for region-specific data. They are equally, if not more, powerful for environment-specific configurations, allowing you to deploy identical infrastructure code across different stages of your lifecycle without code duplication.

The next logical step after mastering mappings for static lookups is to integrate them with dynamic data sources or to manage more complex configurations using AWS Systems Manager Parameter Store, which can be referenced within CloudFormation.

Want structured learning?

Take the full Cloudformation course →