CloudFormation, Applications & Separation of Concerns

Using multiple CloudFormation Stacks to separate key resources

Table Of Contents

Today I Explained

CloudFormation supports the resource type AWS::CloudFormation::Stack, which allows a CloudFormation stack to provision another CloudFormation stack. This is known as a nested stack. These nested stacks allow re-using an already defined CloudFormation stack when deploying a stack, or just as a mechanism for grouping.

A pattern that comes up with nested stacks is using them for separating resources based on concerns. Specifically around the categories of:

  • Identity and Access Management (IAM)
  • Endpoints (DNS, WebACLs, Load Balancing)
  • Data Storage (Databases, Object Storage)
  • Compute (Servers, Functions)

In this pattern, the top-level template is responsible for provisioning the “application”:

 Application                               
┌─────────────────────────────────────────┐
│                                         │
│ ┌─┐                                     │
│ │*│Resource Group                       │
│ └─┘                                     │
│                                         │
│ ──────────────────────────────────────  │
│  IAM      Endpoint  Database   Compute  │
│ ┌──────┐ ┌───────┐ ┌────────┐ ┌───────┐ │
│ │      │ │       │ │        │ │       │ │
│ │ x    │ │    a  │ │ d   h  │ │    j  │ │
│ │    z │ │b      │ │        │ │ h     │ │
│ │  y   │ │    d  │ │   r    │ │     r │ │
│ └──────┘ └───────┘ └────────┘ └───────┘ │
│                                         │
└─────────────────────────────────────────┘

The top-level stack is provisioning resources like Resource Groups, or unique identifiers like Deployment Keys that are used within the other stacks. The substacks within the application are responsible for provisioning pre-built archetypes for the application, and if necessary, using custom built alternatives.

This allows an organization to adopt well-lit paths for applications, taking into consideration threat, cost and performance models. The most common archetype within this model is the “Endpoint”, which provides:

  • A load balancer
  • Predefined security group with appropriate IP allowlists (via prefix lists or IP Sets)
  • A WebACL, with appropriate built-in monitoring & logging controls
  • Predefined list of controls designed to mitigate concerns such as Distributed Denial of Service (DDOS), Regional Outages or ScaleToZero.

A note on the libraries

In comparison to this model, a library model provides flexibility with how a CloudFormation template is crafted, and doesn’t require the usage of nested stacks as the resources are added to the top-level stack. This can be demonstrated using Jsonnet below:

local HTTPSEndpoint(name, sgs) = {
  name: name,
  # ... construct resource here
};

{
    'Resources': [
        HTTPSEndpoint('Service', ...)
    ]
}

The reason why this pattern is being built in the deployment layer, rather than as a library, is for attestation about the resources themselves. Each of the CloudFormation stacks provisioned by the top-level can be considered self-contained. The resources within cannot be modified, or altered beyond what minimum configuration permissions are provided.

An endpoint in this pattern is deployment ready, relying on the properties of the environment (Parameter store variables) to handle opinionated conditions of the blueprints capabilities, rather than permitting misconfiguration through parameters. Using strong references of the outputs from the stacks, prevents accidental deletion of key resources, or undesired actions from being performed.

A note on the state machines

This pattern often adopts ad-hoc state machines using parameters passed to the top-level stack. This model is intended to model robust lifecycle policies in CloudFormation by allowing the stack to slowly transition to safely removing resources. One of the common examples of this is the transition of:

  • Active -> Deactivating -> Deactivated - As a way of halting incoming traffic to the application
  • Deactived -> Expiring -> Expired - As a way of slowly purging all resources within the stack (e.g. S3 bucket expiration)
  • Expired -> Deleting -> Deleted - As a way of tearing down the resources that are no longer in-use

These implementations are often rough as the reusable CloudFormation templates haven’t been planned, so much as built-over-time, which results in them having a number of fragile edges that cannot easily be re-implemented.