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
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.