Conditional Infrastructure with AWS Cloud Formation

I’d recommend atleast peeking at the docs if you have not already. There will probably be some stuff in there that is out of the scope of this post.

The following are some usage examples I think Cloud Formation conditionals can really help with.

I’m going to be using YAML here instead of JSON. YAML easily converts into JSON so if you are stuck in the painful world of JSON, anything discussed here should carry right over. (there are converters).

A few words on Conditionals

Don’t think you can start throwing down programming logic in your Cloud Formation templates as if you were in your favorite scripting language. As with most things AWS, you must play by their rules with their tools.

The premise of Conditionals are you declare them up front in a Conditionals block and you use them in the Resources or Outputs blocks to create bits of logic using If/Then type logic. It can get messy if your logic is nested, but sometimes you can use (abuse?) conditionals a bit so your logic with nested If/Then constructs are atleast more readable. For example, rolling up Or logic in the conditional rather than where you are evaluating it.

Conditional Variables

This concept seems to be the common case in the AWS docs. Nothing really exciting here.

This example illustrates a Conditional that is truthy when a ParameterKey=Environment and ParameterValue is either prod or demo. Otherwise, it’s always falsy.

Cloud Formation template: formation.yml

AWSTemplateFormatVersion: 2010-09-09
Parameters:

  Environment:
    Type: String

Conditions:

  # Look at that super fancy Conditional!
  IsProdLike: !Or
    - !Equals [ !Ref Environment, "prod" ]
    - !Equals [ !Ref Environment, "demo" ]

Resources:

  MyBucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Sub "${AWS::AccountId}-cf-params"
      VersioningConfiguration:
        # Using the Conditional to set a variable in a specific way.
        Status: !If [ IsProdLike, "Enabled", "Suspended" ]

Parameters file: parameters.json

[
    {
        "ParameterKey": "Environment",
        "ParameterValue": "foobar"
    }
]

Create the stack:

aws cloudformation create-stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --stack-name cf-param-conditional \
    --template-body file://formation.yml \
    --parameters file://parameters.json

Check the file versioning:

# Stash the AWS account ID in a variable to build the S3 bucket name.
account_id=$(aws sts get-caller-identity --query 'Account' --output text)

# Query the S3 API to get the versioning status. (should be Suspended)
aws s3api get-bucket-versioning --bucket ${account_id}-cf-params

To verify I’m not full of it, change the Environment Parameter to be prod or demo. This should enable S3 versioning for that bucket.

# Set the environment to "prod"
sed -i '' s/foobar/prod/ parameters.json   # OS X, BSD
sed s/foobar/prod/ parameters.json         # Linux

# Update the Cloud Formation stack.
aws cloudformation update-stack \
    --stack-name cf-param-conditional \
    --template-body file://formation.yml \
    --parameters file://parameters.json

# Query the S3 API to get the versioning status. (should be Enabled)
aws s3api get-bucket-versioning --bucket ${account_id}-cf-params

Clean up:

aws cloudformation delete-stack --stack-name cf-param-conditional

Conditional Resources

So, this is where it starts to get interesting. It’s cool to do a bit of logic within a template so you don’t have to hardcode so many things. It also allows you to have a monolithic template for a service rather than force you into evil non-DRY ways like having a template for each specific use-case.

Taking this one step further, we can say “we want this resource to be created under a specific circumstance”. This is very handy for developer vs prod like environments where you want to optimize for cost and/or reduce the time it takes to stand up a new stack.

For example, suppose you have a Aurora RDS database in a Master/Slave setup for a prod instance. A developer wants to recreate some infrasturcture to start repeating a problem out of band from production. What this will lead to is a developer spinning up their own stack to mimick prod components. Really, does the dev really need a slave DB? It costs a bit more money and it adds quite a bit of time to set up and tear down (RDS takes forrreeevvverr).

I like to use a ${Service}Enabled pattern where I have a Parameter dictate what Conditional resources get stood up.

The formation.yml could use the Parameters across one or more Resources.

AWSTemplateFormatVersion: 2010-09-09
Parameters:

  SlaveEnabled:
    Type: String
    Default: "true"

Conditions:

  IsSlaveEnabled: !Equals [ !Ref SlaveEnabled, "true" ]

Resources:

  DbBackupBucket:
    Type: "AWS::S3::Bucket"
    Condition: IsSlaveEnabled
    Properties:
      BucketName: !Sub "${AWS::AccountId}-${AWS::Region}-database-backups"

