AWS: CloudFormation — использование lists в Parameters

Автор: | 04/09/2020
 

В продолжение темы AWS: Elastic Kubernetes Service — автоматизация создания кластера, часть 1 — CloudFormation — теперь надо добавить передачу в стек параметра в виде списка.

Идея в том, что бы в Ansible получать все AvailabilityZones, а потом этот список использовать для eksctl, который будет создавать WorkerNodes в разных AvailabilityZones, и для CloudFormation — что бы создавать дочерние стеки в нужных AvailabilityZone, используя этот же список.

А с Jenkins Ansible всё будет так:

  1. из параметров Jenkins-джобы передаём $REGION в виде eu-west-3
  2. джоба в Jenkins запускает Docker-контейнер с Ansible, и ему передаёт $REGION в виде переменной окружения
  3. Ansible, используя переменную {{ region }} получает список всех AvailabilityZones этого региона
  4. и передаёт список в роль cloudformation для создания двух стеков в двух AZ
  5. и в роль 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:

mkdir -p roles/common/tasks

В 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 вернём нам структуру типа такого:

...
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"
]
}
}
...

Теперь из строки 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 }}"

Проверяем:

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

Отлично.

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 параметров):

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

Запускаем:

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

Проверяем:

Готово.