AWS: CloudFormation – using Conditions, Fn::Equals, and Fn::If – an example

By | 05/17/2020

I have a CloudFormation stack with VPC Peerings, in that case, it’s a peering between VPC of a new Elastic Kubernetes Service cluster and VPC of the Prometheus monitoring stack.

The EKS cluster’s stack and its whole automation creation were described in the AWS Elastic Kubernetes Service: a cluster creation automation, part 1 – CloudFormation and AWS Elastic Kubernetes Service: a cluster creation automation, part 2 – Ansible, eksctl posts.

The task: add an ability to chose if CloudFormation have to create the peering mentioned above – or skip this step.

The solution: use the AWS CloudFormation Conditions: will add a new parameter VPCPeeringCreate which will accept a true value false from a Jenkins job and then depending on this value CloudFormation will decide if need to create such a peering and related resources – the peering itself and two Routes.

The task is becoming a bit more complicated due to the fact, that I have Nested Stacks used there and resources are created by different stacks, so to create a peering it uses:

  • root-stack:
    • will create a Region and AvalabilityZones-located stacks
  • in the Region-stack:
    • will create  AWS::EC2::VPCPeeringConnection
    • will update a RouteTable of the remote Monitoring stack with Prometheus – will create a new AWS::EC2::Route (a route from the Monitoring VPC into the EKS stack’s VPC)
    • in itsOutputs will return a MonitoringProdVPCPeeringConnectionID
  • AvalabilityZones-located stack:
    • will grab the MonitoringProdVPCPeeringConnectionID
    • will create an AWS::EC2::Route (a route from the EKS cluster’s VPC private subnets to the Monitoring stack VPC)

So, let’s add them one-by-one to see how this is working.

Root stack

