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:

The CloudFormation process

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:

Resource requiring replacement

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.

Twitter