В продолжение темы 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]
Проверяем:
Готово.
