Using CloudFormation Mappings to define preset resource reservations for rightsizing

Using CloudFormation Mappings to define preset cpu, memory or instance types for rightsizing infrastructure

Table Of Contents

Today I Explained

When developing within AWS (& the cloud in-general), it is often the case that multiple environments of an applications exist. This can be for the purpose of prototyping, staging environments for ensuring new changes are working as intended, or taking advantage of computing savings that can exist within other AWS Regions.

This can look something like the following:

      ┌────────AWS────────┐
      │                   │
      ▼                   ▼
   Staging         ┌──Production──┐
┌──────────┐       │              │
│x         │       ▼              ▼
└──────────┘   us-east-1     eu-central-1
              ┌──────────┐   ┌──────────┐
              │xxx       │   │xxxxxxxx  │
              └──────────┘   └──────────┘

In the code block, it shows a graph with a top level node named “AWS”. This node has 2 child nodes, Staging & Production. The staging node has a box below it, showing a utilization of 10%. The production node has 2 child nodes, us-east-1 and eu-central-1. The us-east-1 node has a box below it, showing a utilization of 30%, and the eu-central-1 node has a box below it, showing a utilization of 80%.

This can cause unintended wasteful spending within AWS, as not all environments of the application will have the same resource needs. A production environment requiring 16GB of memory, most likely isn’t needed for the staging environment which isn’t receiving the same traffic as the production environment. The same with other production environments in which the production traffic isn’t evenly distribution between them.

This is illustrated in the above code block, which shows how much of the allocated resources each of the environments is actually using (10% staging, 30% us-east-1, and 80% eu-central-1).

When working within CloudFormation, you could parameterize the instance type of an EC2 or the requested resources of a lambda or ECS task. This may present an organization challenge if many environments exist. To be consistent across services, it is easier to refer to the cpu/memory/etc requested by our application as the resource reservations.

An alternative approach is to define a set of presets within the CloudFormation template itself, using the Mapping to declare the properties such as InstanceType. A single parameter can be used within the CloudFormation template to select from each of these definitions.

AWSTemplateFormatVersion: "2010-09-09"

# ...

Parameters:
  PresetType:
    ConstraintDescription: must be the path to a node contained within the Mapping 'PresetTypeMapping'
    Type: String
    Default: standby
    AllowedValues:
      - standby
      - default
      - staging

Mappings:
  PresetTypeMapping:
    standby:
      InstanceType: t1.micro
    default:
      InstanceType: t2.nano
    staging:
      InstanceType: t2.micro

Outputs:
  InstanceType:
    Value:
      Fn::FindInMap:
        - PresetTypeMapping
        - Ref: PresetType
        - InstanceType

This approach allows for all of our environments to select from the preset resource reservations an approach resoruce reservation based on the workflows.

A note on overrides

In the event of a hotfix, or should a case arise in which the need exists to override the preset resource reservation, this current pattern would require modifying the CloudFormation template to change the resource reservation for a given CloudFormation stack. This is less than ideal, and presents an opportunity for mistakes.

This can be addressed by adding support to the CloudFormation template for overrides. This is a pattern in CloudFormation in which a Parameter exists, that should it be set, will override the default configuration behaviour.

This can be accomplished like so:

# ... (Mapping & others)

Parameters:
  # ...
  InstanceType:
    Description: Override the preset instance. If left blank, InstanceType will be determined by the presets.
    Type: String
    Default: ""
    ConstraintDescription: must be a valid EC2 instance type, or blank
    AllowedValues:
      - ""
      - t1.micro
      - t2.nano
      - t2.micro
      - t2.small
      - t2.medium
      - t2.large

Conditions:
  IsInstanceTypeDefault:
    Fn::Equals:
      - Ref: InstanceType
      - ""

Outputs:
  InstanceType:
    Description: The 'InstanceType' from the presets, unless overwritten by the parameter 'InstanceType'
    Value:
      Fn::If:
        # If unset, use the Preset, if set, use the parameter
        - IsInstanceTypeDefault
        - Fn::FindInMap:
            - PresetTypeMapping
            - Ref: PresetType
            - InstanceType
        - Ref: InstanceType

A note on rightsizing with presets

The usage of presets doesn’t mean that all environments will be perfectly rightsized for their compute workloads. Depending on the number of environments, and the nature of the workload, it can still mean suboptimal utilization in environments. What this pattern does is provide an approach to rightsize environments in a way that doesn’t require the adoption of a new deployment paradigm, or the overhead of managing per-environment parameters for the resource reservations.