Protecting CloudFormation stacks with stack policies
When developing Serverless applications for AWS it's likely that you'll end up working with CloudFormation before long. The Serverless Framework relies heavily on CloudFormation to manage your application infrastructure and if you require resources beyond Lambda functions and API Gateways then it's likely you'll be using CloudFormation yourself too. AWS describes CloudFormation as follows:
AWS CloudFormation provides a common language for you to describe and provision all the infrastructure resources in your cloud environment. CloudFormation allows you to use a simple text file to model and provision, in an automated and secure manner, all the resources needed for your applications across all regions and accounts. This file serves as the single source of truth for your cloud environment.
The Serverless Framework produces a CloudFormation template based on the
configuration of your functions, along with any custom resources
defined in your serverless.yml
file. That template is then uploaded to an S3
bucket from where it is used to create a CloudFormation stack. This diagram,
from the CloudFormation documentation shows the process more clearly:
Do you learn better with a more hands-on approach?
If a face-to-face approach to learning works better for you or your team, orangejellyfish run a popular Serverless workshop which can take place remotely or on-site with you, giving you an opportunity to go deeper into some more advanced Serverless concepts.
Stacks and resources
A stack is the collection of AWS resources that are created to correspond to a template. A stack has a single source of truth - the S3 bucket to which the template was uploaded. This means it's possible to update an existing stack, by modifying the template in that bucket, rather than having to tear it down and replace it each time you need to make a change to a resource.
Let's look at a practical example. Here we have a CloudFormation template that defines a single resource - a DynamoDB table. You can find detailed documentation for each type of resource in the CloudFormation guide on AWS.
Resources:
ExampleTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
When we instantiate a stack from this template, CloudFormation will create the DynamoDB table to our specification. If we want to modify the table we can edit the template and ask CloudFormation to update the stack. This is where things can get a little dangerous.
Updating stacks
Imagine that our current stack, consisting of a single DynamoDB table resource, has been running in production for some time, and the table is therefore full of data. We have a requirement that means we need to make some changes to the table so we update the CloudFormation template:
Resources:
ExampleTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
We've replaced ProvisionedThroughput
with BillingMode
, which changes the way
AWS charges us for DynamoDB usage. When we update the stack CloudFormation
detects the change and applies it to the existing table with no interruption,
and therefore no impact to our running application.
Now we need to make another change:
Resources:
ExampleTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
- AttributeName: updatedAt
AttributeType: N
KeySchema:
- AttributeName: id
KeyType: HASH
- AttributeName: updatedAt
KeyType: RANGE
BillingMode: PAY_PER_REQUEST
We've added an additional attribute to our key schema, which would allow us to efficiently query for items in the table by their "updatedAt" timestamp. This time, when we update the stack CloudFormation replaces the table instead of updating the existing resource. All of the data in the original table has been destroyed and we are left with a completely new one that conforms to the template but has none of the original data. If you have done this in production I hope you had an effective backup strategy on the original table!
Stack policies
To offer some protection from this scenario we can make use of the "stack policy" feature of CloudFormation. A stack policy states whether or not resources created by a template can be updated or deleted. A stack policy can be set on a stack via the AWS CLI once the stack has been created or, if you're using the Serverless Framework, it can be incorporated into your configuration as code. We can apply the following stack policy to prevent our DynamoDB table from being replaced:
{
"Statement" : [
{
"Effect" : "Deny",
"Action" : "Update:Replace",
"Principal": "*",
"Condition" : {
"StringEquals" : {
"ResourceType" : ["AWS::DynamoDB::Table"]
}
}
},
{
"Effect" : "Allow",
"Action" : "Update:*",
"Principal": "*",
"Resource" : "*"
}
]
}
If, with this policy in place, we now attempt to return to the previous key schema, an operation that would again require replacement of the resource, we are notified that CloudFormation is unable to perform the update.
It is good practice to set a strict stack policy on your CloudFormation stacks
to ensure you don't accidentally replace resources and therefore avoid
potentially unrecoverable scenarios. You can get an idea of resource properties
that might require resource replacement upon update from the CloudFormation
template reference. For example, the DynamoDB table resource reference
shows that updates to the KeySchema
property always require replacement of the
table resource:
Stack policies with the Serverless Framework
As mentioned previously, if you're using the Serverless Framework to
produce CloudFormation templates you can manage the stack policy in code along
with the rest of your infrastructure. The serverless.yml
configuration file
supports a stackPolicy
property:
service: example-service
provider:
name: aws
runtime: nodejs8.10
stackPolicy:
- Effect: Deny
Action: Update:Replace
Principal: "*"
Resource: "*"
Condition:
StringEquals:
ResourceType:
- AWS::DynamoDB::Table
- Effect: Allow
Action: "Update:*"
Principal: "*"
Resource: "*"
Updating a protected resource
Once you have set a stack policy such as the one above you are restricted from making any changes that would violate the policy. In our case that means we are unable to update the DynamoDB table schema, something that isn't a particularly rare occurrence.
If we have to make such a change, we first have to modify the stack policy to allow it. This step should help to enforce extra care around this kind of change and could be taken further by perhaps only allowing certain users to modify the stack policy via an IAM role. If you're using the Serverless Framework and have added the stack policy to your configuration file, these changes can also be tracked in source control, giving further visibility and accountability.