Summary

CloudFormation is an excellent tool for writing AWS Infrastructure-as-Code. One of the best features is writing once and running the code in multiple accounts and environments. This is useful if you have dev, UAT and production environments that need similar infrastructure, but you may want different instance sizes. Another example is deploying EC2 instances to other regions where instances will require different AMIs. I will look at three options: Parameters, Mappings, & Config files.

Parameters

The most basic option for reusable code is Parameters. These are variables that you insert at the start of your CloudFormation template. As with everything CloudFormation, there is a specific format for describing the options, but the only required ones are ParameterLogicalID and Type. I also highly recommend including Description, but are several other useful options like Default (to set a default value), NoEcho (won’t display the text) and AllowedValues (to give a list of allowed values). 

 

An example of a Parameter entry, taken from the AWS website, is:

Parameters:
  InstanceTypeParameter:
    Type: String
    Default: t2.micro
    AllowedValues:
      - t2.micro
      - m1.small
      - m1.large
    Description: Enter t2.micro, m1.small, or m1.large. Default is t2.micro.

In that example, you see the two required fields, ParameterLogicalID & Type, and the Default, AllowedValues & Description fields.

The Type field tells CloudFormation what sort of data to expect. The most common are String and Number, but it also allows List and CommaDelimitedList, which are precisely as they sound. Along with those are some AWS and SSM specific parameters. That will enable CloudFormation to do things like confirming a parameter conforms to the requirements for an AMI ID, giving a list of local Security Groups to choose from, or ensuring an entry for a Systems Manager parameter key exists.

Using Parameters allows you to create generic CloudFormation templates that rely on your answers for the specifics.

Further documentation on Parameters can be found at: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html.

 

 

Mappings

 

Mappings are a bit more advanced but also more static than parameters. A mapping is an array of key:value pairs made up of the map name, a top value key (unique), one or more secondary value keys (common), and the value. It looks something like this:

Mappings:
  Mapping01:
    Key01:
      Name: Value01
    Key02:
      Name: Value02
    Key03:
      Name: Value03

Mappings can’t be used by themselves though. To use a mapping, the FindInMap function is needed. This function takes the mapping name and keys as inputs and returns the appropriate value. AWS uses an example of finding an AMI for the region the CloudFormation is run from:

AWSTemplateFormatVersion: "2010-09-09"
Mappings:
  RegionMap:
    us-east-1:
      HVM64: ami-0ff8a91507f77f867
      HVMG2: ami-0a584ac55a7631c0c
    us-west-1:
      HVM64: ami-0bdb828fd58c52235
      HVMG2: ami-066ee5fd4a9ef77f1
    eu-west-1:
      HVM64: ami-047bb4163c506cd98
      HVMG2: ami-0a7c483d527806435
    ap-northeast-1:
      HVM64: ami-06cd52961ce9f0d85
      HVMG2: ami-053cdd503598e4a9d
    ap-southeast-1:
      HVM64: ami-08569b978cc4dfa10
      HVMG2: ami-0be9df32ae9f92309
Resources:
  myEC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", HVM64]
      InstanceType: m1.small

The example from AWS uses the AWS::Region pseudo parameter. You could just as easily take this from the Parameter section, where you specify the environment and the mapping specifies an instance size. The above example also highlights the static nature of Mapping. If an AMI is changed in any region, you need to update the CloudFormation template. It is one of those trade-offs that you always make when coding. Just using a Parameter for the AMI ID would mean that updating an AMI is just a case to doing a CloudFormation update, but that also allows people to use any AMI. If you want to restrict the AMI, you need either AllowedValues or a Mapping; both require updating the template.

You can find further details in the AWS documentation: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html.

 

Configuration Files

 

This one is slightly different from the others as it’s not configured in a CloudFormation template but is a way to pass Parameters using AWS CodePipeline. You can use CodePipeline to deploy CloudFormation templates consistently and triggered from updates to the branch. This strategy works well when you have a single repo for your code and dev, UAT & Prod branches. When deploying CloudFormation templates this way, any Parameters either need a Default value or have values passed using the ParameterOverrides item. The ParameterOverrides is a JSON formatted key:pair list, with the Parameter’s name and the value to set (or override). The values can even be taken from Parameters passed in the CloudFormation template that creates the pipeline.

ParameterOverrides: !Sub |
  {
    "Environment" : "${Environment}",
    "AppName" : "${AppName}"
  }

While using ParameterOverrides to set variables in the individual templates is handy when you start having several templates in your pipeline, and those templates have several parameters each, the Parameters section of the CodePipeline template can begin to get quite busy. Fortunately, you can alleviate this with Configuration files. Configuration files are separate JSON formatted files containing parameter information similar to the ParameterOverrides item. This is set with the TemplateConfiguration item and is a path to the config file.

Configuration:
  ActionMode: REPLACE_ON_FAILURE
  Capabilities: CAPABILITY_NAMED_IAM
  StackName: !Sub "${AppName}-${Environment}-ec2"
  RoleArn: !GetAtt CloudFormationRole.Arn
  TemplatePath: MyRepo::baseinfra/vpc.yml
  TemplateConfiguration: !Sub "MyRepo::config-files/ec2-config-${Environment}.json"
  ParameterOverrides: !Sub |
  {
    "Environment" : "${Environment}"
  }

Something like the above allows you to set a parameter that would be common within the pipeline, like Environment, but then have unique parameters, like AMI & VolumeSize, in a configuration file.

The documentation for using TemplateConfiguration files is more in-depth than the others. AWS has a section describing creating a simple CodePipeline to deploy CloudFormation. The details are in there: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline.html.

More specifically, the configuration of the JSON file is in: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-cfn-artifacts.html#w2ab1c21c15c15.

 

Conclusion

While Parameters are reasonably simple, they can be pretty powerful when used with AWS pseudo parameters. Parameters are also the backbone of reusable CloudFormation templates. Even if I’m writing something simple, I use parameters wherever possible. I can then pull the template from my bag of tricks if I ever need to do something similar again. Write once, use many times!

Mappings are a bit more situational, given their static nature, but are an essential tool when dealing with a multi-environment or multi-account deployment. They can also be necessary when dealing with pipelines and config files. Configuration files are an excellent tool to reduce parameter complexity, but having too many configuration files can be just as complex or confusing. This was a situation I found myself in when I started using configuration files. A good strategy in those situations is using a mix of mappings and configuration files.

Hopefully, the above has given you a start to writing your reusable code or shown some new options. Now, get out there and build … but only write once.