В продолжение темы AWS: Elastic Kubernetes Service — автоматизация создания кластера, часть 1 — CloudFormation – теперь надо добавить передачу в стек параметра в виде списка.
Идея в том, что бы в Ansible получать все AvailabilityZones, а потом этот список использовать для eksctl
, который будет создавать WorkerNodes в разных AvailabilityZones, и для CloudFormation – что бы создавать дочерние стеки в нужных AvailabilityZone, используя этот же список.
А с Jenkins Ansible всё будет так:
- из параметров Jenkins-джобы передаём $REGION в виде eu-west-3
- джоба в Jenkins запускает Docker-контейнер с Ansible, и ему передаёт $REGION в виде переменной окружения
- Ansible, используя переменную {{ region }} получает список всех AvailabilityZones этого региона
- и передаёт список в роль cloudformation для создания двух стеков в двух AZ
- и в роль eksctl для создания NodeGroups в двух AZ
Сейчас в CloudFormation AvailabilityZones получаются прямо в шаблоне через вызов 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": "" } ] }, ...
Итак, вместо выборки { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] }
– в Ansible добавим задачу, которая будет получать список AvailabilityZone региона, в котором разворачивается стек, а потом обе роли – eksctl и cloudformation – будут использовать единый список из единого источника.
Пример со списками в CloudForamtion был когда-то нагуглен тут – How do I use multiple values for individual parameters in an AWS CloudFormation template, теперь появился повод его попробовать в деле.
Содержание
Ansible
Больше всего сложности было с формированием списка в таком виде, что бы его скушал CloudFromation, поэтому – начнём с него.
Причём, так как этот список будет использоваться в двух разных ролях – имеет смысл вынести получение списка AvailabilityZones в отдельную задачу, и её вызывать из обеих ролей, что бы не дублировать код и задачи.
Создаём роль common:
[simterm]
$ mkdir -p roles/common/tasks
[/simterm]
В roles/common/tasks/main.yml
создаём таску на получение AvailabilityZones и создание переменной cluster_azs
, в которую сохраняем результат:
- name: "Getting AvailabilityZones list" command: "aws ec2 describe-availability-zones --region {{ region }} --query 'AvailabilityZones[*].ZoneName' --output text" register: cluster_azs
cluster_azs
вернём нам структуру типа такого:
[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]
Теперь из строки stdout
со значением “eu-west-2a\teu-west-2b\teu-west-2c” надо сформировать список, разделённый запятой.
Элементы у нас разделены табуляцией – используем split()
, что бы разделить их, а затем через pipe вызовем join()
, который сформирует новый список – но с разделителем в виде запятой:
... - set_fact: cluster_azs_names: "{{ cluster_azs.stdout.split('\t') | join(',') }}"
И в конце для дебага выведем получившуюся строку:
... - debug: msg: "cluster_azs_names: {{ cluster_azs_names }}"
Проверяем:
[simterm]
... TASK [cloudformation : debug] **** ok: [localhost] => { "msg": "cluster_azs_names: eu-west-2a,eu-west-2b,eu-west-2c" } ...
[/simterm]
Отлично.
CloudFormation Parameter
List<AWS::EC2::AvailabilityZone::Name
Добавляем новый параметр AvailabilityZones, ему указываем тип “List<AWS::EC2::AvailabilityZone::Name>
“, см. полный список тут – 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" } }, ...
Далее – обновляем создаваемые стеки, и вместо { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] }
выполняем { "Fn::Select": [ "0", { "Ref": "AvailabilityZones" } ] }
– получаем первый элемент из переданного списка:
... "AZNetworkStackA": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "eks-azs-networking.json", "Parameters": { "VPCID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] }, "AZ": { "Fn::Select": [ "0", { "Ref": "AvailabilityZones" } ] }, ...
Упаковываем (см. AWS: CloudFormation — вложенные стеки и 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]
Запускаем:
[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]
Проверяем:
Готово.