Brief Intro to CloudFormation
This post is adapted from a quick-start guide I wrote for our infrastructure team. I wrote this because the CloudFormation docs, though well-written and thorough, are hardly digestible as a newcomer.
This is an introduction to CloudFormation. This is meant to be a quick intro and reference; for more detail, headers are links to the CloudFormation docs.
CloudFormation allows us to describe and deploy all the resources and configuration needed for an application in a single file called a template. A template is deployed to create a stack of resources.
- A version controlled template means version controlled configuration of our infrastructure
- One template can deploy multiple stacks/environments
- Stacks describe what resources are related and let us provision, update, and teardown
- Each deployment yields outputs which can be used to parameterize other processes
Stack Drift
That last point means that stack resources should only be configured through templates. Changes outside the template lead to "stack drift". AWS can help detect drift for some resources.
In the CloudFormation console you can view a list of stacks and their templates, history, resources, and outputs.
Templates
Templates can be in yaml or json. We'll use yaml here for the sake of readabiity. All of the AWS documentation comes with examples in both formats. You can easily convert between the two using the cfn-flip
utility.
The main components of a template:
Parameters
Inputs that are specified at the time of deployment. They can have descriptions and constraints:
Parameters:
AppSlug:
Type: String
AllowedPattern: ^[\w\s+=,.@-]+$
ConstraintDescription: must be a valid name for a Cognito client.
Description: Display name of the application.
Environment:
Type: String
AllowedPattern: (prod|stage|dev|review.*)
Description: Name of the environment.
To define them at deploy-time, use the (oddly-named) --parameter-overrides
argument.
aws cloudformation deploy
--template-file packaged.yaml
--stack-name myapp-prod
--parameter-overrides AppSlug=myapp Environment=prod --capabilities CAPABILITY_IAM
Resources
A list of AWS resources to provision/configure as the stack.
For each resource declare its name WebsiteBucket
, type AWS::S3::Bucket
, and how it should be configured. You must check the resource reference to know what a resource's properties can be!
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub
- app-${AppSlug}-${Environment}
- AppSlug: !Ref 'AppSlug'
Environment: !Ref 'Environment'
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: 404.html
Tags:
- Key: application
Value: !Ref 'AppSlug'
- Key: environment
Value: !Ref 'Environment'
Let's break down some of the other things that are going on in this little chunk of template. Here we can see our parameters come into play. We are able to reference them using...
Intrinsic Functions
Again with the smooth Amazon naming. These are just ways to inject some dynamic behavior into our templates. Let's look at a few of them.
!Ref
!Ref
returns the value of a parameter or resource. To know what you can get out of a resource, you need to check its Ref return value in the resource reference.
In the case of a S3 Bucket, we would get back the bucket name.
Other times, we just want to refer to one of our parameters:
Outputs:
BucketName:
Value: !Ref WebsiteBucket # -> app-myapp-prod WebsiteDomain:
Value: !GetAtt WebsiteCloudfront.DomainName
!GetAtt
If we want to get back other information about the bucket, we can instead use !GetAtt
to get one of the other resource's return values:
Outputs:
BucketName:
Value: !Ref WebsiteBucket
WebsiteDomain:
Value: !GetAtt WebsiteCloudfront.DomainName # -> app-myapp-prod-kdwwxmddtr2g.s3.amazonaws.com
!Sub
This function let us substitute values into a string. It takes an array: the first member is the string, and the second member is the map of key/values to substitute. We have to use !Ref
inside of !Sub
to pull in our parameters.
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub - app-${AppSlug}-${Environment} - AppSlug: !Ref 'AppSlug' Environment: !Ref 'Environment' # -> app-myapp-prod
Prefer
!Sub
over!Join
Often people use
!Join
for this kind of thing; however, I think!Sub
leads to a more readable template.
Outputs
Key/value pairs which indicate important values of the stack's resources. These are pretty straightforward; we grab values from our resources using !Ref
or !GetAtt
and assign names to them.
Outputs:
BucketName:
Value: !Ref WebsiteBucket
WebsiteDomain:
Value: !GetAtt WebsiteCloudfront.DomainName
CognitoClientId:
Value: !Ref WebsiteCognitoClient
These are especially useful because they can be used as parameters for parts of the deployment process handled outside of CloudFormation!
Deployment
Deploying a template is pretty straightforward, and it will usually happen in CI using the AWS CLI.
If we need to deploy code along with our template (like for a Lambda function) we must package it as an artifact and upload it to a bucket. The package
command will find the code referenced in the template and do just that:
aws cloudformation package
--template-file template.yaml
--s3-bucket cf-templates-36sbt3jod97u-us-west-2
--output-template-file packaged.yaml
This outputs another template that we can then deploy. If no artifacts need to be packaged, we can just deploy the original template directly.
aws cloudformation deploy
--template-file packaged.yaml
--stack-name myapp-prod
--capabilities CAPABILITY_IAM
--parameter-overrides AppSlug=myapp Environment=prod
Deletion
Stacks can be deleted end all resources will be torn down, unless the template specifically says to retain them.
Deleting S3 Buckets
Unless otherwise configured, S3 Buckets must be emptied before their stack is deleted! Trying to delete a non-empty bucket will cause the entire operation to fail.