In the Parameters of the Root stack add a new parameter called VPCPeeringCreate, which can accept true or false:

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "AWS CloudFormation stack for Kubernetes cluster",

  "Parameters": {

...

    "VPCPeeringCreate": {
      "Description": "Create or not VPC peering connections",
      "Type": "String",
      "Default": true,
      "AllowedValues": [
          "true",
          "false"
      ]
    }

Update the Region-stack resource in the Root-stack template- add VPCPeeringCreate parameter to be passed to the Root-stack template:

  "Resources": {

    "RegionNetworkStack": {
      "Type": "AWS::CloudFormation::Stack",
      "Properties": {
        "TemplateURL": "eks-region-networking.json",
        "Parameters": {
          "VPCCIDRBlock": { "Ref": "VPCCIDRBlock" },
          "VPCPeeringCreate": { "Ref":  "VPCPeeringCreate"}
        }
      }
    },
...

Region Stack

Will accept the VPCPeeringCreate – remove the default value here as it will be passed from the Root-stack:

{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "AWS CloudFormation Region Networking stack for Kubernetes cluster",

  "Parameters" : {
...

    "VPCPeeringCreate": {
      "Description": "Create or not VPC peering connections",
      "Type": "String",
      "AllowedValues": [
          "true",
          "false"
      ]
    }
  },
...

Conditions

Now – the main part here: add the  Conditions called DoVPCPeeringCreate where we will check the VPCPeeringCreate‘s value using  Fn::Equals and if it’s true – then return true from the Condition:

...
    "VPCPeeringCreate": {
      "Description": "Create or not VPC peering connections",
      "Type": "String",
      "AllowedValues": [
          "true",
          "false"
      ]
    }
  },

  "Conditions" : {
    "DoVPCPeeringCreate" : {"Fn::Equals" : [ {"Ref" : "VPCPeeringCreate"}, true] }
  },

...

Next, add a check for both MonitoringProdVPCPeeringConnection and MonitoringToEksProdPeeringRoute resources – "Condition" : "DoVPCPeeringCreate":

...
    "MonitoringProdVPCPeeringConnection": {
      "Type": "AWS::EC2::VPCPeeringConnection",
      "Condition" : "DoVPCPeeringCreate",
      "Properties": {
        "VpcId": {
          "Ref": "VPC"
        },
        "PeerVpcId": { "Fn::ImportValue" : "monitoring-production-VPC-ID" },
        "PeerRegion": { "Fn::ImportValue" : "monitoring-production-StackRegion" },
        "Tags": [
          {
            "Key": "Name",
            "Value": { "Fn::Join": [ "-", [ {"Ref": "AWS::StackName"}, "vpc-monitoring-prod"] ] }
          }
        ]
      }
    },

    "MonitoringToEksProdPeeringRoute": {
      "Type": "AWS::EC2::Route",
      "Condition" : "DoVPCPeeringCreate",
      "Properties": {
        "RouteTableId": { "Fn::ImportValue" : "monitoring-production-VPC-PublicRouteTable" },
        "DestinationCidrBlock": { "Ref": "VPCCIDRBlock" },
        "VpcPeeringConnectionId": {
          "Ref": "MonitoringProdVPCPeeringConnection"
        }
      }
    },
...

So, if the DoVPCPeeringCreate Condition will return True – then the MonitoringProdVPCPeeringConnection and MonitoringToEksProdPeeringRoute resources will be triggered for creation.

Run to check – and here they are created:

Unresolved resource dependencies and Fn::If

And now – run the same but at this time specify the VPCPeeringCreate == false, and you’ll get an error because of missing value in the Outputs on th Region-stack:

Why so? Well – because we’ve disabled the peering to be created, but in the Region-stack’s Outpus we still trying to put an ID of the peering which wasn’t created:

...
    "MonitoringProdVPCPeeringConnectionID": {
      "Description" : "MonitoringProdVPCPeeringConnection ID",
      "Value" : {"Ref" : "MonitoringProdVPCPeeringConnection" }
    }
...

To resolve this – update the  Outputs and use the Fn::If to chose what exactly to return:

...
  "Outputs" : {

    "VPCID" : {
      "Description" : "EKS VPC ID",
      "Value" : { "Ref" : "VPC" }
    },

    "IGWID" : {
      "Description" : "InternetGateway ID",
      "Value" : { "Ref" : "InternetGateway" }
    },

    "MonitoringProdVPCPeeringConnectionID": {
      "Description" : "MonitoringProdVPCPeeringConnection ID",
      "Value" : { "Fn::If" : [ "DoVPCPeeringCreate", {"Ref" : "MonitoringProdVPCPeeringConnection" }, "Zero" ] }
    }

  }
}
...

I.e. if the DoVPCPeeringCreate Condition will return True – then we will grab the MonitoringProdVPCPeeringConnection’s ID and will put to the, if DoVPCPeeringCreate ==  false – then in the Outputs we will put some other value, here is for example “Zero”.

AvailabilityZones stack

And in the AZ-stack we need to do the same things – add the parameter and add the Conditions – copy-past it from the Region-stack, and create a check for the AWS::EC2::Route resource – "Condition" : "DoVPCPeeringCreate":

...
    "EksToMonitoringProdPeeringRoute": {
      "Type": "AWS::EC2::Route",
      "Condition" : "DoVPCPeeringCreate",
      "Properties": {
        "RouteTableId": {
          "Ref": "PrivateRouteTable"
        },
        "DestinationCidrBlock": {
          "Fn::ImportValue" : "monitoring-production-VPC-CIDR"
        },
        "VpcPeeringConnectionId": {
          "Ref": "MonitoringProdVPCPeeringConnectionID"
        }
      }
    },
...

In the Root-стеке add the VPCPeeringCreate to be passed to the AZ-stack:

...
    "AZNetworkStackA": {
      "Type": "AWS::CloudFormation::Stack",
      "Properties": {
        "TemplateURL": "eks-azs-networking.json",
        "Parameters": {
          "VPCID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] },
          "AZ": { "Fn::Select": [ "0", { "Ref": "AvailabilityZones" } ] },
          "IGWID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.IGWID"] },
          "VPCPeeringCreate": { "Ref":  "VPCPeeringCreate"},
...

Create stacks – and no peering created:

To check – create again, with the VPCPeeringCreate == true:

Done.