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.