CloudFormation: Conditionals in Resource Parameters

Our organization has been doing a bunch of AWS CloudFormation lately. Recently I’d stumbled across a method for doing conditional logic within any Resource’s Parameters array.

Conditional logic within a Resource’s Parameters comes up often when it’s something like an RDS Database instance.   In that Resource type, there are explicit Parameters (like AllocatedStorage),  but there are also Parameters which contain an array, like VPCSecurityGroups.

There are many options to configure, and if you modularize your CloudFormation templates as we do, you’ll find it necessary to completely change, or even omit certain Parameter array elements depending on other parts of your template.

The method for doing so is something I hadn’t found documented in the official AWS CloudFormation material on Conditionals, so figured I’d put it here to help my other DevOps brethren.

Take the following example snippet:

Conditions:
  conditions !Equals [ !Ref parameterEnv, "Prod" ]

Resources:
  createDatabase:
    Type: "AWS::RDS::DBInstance"
    Properties:
      AllocatedStorage: "5"
      Engine: "MySQL"
      EngineVersion: "5.6"
      DBInstanceClass: "db.t2.small"
      MasterUsername: "someuser"
      MasterUserPassword: "somepassword"
      VPCSecurityGroups:
        - sg-abc12345
        - sg-def67890

In the above example, we’re creating a basic RDS instance that has two security groups.   In addition, there is a Conditions statement checking whether the CloudFormation template is used for a production environment.

Let’s say there is now a requirement to omit SecurityGroup “sg-def67890” because that allows access from the developer network.

In some examples I saw accomplished it this way, rather inelegantly:

Conditions:
  conditionIsProd !Equals [ !Ref parameterEnv, "Prod" ]

Resources:
  createDatabase:
  Type: "AWS::RDS::DBInstance"
  Condition: !Not conditionIsProd
  Properties:
    AllocatedStorage: "5"
    Engine: "MySQL"
    EngineVersion: "5.6"
    DBInstanceClass: "db.t2.small"
    MasterUsername: "someuser"
    MasterUserPassword: "somepassword"
    VPCSecurityGroups:
      - sg-abc12345
      - sg-def67890

  createDatabase:
    Type: "AWS::RDS::DBInstance"
    Condition: conditionIsProd
    Properties:
      AllocatedStorage: "5"
      Engine: "MySQL"
      EngineVersion: "5.6"
      DBInstanceClass: "db.t2.small"
      MasterUsername: "someuser"
      MasterUserPassword: "somepassword"
      VPCSecurityGroups:
        - sg-abc1234

Doing it in the way above is also limiting.  If you have multiple permutations of options in your CloudFormation template, you’d need to build out a Resource stanza for each situation.  Not very extensible.

Instead, try this (it works):

Conditions:
  conditionIsProd !Equals [ !Ref parameterEnv, "Prod" ]

Resources:
  createDatabase:
    Type: "AWS::RDS::DBInstance"
    Properties:
      AllocatedStorage: "5"
      Engine: "MySQL"
      EngineVersion: "5.6"
      DBInstanceClass: "db.t2.small"
      MasterUsername: "someuser"
      MasterUserPassword: "somepassword"
      VPCSecurityGroups:
        - sg-abc12345
        - !If
            - conditionIsProd
            -
              !Ref "AWS::NoValue"
            - sg-def67890

This strays from the official documenation examples because the Fn::If function is being leveraged within an array element.   Even though it seems counter-intuitive, the key is to place the If statement after the array delimiter for the parent object, which in this YAML example is the hypen, i.e.

- !If

The only caveat here is that you’re limited to one array item within the conditional statement.   So if you need multiple array lines to accomplish your goals, you’ll need an Fn::If for each.

However,  if your scenario requires multiple lines within one element, putting them in one Fn::If works fine.

Now go automate everything in your AWS environments!