Create the parameters.json file with:

[
    {
        "ParameterKey": "SlaveEnabled",
        "ParameterValue": "false"
    }
]

Create the stack:

aws cloudformation create-stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --stack-name cf-resource-conditional \
    --template-body file://formation.yml \
    --parameters file://parameters.json

Notice you should only have 1 DB instance:

aws rds describe-db-instances \
    --query 'DBInstances[?MasterUsername==`resource_conditionals`]'

Change the ParameterValue for SlaveEnabled to be true and update the stack.

# Set SlaveEnabled to "true"
sed -i '' s/false/true/ parameters.json   # OS X, BSD
sed s/false/true/ parameters.json         # Linux

aws cloudformation update-stack \
    --stack-name cf-resource-conditional \
    --template-body file://formation.yml \
    --parameters file://parameters.json

After waiting a bit (hit the Cloud Formation page in the Console to check status), you’ll notice there’s a replica/slave stood up!

aws rds describe-db-instances \
    --query 'DBInstances[?MasterUsername==`resource_conditionals`]'

Clean up:

aws cloudformation delete-stack --stack-name cf-resource-conditional

Conditional Stacks

Thus far we’ve implemented Parameter value logic and Resource creation logic. Let’s unpeel the onion one more layer!

This is sort of a combo feature since it requires you to also be using the AWS::CloudFormation::Stack primitive. I’ve written about it here for more info on it.

A Condition on a Stack is a great building block for à la carte infrastructure; meaning you can easily flag out stacks so you can isolate or easily choose which parts of a stack you want to stand up.

For example, suppose you have a few Stack definitions already:

  • API (RDS, EC2, S3 bucket for uploads)
  • Analytics Pipeline (Kinesis Input Stream, Kinesis Analytics App, Kinesis Firehose –> S3)
  • Frontend (S3 bucket for static assets, Cloud Front distribution for CDN, ACM cert for SSL)

Say you’re a developer. You want to do some analytics work but don’t care about spinning up an API stack (time, money, irrelevant) or a Frontend. You just want to get at the Analytics pieces.

Using a Condition on a AWS::CloudFormation::Stack Resource, we can accomplish this very easily with the same pattern that was used for a regular ‘ol Resource.

For example, take this formation.yml:

AWSTemplateFormatVersion: 2010-09-09

Parameters:

  ApiEnabled:
    Type: String
    Default: "true"

  AnalyticsEnabled:
    Type: String
    Default: "true"

  FrontendEnabled:
    Type: String
    Default: "true"

Conditions:

  IsApiEnabled: !Equals [ !Ref ApiEnabled, "true" ]
  IsAnalyticsEnabled: !Equals [ !Ref AnalyticsEnabled, "true" ]
  IsFrontendEnabled: !Equals [ !Ref FrontendEnabled, "true" ]

Resources:

  Api:
    Type: 'AWS::CloudFormation::Stack'
    Condition: IsApiEnabled
    Properties:
      Parameters:
        SomeParameter: "abc123"
      TemplateURL: 'https://s3.amazonaws.com/your-stack-bucket/api.yml'

  Analytics:
    Type: 'AWS::CloudFormation::Stack'
    Condition: IsAnalyticsEnabled
    Properties:
      Parameters:
        SomeParameter: "abc123"
      TemplateURL: 'https://s3.amazonaws.com/your-stack-bucket/analytics.yml'

  Frontend:
    Type: 'AWS::CloudFormation::Stack'
    Condition: IsFrontendEnabled
    Properties:
      Parameters:
        SomeParameter: "abc123"
      TemplateURL: 'https://s3.amazonaws.com/your-stack-bucket/frontend.yml'

Then, with a parameters.json file like:

[
    {
        "ParameterKey": "ApiEnabled",
        "ParameterValue": "false"
    },
    {
        "ParameterKey": "AnalyticsEnabled",
        "ParameterValue": "true"
    },
    {
        "ParameterKey": "FrontendEnabled",
        "ParameterValue": "false"
    }
]

You can begin to see what sort of toggles we can build. Pretty powerful stuff for just a little bit of YAML config!

Note: You can’t use multiple conditions for a Resource’s Condition value. This is lame, I know. You need to just get creative with your Condition blocks and use !And and !Or to create a single Condition that suits your needs.