Organizing Cloudformation Stacks and Templates

Amazon Cloudformation allows you to define all of your AWS resources in code. In an ideal world, you should be able to roll out your application and update it as needed without running ad-hoc commands on the CLI or by fiddling around in the web console. Everything should be laid out in config files (I use YAML here), and stored in version control.

Use multiple templates and cross-stack references

You used to need to have one monolithic template for your resources, which could get very unwieldy. Setting up a VPC with all associated networking resources could easily utilize over 15 different AWS resources (subnets, security groups, internet gateways, etc.). That's before you get to the actual meat of your application, the EC2 instances, S3 buckets, RDS instances, etc.

A more recent feature allows you to define your AWS resources with multiple templates, much like you would separate application business logic into multiple files and classes. Each template can "export" AWS resources, which can in turn be "imported" into other templates.

I like to create a stack for each service of my applications, as follows:

        ./templates
            network.yml
            database.yml
            s3.yml
            web.yml
    

I define all of my baseline network resources in network.yml, which sets up my VPC, subnets, internet gateways, route tables, etc. - all of the stuff I need for a public facing website. My actual EC2 web servers would then be defined in web.yml.

Naturally, the EC2 nodes would need to be in the VPC that I defined. In order to use that same VPC in my web.yml template, I would need to set it as an Output, and give it an Export name:

Resources:
  VPC1:
    Type: AWS::EC2::VPC
    ...
Outputs:
  VPC1:
    Value:
      Ref: VPC1
    Export:
      Name: MyStackName-VPC1
    

Now I can import the VPC name dynamically in other templates.

WebServerSecurityGroup:
  Type: AWS::EC2::SecurityGroup
  Properties:
    VpcId:
      Fn::ImportValue: MyStackName-VPC1
    

Sharing parameters between stacks

I like to define my stack parameters in a separate JSON file and pass those in using the --parameters flag.

aws cloudformation update-stack \
    --stack-name production-application \
    --template-body file://cloudformation/templates/application.yml \
    --parameters file://cloudformation/env-production.json
    

Parameters are defined in JSON using a specific format that the AWS CLI expects:

[
    {
        "ParameterKey": "ApplicationStackName",
        "ParameterValue": "production-application"
    }, {
        "ParameterKey": "AppEnvironment",
        "ParameterValue": "production"
    }, {
        "ParameterKey": "TrustedIpRange",
        "ParameterValue": "1.2.3.4/32"
    }, {
        "ParameterKey": "DBName",
        "ParameterValue": "myDbName"
    }, {
        "ParameterKey": "DBUser",
        "ParameterValue": "myDbUser"
    }, {
        "ParameterKey": "DBPassword",
        "ParameterValue": "passw0rd"
    }, {
        "ParameterKey": "DBAllocatedStorage",
        "ParameterValue": "20"
    }, {
        "ParameterKey": "DBInstanceClass",
        "ParameterValue": "db.m4.large"
    }
]
    

An issues arises when using multiple stacks template files as described above. When using the --parameters flag, each parameter must be used in each template, and there cannot be any extra parameters in the file that are unused. I imagine the solution would normally be to have multiple JSON parameters files, one per stack. That isn't very DRY, and leaves app secrets laying around in multiple files.

Instead, I use the extremely powerful jq library to extract the JSON params I need from my parameters file, and pass that along to each AWS CLI call. The magic is this jq command, which pulls out those parameters in the proper format:

jq -c '[.[] | select(.ParameterKey as $key | ["AppEnvironment", "AppName", "TrustedIpRange"] | index($key))]' cloudformation/env-production.json
    

I can then pass that in with my AWS CLI call:

aws cloudformation update-stack \
    --stack-name production-application \
    --template-body file://cloudformation/templates/application.yml \
    --parameters $(jq -c '[.[] | select(.ParameterKey as $key | ["AppEnvironment", "AppName", "TrustedIpRange"] | index($key))]' cloudformation/env-production.json)
    

In the above, I am selecting the AppEnvironment, AppName, and TrustedIpRange parameters from the JSON file and passing those to the CLI directly. This would be the equivalent of having a dedicated JSON file with only those parameters, but in the DRYest way possible.