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:
- from a Jenkins, job Parameters take a
$REGION
with value eu-west-3 - a Jenkins job will spin up a Docker container with Ansible passing the
$REGION
variable as an environment variable - Ansible using its
{{ region }}
variable will get a list of the AvailabilityZones of the region specified from Jenkins - and will pass this list to the cloudformation role to create two nested stacks in two AvailabilityZones
- 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.
Contents
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:
[simterm]
$ mkdir -p roles/common/tasks
[/simterm]
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:
[simterm]
... 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" ] } } ...
[/simterm]
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:
[simterm]
... TASK [cloudformation : debug] **** ok: [localhost] => { "msg": "cluster_azs_names: eu-west-2a,eu-west-2b,eu-west-2c" } ...
[/simterm]
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):
[simterm]
$ 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
[/simterm]
Run:
[simterm]
$ 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
[/simterm]
Check:
Done.