AWS: CloudFormation – using lists in Parameters

By | 05/08/2020
 

In addition to 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 – now I’d like to pass a Parameter as a List with multiply values to a CloudForamtion stack.

The idea is to get all AvailabilityZones of a region in Ansible, and then use this list for the eksctl, which will create WorkerNodes groups in dedicated AvailabilityZones, and use the same list for CloudFormation to create child stacks in right AvailabilityZone.

With Jenkins and Ansible it will work in a next way:

  1. from a Jenkins, job Parameters take a $REGION with value eu-west-3
  2. a Jenkins job will spin up a Docker container with Ansible passing the $REGION variable as an environment variable
  3. Ansible using its {{ region }} variable will get a list of the AvailabilityZones of the region specified from Jenkins
  4. and will pass this list to the cloudformation role to create two nested stacks in two AvailabilityZones
  5. and will pass this list to the eksctl role to create NodeGroups in two AvailabilityZones

At this moment in my CloudFormation AvailabilityZones are taken via the Fn::GetAZs - { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] }:

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

So, instead of using  { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] } – let’s add a new task in Ansible to obtain all AvailabilityZones of a region where a CloudFormation stack will be created and then both its roles – eksctl и cloudformation – will use the same list with the same values.

The initial example was googled long time ago here – How do I use multiple values for individual parameters in an AWS CloudFormation template, and now I got a chance to use it in a real example.

Ansible

The most difficult task was to create the list in such a form, that CloudForamtion can use it in a correct way, so let’s start with this.

Also, as this list will be used by two separated roles in Ansible – worth to move it in a dedicated task and call it from the roles directly, to avoid duplicating the code.

So, create a common role:

mkdir -p roles/common/tasks

Create a new file roles/common/tasks/main.yml and create a task here to obtain all AvailabilityZones and save the result to a cluster_azs variable:

- name: "Getting AvailabilityZones list"
  command: "aws ec2 describe-availability-zones --region {{ region }} --query 'AvailabilityZones[*].ZoneName' --output text"
  register: cluster_azs

The cluster_azs will return us a structure like the next:

...
ok: [localhost] => {
"msg": {
"changed": true,
"cmd": [
"aws",
"ec2",
"describe-availability-zones",
"--region",
"eu-west-2",
"--query",
"AvailabilityZones[*].ZoneName",
"--output",
"text"
],
"delta": "0:00:00.993216",
"end": "2020-04-08 15:55:56.357234",
"failed": false,
"rc": 0,
"start": "2020-04-08 15:55:55.364018",
"stderr": "",
"stderr_lines": [],
"stdout": "eu-west-2a\teu-west-2b\teu-west-2c",
"stdout_lines": [
"eu-west-2a\teu-west-2b\teu-west-2c"
]
}
}
...

Now, from the stdout string with the “eu-west-2a\teu-west-2b\teu-west-2c” value need to create a comma-separated list.

The elements here are divided with the TAB, so we can use the split() function to separate them, and then pass the data via a pipe to the join() which will concatenate them to a new list – but with the comma as a separator:

...
- set_fact:
    cluster_azs_names: "{{ cluster_azs.stdout.split('\t') | join(',') }}"

And add a debug at the end to check the result:

...
- debug:
    msg: "cluster_azs_names: {{ cluster_azs_names }}"

Check:

...
TASK [cloudformation : debug] ****
ok: [localhost] => {
"msg": "cluster_azs_names: eu-west-2a,eu-west-2b,eu-west-2c"
}
...

Cool.

CloudFormation Parameter

List<AWS::EC2::AvailabilityZone::Name

Go to your CloudFormation template and add a new parameter with the “List<AWS::EC2::AvailabilityZone::Name>” type. See the full list in the AWS-Specific Parameter Types:

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

  "Parameters": {

    "VPCCIDRBlock": {
      "Description": "VPC CidrBlock",
      "Type": "String",
      "Default": "10.0.0.0/16"
    },
    "AvailabilityZones": {
      "Type": "List<AWS::EC2::AvailabilityZone::Name>",
      "Description": "The list of the AvailabilityZones in a current Region",
      "Default": "eu-west-2a, eu-west-2b"
    }
  },

...

Nett, add nested stacks and instead of the { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] } use { "Fn::Select": [ "0", { "Ref": "AvailabilityZones" } ] } – to obtain the first element from the list passed to the AvailabilityZones Parameter:

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

Pack them (see the AvailabilityZonesAWS: CloudFormation – Nested Stacks and stacks parameters Import/Export):

aws --region eu-west-2 cloudformation package --template-file eks-root.json --output-template /tmp/packed-eks-stacks.json --s3-bucket eks-cloudformation-eu-west-2 --use-json

Run:

ansible-playbook eks-cluster.yml --tags infra
...
TASK [cloudformation : Getting AvailabilityZones list] ****
changed: [localhost]
TASK [cloudformation : set_fact] ****
ok: [localhost]
TASK [cloudformation : debug] ****
ok: [localhost] => {
"msg": "cluster_azs_names: eu-west-2a,eu-west-2b,eu-west-2c"
}
TASK [cloudformation : Create EKS EKSCTL-BTTRM-EKS-DEV-3-STACK CloudFormation stack] ****
changed: [localhost]
PLAY RECAP ****
localhost                  : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Check:

Done.



Also published on Medium.