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.