You can find plenty of frameworks and tools to provision your AWS resources. Some of them do a great job for a specific purpose, others are more generic. Nevertheless, I do prefer to use native CloudFormation templates as much as possible.
Together with my Makefile Best-Practices you can build a powerful toolbox around your service without managing a complex list of dependencies.
Prefix all the Things
When using Continuous Integration and Continuous Deployment patterns for your service, you might want to spin up your CloudFormation Stack in different environments automatically. Sooner or later, you might end up with resources using the same name. So: Add a prefix or suffix to all your resources.
AWSTemplateFormatVersion: 2010-09-09
Resources:
User:
Type: AWS::IAM::User
Properties:
UserName: !Sub ${Prefix}-username
Parameters:
Prefix:
Type: String
Using prefixes, you can re-use a CloudFormation template for all your environments. Regardless if it’s for production
and stable
, or if you want to spin up temporary environments having a pullrequest-123
prefix.
$ > aws cloudformation deploy \
--template-file ./infra.yml \
--parameter-overrides Prefix=production
Split up Stacks
Of course, you can put all resources into a single CloudFormation Stack. But don’t! As soon as you have resources for storage or state handling and consumers to access the data, split those resources into multiple stacks. Only group resources by their technical context, not the AWS service name.
./example
├── Makefile
├── README.md
└── infra
├── api.yml
├── domain.yml
└── storage.yml
I tend to store all CloudFormation templates inside a folder named infra
and name them by purpose, not by AWS product names.
Nested Stacks
With multiple CloudFormation Stacks, you cannot avoid dependencies between stacks. Sometimes you need the Name
of a resource or an Arn
to create an IAM Policy
for example.
CloudFormation Stacks can define Outputs
and you can use Export
to enable access to those outputs by other CloudFormation Stacks. But don’t! Avoid exporting CloudForamtion Stack Outputs whenever possible! Nested CloudFormation Stacks provide a slick approach to handle dependencies between stacks and minimize the needed tooling.
./example
├── Makefile
├── README.md
├── infra.yml
└── infra
├── api.yml
└── storage.yml
AWSTemplateFormatVersion: 2010-09-09
Resources:
Storage:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./infra/storage.yml
API:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./infra/api.yml
Parameters:
DynamoDBStreamArn: !GetAtt Storage.Outputs.DynamoDBStreamArn
With a Nested CloudFormation Stack, you do not need to invest effort and time to come up with tooling that reads the Outputs
from one stack and passes it as Parameters
to another stack. Just use GetAtt
to pass data from one stack to another.
Property Mappings
If your setup uses Continuous Integration and Continuous Deployment, you may want to have different provisioned resources. Together with passing a prefix to my CloudFormation Stack, I prefer to pass an environment identifier, called ENV
for example.
Parameters:
ENV:
Type: String
Prefix:
Type: String
Having two parameters ENV
and Prefix
, it’s simple to deploy multiple CloudFormation Stacks to one environment; PR15
and PR16
for the dev
environment for example. Thanks to CloudFormation Mappings, and the FindInMap
function, you can provision resources based on the ENV
parameter now:
Mappings:
ElasticsearchDomainConfiguration:
prod:
InstanceType: m4.10xlarge.elasticsearch
dev:
InstanceType: t2.micro.elasticsearch
Resources:
ElasticsearchDomain:
Type: AWS::Elasticsearch::Domain
Properties:
DomainName: !Sub ${Prefix}-es-domain
ElasticsearchClusterConfig:
InstanceType:
Fn::FindInMap:
[ElasticsearchDomainConfiguration, !Ref ENV, InstanceType]
As there is no support for fallback values for FindInMap
, you must need to configure a mapping configuration for every value of ENV
. In this case, there is luckily only prod
and dev
as environment types.