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

Автор: | 09/04/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:

[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]

Проверяем:

Готово.