AWS Cloud Formation AWS::Stack primitive
I’ve finally been able to dig into the AWS::Stack
primitive! Since discovering it, I’ve been
refactoring both multi-formation setups for both work and my personal projects. It really was a
crucial missing piece.
What it gives you
Orchestrating many small pieces of infrastructure can be a huge pain in the ass. Similarly, managing a bunch of small AWS Cloud Formation stacks can be painful. Deployments take thought (gross) and mistakes are more likely to occur rolling out updates. With so many small pieces of Cloud Formation stuff going on, you’ll find yourself in one or more situations:
- Many small stacks are used to build a larger piece of infrastructure
- Building an overly large and complex single formation file that you don’t want to split up in fear of pieces not getting deployed together
- A deploy process that requires that you deploy services in a specific order
- A loss of transactionality when deploying services; deploy stack a, b, c, problem on d. The only way to roll back a, b, c is to undo the commit and redeploy the formation yourself. In reverse order!
Utilizing an abstraction layer higher than an individual stack addresses these concerns head-on:
- The deployment story becomes deploying a single stack. This will deploy all nested stacks and handle the dependency graph much more gracefully.
- Transactional rollouts over multiple services; all work that was done can be rolled back if something fails later in the stack update. The power to turn this off is now in your hands as well.
- À la carte infrastructure becomes easier; see this post for more info.
What it looks like
AWSTemplateFormatVersion: 2010-09-09
Parameters:
ApiEnabled:
Type: String
Default: "true"
ApiDatabaseName:
Type: String
Default: api
AnalyticsEnabled:
Type: String
Default: "true"
AnalyticsDatabaseName:
Type: String
Default: analytics
FrontendEnabled:
Type: String
Default: "true"
Conditions:
IsApiEnabled: !Equals [ !Ref ApiEnabled, "true" ]
IsAnalyticsEnabled: !Equals [ !Ref AnalyticsEnabled, "true" ]
IsFrontendEnabled: !Equals [ !Ref FrontendEnabled, "true" ]
Resources:
Api:
Type: 'AWS::CloudFormation::Stack'
Condition: IsApiEnabled
Properties:
Parameters:
DatabaseName: !Ref ApiDatabaseName
TemplateURL: 'https://s3.amazonaws.com/your-stack-bucket/api.yml'
Analytics:
Type: 'AWS::CloudFormation::Stack'
Condition: IsAnalyticsEnabled
Properties:
Parameters:
DatabaseName: !Ref AnalyticsDatabaseName
TemplateURL: 'https://s3.amazonaws.com/your-stack-bucket/analytics.yml'
Frontend:
Type: 'AWS::CloudFormation::Stack'
Condition: IsFrontendEnabled
Properties:
TemplateURL: 'https://s3.amazonaws.com/your-stack-bucket/frontend.yml'
The parameters.json
shouldn’t really look any different:
[
{
"ParameterKey": "ApiDatabaseName",
"ParameterValue": "foobarbaz"
},
{
"ParameterKey": "AnalyticsDatabaseName",
"ParameterValue": "bingbangboom"
}
]
Parameters
Parameters bubble down from the outer formation file down to the child formations. To keep things clean (and readable!), I tend to namespace my Parameter names in the outer formation file by prefixing them with the Stack’s Resource name. This is needed in the following example:
Say you have a database1 stack and a database2 stack defined in your formation template. Each
individual stack takes DatabaseName
as a Parameter (and lets assume they might be different
values). You need to differentiate them in the Parameters supplied to the outer template and I’ve
found it very clean, albiet verbose, to use the prefix of the Resource of that stack. For example,
the above formation has the following Resources: Api, Analytics, Frontend. So, any Parameters to
the API stack would have the “API” prefix; ie, ApiDatabaseName
.
A helpful Makefile
Annoyingly, the child stacks must have their formation file(s) in S3. I tend to create a Makefile
(for better or for worse sometimes…) so I have an error-free and limited typing dev cycle.
profile ?= default
region ?= us-east-1
default: update
define sync_bucket
aws s3 sync \
--profile $(profile) \
--region $(region) \
--delete \
formations/ s3://gc-stack-files
aws cloudformation $(1)-stack \
--profile $(profile) \
--region $(region) \
--stack-name stack \
--template-body file://formation.yml \
--parameters file://parameters.json
endef
create:
aws s3 mb \
--profile $(profile) \
--region $(region) \
s3://gc-stack-files || true
aws s3api put-bucket-versioning \
--profile $(profile) \
--bucket gc-stack-files \
--versioning-configuration Status=Enabled
$(call sync_bucket,create)
update:
$(call sync_bucket,update)
delete:
aws s3 rb \
--profile $(profile) \
--region $(region) \
s3://gc-stack-files \
--force
aws cloudformation delete-stack \
--profile $(profile) \
--region $(region) \
--stack-name stack
This allows make to just type make create
or make update
.
I also always need support for using other AWS cli profiles and regions. These are supported, too,
by doing something like make profile=otherprofile region=eu-west-2
(update is implicit default)
or just make region=eu-west-2 create
.