AWS Elastic Kubernetes Service: – автоматизация создания кластера, часть 2 — Ansible, eksctl

Автор: | 31/03/2020
 

Первая часть – AWS: Elastic Kubernetes Service – автоматизация создания кластера, часть 1 – CloudFormation.

Напомню, что общая идея заключается в следующем:

  1. Ansible использует модуль cloudformation , создаёт инфрастуктуру
  2. используя Outputs созданного стека CloudFormation – Ansible из шаблона генерирует файл настроек для eksctl
  3. Ansible вызывает eksctl, передавая ему конфиг кластера, и создаёт или обновляет кластер

Запускаться всё будет из Jenkins-джоб, используя Docker с AWS CLI, Ansible и eksctl. Возможно – это будет третья часть серии.

Все получившиеся в результате написания файлы доступны в репозитории eksctl-cf-ansible – тут ссылка на бранч с результатами, получившимися именно в конце написания этого и предыдущего постов. Возможно потом будут ещё изменения.

AWS Authentification

Лучше продумать сразу, что бы потом не переделывать всё с нуля (я переделывал).

Категорически рекомендую к прочтению Kubernetes: знакомство, часть 4 — аутентификация в AWS EKS, aws-iam-authenticator и AWS IAM.

Нам потребуется какой-то продуманный механизм аутентификации в AWS, т.к. туда будут обращаться:

  • Ansible для роли cloudformation
  • Ansible для роли eksctl
  • Ansible для вызова AWS CLI

Самая неочевидная проблема тут это то, что IAM-пользователь, который создаёт стек EKS становится его “супер-администратором” – и изменить это потом нельзя никак.

К примеру, в предыдущем посте мы создали кластер с помощью:

[simterm]

$ eksctl --profile arseniy create cluster -f eks-cluster-config.yml

[/simterm]

В CloudWatch проверяем логи аутентификатора, самые первые записи – и видим нашего корневого пользователя из группы system:masters в правами kubernetes-admin:

Что мы можем использовать для аутентифицикации в AWS?

  1. ключи доступа – AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY
  2. именованные профили доступа
  3. AWS IAM EC2 Instance профайлы (см. также AWS: IAM AssumeRole — описание, примеры)

В целом – всё будет запускаться с EC2 с Jenkins, которому мы можем подключить IAM роль с доступами.

Пока у меня вырисовывается такая схема:

  • руками создаём IAM пользователя, назовём его eks-root, ему подключаем политику EKS Admin
  • Jenkins создаёт ресурсы, используя свою IAM Instance роль, к которой подключена политика EKS Admin, и становится “супер-админом” всех создаваемых кластеров
  • в Ansible добавим вызов eksctl create iamidentitymapping (см. Managing IAM users and roles)

Проверяем aws-auth ConfigMap сейчас:

[simterm]

$ eksctl --profile arseniy --region eu-west-2 get iamidentitymapping --cluster eks-dev
ARN                                                                                             USERNAME                                GROUPS
arn:aws:iam::534***385:role/eksctl-eks-dev-nodegroup-worker-n-NodeInstanceRole-UNGXZXVBL3ZP  system:node:{{EC2PrivateDNSName}}       system:bootstrappers,system:nodes

[/simterm]

Или так:

[simterm]

$ kubectl -n kube-system get cm aws-auth -o yaml
apiVersion: v1
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::534***385:role/eksctl-eks-dev-nodegroup-worker-n-NodeInstanceRole-UNGXZXVBL3ZP
      username: system:node:{{EC2PrivateDNSName}}
  mapUsers: |
    []
kind: ConfigMap
...

[/simterm]

Попробуем добавить руками, что бы потом в автоматизации не столкнуться с неожиданностями:

  1. создаём IAM пользовалея
  2. “EKS Admin”-политику с разрешением на любые действия с EKS
  3. подключаем ему политику EKS Admin
  4. настраиваем локальный AWS CLI на профиль этого юзера
  5. настраиваем локальный kubectl на профиль этого юзера
  6. выполняем eksctl create iamidentitymapping
  7. выполняем kubectl get nodes

Выполняем – создаём пользователя:

[simterm]

$ aws --profile arseniy --region eu-west-2 iam create-user --user-name eks-root
{
    "User": {
        "Path": "/",
        "UserName": "eks-root",
        "UserId": "AID***PSA",
        "Arn": "arn:aws:iam::534***385:user/eks-root",
        "CreateDate": "2020-03-30T12:24:28Z"
    }
}

[/simterm]

Создаём ему ключи доступа:

[simterm]

$ aws --profile arseniy --region eu-west-2 iam create-access-key --user-name eks-root
{
    "AccessKey": {
        "UserName": "eks-root",
        "AccessKeyId": "AKI****45Y",
        "Status": "Active",
        "SecretAccessKey": "Qrr***xIT",
        "CreateDate": "2020-03-30T12:27:28Z"
    }
}

[/simterm]

Настраиваем локальный AWS CLI профиль:

[simterm]

$ aws configure --profile eks-root
AWS Access Key ID [None]: AKI***45Y
AWS Secret Access Key [None]: Qrr***xIT
Default region name [None]: eu-west-2
Default output format [None]: json

[/simterm]

Пробуем доступ – должно вернуть ошибку:

[simterm]

$ aws --profile eks-roo eks list-clusters

An error occurred (AccessDeniedException) when calling the ListClusters operation [...]

[/simterm]

Создаём политику – примеры тут>>> (позже её создание можно добавить в отдельный дочерний стек CloudFormation наашей роли cloudformation, аналогично будут создаваться SecurityGroups), сохраним пока в отдельном файле ../../cloudformation/files/eks-root-policy.json:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "eks:*"
            ],
            "Resource": "*"
        }
    ]
}

Добавляем её в IAM:

[simterm]

$ aws --profile arseniy --region eu-west-2 iam create-policy --policy-name eks-root-policy --policy-document file://../../cloudformation/files/eks-root-policy.json
{
    "Policy": {
        "PolicyName": "eks-root-policy",
        "PolicyId": "ANPAXY5JMBMEROIOD22GM",
        "Arn": "arn:aws:iam::534***385:policy/eks-root-policy",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2020-03-30T12:44:58Z",
        "UpdateDate": "2020-03-30T12:44:58Z"
    }
}

[/simterm]

Подключаем политику к пользователю:

[simterm]

$ aws --profile arseniy --region eu-west-2 iam attach-user-policy --user-name eks-root --policy-arn arn:aws:iam::534***385:policy/eks-root-policy

[/simterm]

Проверяем:

[simterm]

$ aws --profile arseniy --region eu-west-2 iam list-attached-user-policies --user-name eks-root --output text
ATTACHEDPOLICIES        arn:aws:iam::534***385:policy/eks-root-policy        eks-root-policy

[/simterm]

Ещё раз проверяем доступ к кластерам EKS:

[simterm]

$ aws --profile eks-root --region eu-west-2 eks list-clusters --output text
CLUSTERS        eks-dev

[/simterm]

И в другом регионе:

[simterm]

$ aws --profile eks-root --region us-east-2 eks list-clusters --output text
CLUSTERS        eksctl-bttrm-eks-production-1
CLUSTERS        mobilebackend-dev-eks-0-cluster

[/simterm]

Дальше – лучше сделать с машины, на которой никаких доступов не настроено вообще, например – с сервера, на котором хостится RTFM 🙂

Устанвливаем AWS CLI:

[simterm]

root@rtfm-do-production:/home/setevoy# pip install awscli

[/simterm]

Настраиваем дефолтный профиль:

[simterm]

root@rtfm-do-production:/home/setevoy# aws configure

[/simterm]

Проверяем доступ к AWS EKS вообще – в IAM-политике: права на eks:ListClusters есть – должно сработать:

[simterm]

root@rtfm-do-production:/home/setevoy# aws eks list-clusters --output text
CLUSTERS        eks-dev

[/simterm]

Устанавливаем eksctl:

[simterm]

root@rtfm-do-production:/home/setevoy# curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
root@rtfm-do-production:/home/setevoy# mv /tmp/eksctl /usr/local/bin

[/simterm]

Повторяем eks:ListClusters – тоже должна сработать, т.к. eksctl использует профили AWS CLI:

[simterm]

root@rtfm-do-production:/home/setevoy# eksctl get cluster
NAME    REGION
eks-dev eu-west-2

[/simterm]

Устанавливаем kubectl:

[simterm]

root@rtfm-do-production:/home/setevoy# curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl 
root@rtfm-do-production:/home/setevoy# chmod +x ./kubectl
root@rtfm-do-production:/home/setevoy# mv ./kubectl /usr/local/bin/kubectl

[/simterm]

Устанавливаем aws-iam-authenticator, что бы kubectl мог выполнить аутентифицию в AWS:

[simterm]

root@rtfm-do-production:/home/setevoy# curl -o aws-iam-authenticator https://amazon-eks.s3.us-west-2.amazonaws.com/1.15.10/2020-02-22/bin/linux/amd64/aws-iam-authenticator
root@rtfm-do-production:/home/setevoy# chmod +x ./aws-iam-authenticator
root@rtfm-do-production:/home/setevoy# mv aws-iam-authenticator /usr/local/bin/

Настраиваем kubectl:

[simterm]

root@rtfm-do-production:/home/setevoy# aws eks update-kubeconfig --name eks-dev
Updated context arn:aws:eks:eu-west-2:534***385:cluster/eks-dev in /root/.kube/config

[/simterm]

Пробуем выполнить команду на кластере – должны получить отказ авторизации:

[simterm]

root@rtfm-do-production:/home/setevoy# kubectl get nodes
error: You must be logged in to the server (Unauthorized)
root@rtfm-do-production:/home/setevoy# kubectl get pod
error: You must be logged in to the server (Unauthorized)

[/simterm]

Слава богу – хоть что-то не работает, как запланировано 🙂

Возвращаемся к рабочей машине – и подключаем юзера eks-root в aws-auth ConfigMap:

[simterm]

$ eksctl --profile arseniy --region eu-west-2 create iamidentitymapping --cluster eks-dev --arn arn:aws:iam::534***385:user/eks-root --group system:masters --username eks-root
[ℹ]  eksctl version 0.16.0
[ℹ]  using region eu-west-2
[ℹ]  adding identity "arn:aws:iam::534***385:user/eks-root" to auth ConfigMap

[/simterm]

Ещё раз проверяем:

[simterm]

$ eksctl --profile arseniy --region eu-west-2 get iamidentitymapping --cluster eks-dev
ARN                                                                                             USERNAME                                GROUPS
arn:aws:iam::534***385:role/eksctl-eks-dev-nodegroup-worker-n-NodeInstanceRole-UNGXZXVBL3ZP  system:node:{{EC2PrivateDNSName}}       system:bootstrappers,system:nodes
arn:aws:iam::534***385:user/eks-root                                                         eks-root                                system:masters

[/simterm]

Возвращаемся к тестовой машине – проверяем доступ оттуда:

[simterm]

root@rtfm-do-production:/home/setevoy# kubectl get node
NAME                                        STATUS   ROLES    AGE    VERSION
ip-10-0-40-30.eu-west-2.compute.internal    Ready    <none>   133m   v1.15.10-eks-bac369
ip-10-0-63-187.eu-west-2.compute.internal   Ready    <none>   133m   v1.15.10-eks-bac369

[/simterm]

И “копнём поглубже” – посмотрим системный ConfigMap, например – тот же aws-auth:

[simterm]

root@rtfm-do-production:/home/setevoy# kubectl -n kube-system get cm aws-auth -o yaml
apiVersion: v1
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::534***385:role/eksctl-eks-dev-nodegroup-worker-n-NodeInstanceRole-UNGXZXVBL3ZP
      username: system:node:{{EC2PrivateDNSName}}
  mapUsers: |
    - groups:
      - system:masters
      userarn: arn:aws:iam::534***385:user/eks-root
      username: eks-root
kind: ConfigMap
...

[/simterm]

mapUsers с нашим пользователем добавлен – замечательно.

Далее – можем использовать aws-auth для добавления новых пользователей – или добавим в Ansible задачу, которая будет выполнять eksctl create iamidentitymapping, либо – будем генерировать свой файл с ConfigMap, и загружать его в EKS.

Имеет смысл перенести дальнейшую разработку уже на Jenkins-хост – благо vim нам доступен везде.

Ansible CloudFormation

CloudFormation Parameters и Ansible переменные

Перед тем, как начинать – подумаем, какие параметры используются в CloudFormation стеке и какие потребуются в ролях Ansible.

Первым, что в голову приходит – у нас будут Dev, Stage, Production кластера + один динамический для QA (возможность из Jenkins создать новый кластер для тестирования чего-либо), значит – параметры должны быть гибкими – мочь создать в любом регионе с любыми сетями для VPC.

Следовательно:

  • общие параметры для всех:
  • отдельные параметры для каждого из окружений:
    • ENV(dev, stage, prod)
    • VPC сеть (10.0.0.0/16, 10.1.0.0/16, etc)
    • SSH ключ для доступа на Kubernetes Worker Nodes
    • типы AWS ЕС2 инстансов для Kubernetes Worker Nodes (t3.nano, c5.9xlarge, etc)

В особенности стоит обратить внимание на имена стеков и кластеров – иначе потом будет хаос и ад перфекциониста.

У нас будут четыре имени, которые используются “глобально” и будут отображаться в AWS Console:

  1. имя окружения – dev, stage, prod
  2. имя CloudFromation стека, который создаём мы (рутовый и его дочерние стеки)
  3. имя CloudFromation стека, который создаётся eksctl при создании кластера и стека для WorkerNodes
  4. имя самого EKS кластера

eksctl, само собой, немного занимается самодеятельностью, и добавляет к именам свои префиксы-постфиксы.

Например, при создании стека для EKS к имени CloudFormation стека будет добавлен префикс eksctl- и постфикс -cluster, а для WokerNodes – префикс eksctl- и постфикс -nodegroup + имя WorkerNodes группы, как оно позже будет указано в файле настроек кластера.

При этом сам кластер будет создан с тем именем, которое мы укажем в том же eks-cluster-config.yml.

Собираем всё вместе – используем следующие имена переменных и их значения:

  • env:
    • формат: строка “dev
    • описание: используется для формирования значений для остальных переменных
    • результат – dev
  • eks_cluster_name:
    • формат: bttrm-eks-${env}
    • описание: задаст metadata: name: файла настроек eks-cluster-config.yml, участвствует в формировании значений других переменных и тегов CloudFormation
    • результат – bttrm-eks-dev
  • cf_stack_name:
    • формат: eksctl-${eks_cluster_name}-stack
    • описание:  последним словом обозначаем, что данный стек содержит только инфрастуктурные ресурсы AWS, без EKS
    • результат: eksctl-bttrm-eks-dev-stack
  • (AUTO) eks_cluster_stack_name:
    • формат: eksctl-${cluster_name}-cluster
    • описание: формируется самим eksctl как имя CloudFormation-стека, нигде не указываем, просто имеем ввиду
    • результат: eksctl-bttrm-eks-dev-cluster
  • (AUTO) eks_nodegroup_stack_name:
    • формат: eksctl-${cluster_name}-nodegroup-${worker-nodes-name}, где worker-nodes-name – значение из nodeGroups: - name: файла настроек eks-cluster-config.yml
    • описание: формируется самим eksctl как имя CloudFormation-стека, нигде не указываем, просто имеем ввиду
    • результат: eksctl-bttrm-eks-dev-cluster

По поводу разделения переменных по файлам — пока не будем ломать себе голову, используем общий group_vars/all.yml, а по ходу дела посмотрим как их лучше раскидать.

Создаём каталог:

[simterm]

$ mkdir group_vars

[/simterm]

В нём файл all.yml, и приводим его к виду:

###################
# ANSIBLE globals #
###################

ansible_connection: local

#####################
# ENV-specific vars #
#####################

env: "dev"

#################
# ROLES globals #
#################

region: "eu-west-2"

# (THIS) eks_cluster_name: used for EKS service to set an exactly cluster's name - "{{ eks_cluster_name }}"
# (AUTO) eks_cluster_stack_name: used for CloudFormation service to format a stack's name as "eksctl-{{ eks_cluster_name }}-cluster"
# (AUTO) eks_nodegroup_stack_name: used for CloudFormation service to format a stack's name as "eksctl-{{ eks_cluster_name }}-nodegroup-{{ worker-nodes-name }}"
eks_cluster_name: "bttrm-eks-{{ env }}"

# used bythe cloudformation role to st a stack's name
cf_stack_name: "eksctl-{{ eks_cluster_name }}-stack"

##################
# ROLES specific #
##################

# cloudforation role
vpc_cidr_block: "10.0.0.0/16"

Заодно сразу сюда вносим общие переменные – region будет использоваться ролями и cloudformation, и eksctl, а vpc_cidr_block – только в cloudformation.

CloudFormation роль

После окончания написания шаблонов из предыдущего поста у нас получилась такая структура каталогов и файлов:

[simterm]

$ tree 
.
└── roles
    ├── cloudformation
    │   ├── files
    │   │   ├── eks-azs-networking.json
    │   │   ├── eks-region-networking.json
    │   │   └── eks-root.json
    │   ├── tasks
    │   └── templates
    └── eksctl
        ├── tasks
        └── templates
            └── eks-cluster-config.yml

[/simterm]

В корне репозитория создаём файл eks-cluster.yml – это будет наш основной Ansible-плейбук.

В нём добавляем вызов роли cloudformation:

- hosts:
  - all
  become:
    true
  roles:
    - role: cloudformation
      tags: infra

Сразу добавляем tags, что бы иметь возможность запускать роли независимо – пригодится, когда будем писать вторую роль, для eksctl.

Создаём файл roles/cloudformation/tasks/main.yml – добавляем вызов модуля cloudformation, задаём необходимые переменные для доступа и через template_parameters модуля передаём ему значение для параметра VPCCIDRBlock из переменной vpc_cidr_block:

- name: "Create EKS {{ cf_stack_name | upper }} CloudFormation stack"
  cloudformation:
    region: "{{ region }}"
    stack_name: "{{ cf_stack_name }}"
    state: "present"
    disable_rollback: true
    template: "/tmp/packed-eks-stacks.json"
    template_parameters:
      VPCCIDRBlock: "{{ vpc_cidr_block }}"
      EKSClusterName: {{ eks_cluster_name }},
    tags:
      Stack: "{{ cf_stack_name }}"
      Env: "{{ env }}"
      EKS-cluster: "{{ eks_cluster_name }}"

С полгода не трогал Ansible — всё забыл, на самом деле(

Погуглим “ansible inventory” – почитаем Best Practices

В корне репозитория создаём ansible.cfg с различными своими дефолтными настройками:

[defaults]
gather_facts = no
inventory = hosts.yml

Там же создаём inventory-файл — hosts.yml:

all:
  hosts:
    "localhost"

Используем только localhost, т.к. по сути Ansible никуда по SSH ходить и не будет — все задачи выполняются локально.

Проверяем синтаксис:

[simterm]

admin@jenkins-production:~/devops-kubernetes$ ansible-playbook eks-cluster.yml --syntax-check

playbook: eks-cluster.yml

[/simterm]

Вроде ОК? Генерируем файл шаблона (в Jenkins нужен будет отдельный pipeline stage для этого):

[simterm]

admin@jenkins-production:~/devops-kubernetes$ cd roles/cloudformation/files/
admin@jenkins-production:~/devops-kubernetes/roles/cloudformation/files$ 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

Successfully packaged artifacts and wrote output template to file /tmp/packed-eks-stacks.json.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /tmp/packed-eks-stacks.json --stack-name <YOUR STACK NAME>

[/simterm]

И запускаем создание стека:

[simterm]

admin@jenkins-production:~/devops-kubernetes$ ansible-playbook eks-cluster.yml 
...

TASK [cloudformation : Setting the Stack name] ****
ok: [localhost]

TASK [cloudformation : Create EKS EKSCTL-BTTRM-EKS-DEV-STACK CloudFormation stack] ****
changed: [localhost]

PLAY RECAP ****
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[/simterm]

Проверяем:

Имя стека eksctl-bttrm-eks-dev-stack – всё хорошо.

Теперь можно приступать к вызову самого eksctl для создания или обновления кластера.

Ansible eksctl

Файл шаблона у нас уже есть – переименуем его в .j2:

[simterm]

admin@jenkins-production:~/devops-kubernetes$ mv roles/eksctl/templates/eks-cluster-config.yml roles/eksctl/templates/eks-cluster-config.yml.j2

[/simterm]

Из CloudFormation стека, который создаётся в первой роли, нам надо в этот шаблон передать пачку значений – VPC ID, ID подсетей и т.д, что бы потом сгенерировать файл настроек для eksctl.

Используем Ansible модуль cloudformation_info, что бы получить информацию о только что созданном стеке.

Добавим, сохраним всё в переменную stack_info, и потом глянем её содержимое.

Создаём файл roles/eksctl/tasks/main.yml:

- cloudformation_info:
    region: "{{ region }}"
    stack_name: "cf_stack_name"
  register: stack_info

- debug:
    msg: "{{ stack_info }}"

Добавляем вызов роли eksctl в плейбук eks-cluster.yml, добавляем tags, что бы иметь возможность запускать роли независимо:

- hosts:
  - all
  become:
    true
  roles:
    - role: cloudformation
      tags: infra
    - role: eksctl
      tags: eks

Запускаем, передавая --tags eks, что бы вызвать только роль eksctl:

[simterm]

admin@jenkins-production:~/devops-kubernetes$ ansible-playbook --tags eks eks-cluster.yml
...

TASK [eksctl : cloudformation_info] ****
ok: [localhost]

TASK [eksctl : debug] ****
ok: [localhost] => {
    "msg": {
        "changed": false, 
        "cloudformation": {
            "eksctl-bttrm-eks-dev-stack": {
                "stack_description": {
                    ...
                    "outputs": [
                        {
                            "description": "EKS VPC ID", 
                            "output_key": "VPCID", 
                            "output_value": "vpc-042082cd2d011f44d"
                        },
                    ...

 

[/simterm]

Отлично.

Теперь — вырежем из всего этого только блок { "outputs" }.

Обновляем таску – из stack_info в msg выведем только stack_outputs:

- cloudformation_info:
    region: "{{ region }}"
    stack_name: "{{ cf_stack_name }}"
  register: stack_info

- debug:
    msg: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs }}"

Запускаем:

[simterm]

admin@jenkins-production:~/devops-kubernetes$ ansible-playbook --tags eks eks-cluster.yml
...

TASK [eksctl : debug] ****
ok: [localhost] => {
    "msg": {
        "APrivateSubnetID": "subnet-0471e7c28a3770828", 
        "APublicSubnetID": "subnet-07a0259b33ddbcb4c", 
        "AStackAZ": "eu-west-2a", 
        "BPrivateSubnetID": "subnet-0fa6eece43b2b6644", 
        "BPublicSubnetID": "subnet-072c107cef77fe859", 
        "BStackAZ": "eu-west-2b", 
        "VPCID": "vpc-042082cd2d011f44d"
    }
}
...

[/simterm]

Атлична.

А теперь попробуем сделать их переменными, и использовать шаблонизатор Ansible, что бы сгенерировать файл настроек eksctl.

Обновляем roles/eksctl/tasks/main.yml, создаём переменную vpc_id, и выведем её через debug, что бы проверить значение:

- cloudformation_info:
    region: "{{ region }}"
    stack_name: "{{ cf_stack_name }}"
  register: stack_info

- debug:
    msg: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs }}"

- set_fact:
    vpc_id: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.VPCID }}"

- debug:
    msg: "{{ vpc_id }}"

- name: "Check template's content"
  debug:
    msg: "{{ lookup('template', './eks-cluster-config.yml.j2') }}"

В «Check the template content» — с помощью прямого вызова модуля template проверим, что у нас будет получаться из файла шаблона.

Редактируем получившийся roles/eksctl/templates/eks-cluster-config.yml.j2 — вписываем {{ vpc_id }}, остальное пока не трогаем:

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: eks-dev
  region: eu-west-2
  version: "1.15"
nodeGroups:
  - name: worker-nodes
    instanceType: t3.medium
    desiredCapacity: 2
    privateNetworking: true
vpc:
  id: "{{ vpc_id }}"
  subnets:
    public:
      eu-west-2a:
...

Проверяем:

[simterm]

admin@jenkins-production:~/devops-kubernetes$ ansible-playbook --tags eks eks-cluster.yml

...
TASK [eksctl : debug] ****
ok: [localhost] => {
    "msg": {
        "APrivateSubnetID": "subnet-0471e7c28a3770828", 
        "APublicSubnetID": "subnet-07a0259b33ddbcb4c", 
        "AStackAZ": "eu-west-2a", 
        "BPrivateSubnetID": "subnet-0fa6eece43b2b6644", 
        "BPublicSubnetID": "subnet-072c107cef77fe859", 
        "BStackAZ": "eu-west-2b", 
        "VPCID": "vpc-042082cd2d011f44d"
    }
}

TASK [eksctl : set_fact] ****
ok: [localhost]

TASK [eksctl : debug] ****
ok: [localhost] => {
    "msg": "vpc-042082cd2d011f44d"
}

TASK [eksctl : Check the template's content] ****
ok: [localhost] => {
    "msg": "apiVersion: eksctl.io/v1alpha5\nkind: ClusterConfig\nmetadata:\n  name: eks-dev\n  region: eu-west-2\n  version: \"1.15\"\nnodeGroups:\n  - name: worker-nodes\n    instanceType: t3.medium\n    desiredCapacity: 2\n    privateNetworking: true\nvpc:\n  id: \"vpc-042082cd2d011f44d\"\n [...]

[/simterm]

vpc:\n id: \”vpc-vpc-042082cd2d011f44d\” – отлично, наша VPC в шаблоне появилась.

Добавляем остальные переменные вместо харкода, приводим шаблон к такому виду:

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: "{{ eks_cluster_name }}"
  region: "{{ region }}"
  version: "{{ k8s_version }}"
nodeGroups:
  - name: "{{ k8s_worker_nodes_group_name }}"
    instanceType: "{{ k8s_worker_nodes_instance_type }}"
    desiredCapacity: {{ k8s_worker_nodes_capacity }}
    privateNetworking: true
vpc:
  id: "{{ vpc_id }}"
  subnets:
    public:
      {{ a_stack_az }}:
        id: "{{ a_stack_pub_subnet }}"
      {{ b_stack_az }}:
        id: "{{ b_stack_pub_subnet }}"
    private:
      {{ a_stack_az }}:
        id: "{{ a_stack_priv_subnet }}"
      {{ b_stack_az }}:
        id: "{{ b_stack_priv_subnet }}"
  nat:
    gateway: Disable
cloudWatch:
  clusterLogging:
    enableTypes: ["*"]

Теперь надо добавить в Ansbile пачку переменных, которые мы используем в этом шаблоне:

  • eks_cluster_name – уже добавили
  • vpc_id – уже добавили
  • для стека в AvailablityZone-А добавить:
    • a_stack_pub_subnet
    • a_stack_priv_subnet
  • для стека в AvailablityZone-В добавить:
    • a_stack_pub_subnet
    • a_stack_priv_subnet
  • region — уже есть, будет переопределяться из параметров Jenkins
  • kubernetes_version
  • kubernetes_worker_nodes_group_name
  • kubernetes_worker_nodes_instance_type
  • kubernetes_worker_nodes_capacity

Добавляем set_facts в файл roles/eksctl/tasks/main.yml, приводим его к виду:

- cloudformation_info:
    region: "{{ region }}"
    stack_name: "{{ cf_stack_name }}"
  register: stack_info

- debug:
    msg: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs }}"

- set_fact:
    vpc_id: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.VPCID }}"
    a_stack_az: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.AStackAZ }}"
    a_stack_pub_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.APublicSubnetID }}"
    a_stack_priv_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.APrivateSubnetID }}"
    b_stack_az: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BStackAZ }}"
    b_stack_pub_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BPublicSubnetID }}"
    b_stack_priv_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BPrivateSubnetID }}"

- name: "Check the template's content"
  debug:
    msg: "{{ lookup('template', './eks-cluster-config.yml.j2') }}"

В файл group_vars/all.yml добавляем переменные для роли eksctl:

...
##################
# ROLES specific #
##################

# cloudforation role
vpc_cidr_block: "10.0.0.0/16"

# eksctl role
k8s_version: 1.15
k8s_worker_nodes_group_name: "worker-nodes"
k8s_worker_nodes_instance_type: "t3.medium"
k8s_worker_nodes_capacity: 2

Попробуем?

Пока просто тем же образом — через печать содержимого шаблона:

[simterm]

...
TASK [eksctl : Check the template's content] ****
ok: [localhost] => {
    "msg": "apiVersion: eksctl.io/v1alpha5\nkind: ClusterConfig\nmetadata:\n  name: \"bttrm-eks-dev\"\n  region: \"eu-west-2\"\n  version: \"1.15\"\nnodeGroups:\n  - name: \"worker-nodes\"\n    instanceType: \"t3.medium\"\n    desiredCapacity: 2\n    privateNetworking: true\nvpc:\n  id: \"vpc-042082cd2d011f44d\"\n  subnets:\n    public:\n      eu-west-2a:\n        id: \"subnet-07a0259b33ddbcb4c\"\n      eu-west-2b:\n        id: \"subnet-072c107cef77fe859\"\n    private:\n      eu-west-2a:\n        id: \"subnet-0471e7c28a3770828\"\n      eu-west-2b:\n        id: \"subnet-0fa6eece43b2b6644\"\n  nat:\n    gateway: Disable\ncloudWatch:\n  clusterLogging:\n    enableTypes: [\"*\"]\n"
}
...

[/simterm]

Работает.

Пришло время запускать сам eksctl.

Создание eks-cluster-config.yml

Перед тем, как вызывать eksctl — надо сгенерировать файл конфига кластера из шаблона.

В конце roles/eksctl/tasks/main.yml добавляем вызов template, и используя шаблон role/eksctl/templates/eks-cluster-config.yml.j2 генерируем файл eks-cluster-config.yml в /tmp:

...
- name: "Generate eks-cluster-config.yml"
  template:
    src: "eks-cluster-config.yml.j2"
    dest: /tmp/eks-cluster-config.yml

eksctl create vs update

Кстати, вопрос — а если кластер уже есть? Джоба сфейлится, т.к. мы тут явно передаём create.

Можно добавить проверку существует ли уже кластер, а затем создать переменную, которая будет хранить значение create или update, по аналогии с bash-скриптом из поста BASH: скрипт создания AWS CloudFormation стека.

Значит, надо:

  1. получить список уже имеющихся кластеров EKS в регионе
  2. найти имя создаваемого кластера в списке существующих кластеров
  3. если не найдено — то задаём create
  4. если найдено — задаём update

Пробуем так:

# populate a clusters_exist list with names of clusters devided by TAB "\t"
- name: "Getting existing clusters list"
  shell: "aws --region {{ region }} eks list-clusters --query '[clusters'] --output text"
  register: clusters_exist

- debug:
    msg: "{{ clusters_exist.stdout }}"

# create a list from the clusters_exist
- set_fact:
    found_clusters_list: "{{ clusters_exist.stdout.split('\t') }}"

- debug:
    msg: "{{ found_clusters_list }}"

# check if a cluster's name is found in the existing clusters  list
- fail:
    msg: "{{ eks_cluster_name }} already exist in the {{ region }}"
  when: eks_cluster_name not in found_clusters_list

- meta: end_play
...

Эти таски на время проверки добавляем в начало файла roles/eksctl/tasks/main.yml, а что бы не дёргать следующие задачи — на время тестирования завершаем выполнение задач с помощью - meta: end_play.

В условии fail сейчас для проверки и наглядности сделаем “not in“, т.к. нашего кластера ещё нет (мы ведь переопределили переменные – и у нас теперь новый стек, и будет новый кластер):

[simterm]

TASK [eksctl : debug] *****
ok: [localhost] => {
“msg”: “eks-dev”
}

TASK [eksctl : set_fact] ****
ok: [localhost]

TASK [eksctl : debug] ****
ok: [localhost] => {
“msg”: [
“eks-dev”
]
}

TASK [eksctl : fail] ****
fatal: [localhost]: FAILED! => {“changed”: false, “msg”: “bttrm-eks-dev already exist in the eu-west-2”}

[/simterm]

Отлично.

Теперь добавим создание переменной eksctl_action со значением «create» или «update«, в зависимости от того — найдено ли имя кластера в уже существующих кластерах региона.

# populate a clusters_exist list with names of clusters devided by TAB "\t"
- name: "Getting existing clusters list"
  shell: "aws --region {{ region }} eks list-clusters --query '[clusters'] --output text"
  register: clusters_exist

- debug:
    msg: "{{ clusters_exist.stdout }}"

# create a list from the clusters_exist
- set_fact:
    found_clusters_list: "{{ clusters_exist.stdout.split('\t') }}"

- debug:
    msg: "{{ found_clusters_list }}"

- set_fact:
    eksctl_action: "{{ 'create' if (eks_cluster_name not in found_clusters_list) else 'update' }}"
   
- debug:
    var: eksctl_action

# check if a cluster's name is found in the existing clusters  list
- fail:
    msg: "{{ eks_cluster_name }} already exist in the {{ region }}"
  when: eks_cluster_name not in found_clusters_list

...

Проверяем:

[simterm]

...
TASK [eksctl : debug] ****
ok: [localhost] => {
    "eksctl_action": "create"
}
...

[/simterm]

А теперь — надо прикрутить это к задаче с вызовом eksctl — добавляем туда {{ eksctl_action }}.

Приводим файл к виду:

- cloudformation_info:
    region: "{{ region }}"
    stack_name: "{{ cf_stack_name }}"
  register: stack_info

- set_fact:
    vpc_id: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.VPCID }}"
    a_stack_az: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.AStackAZ }}"
    a_stack_pub_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.APublicSubnetID }}"
    a_stack_priv_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.APrivateSubnetID }}"
    b_stack_az: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BStackAZ }}"
    b_stack_pub_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BPublicSubnetID }}"
    b_stack_priv_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BPrivateSubnetID }}"
 
- name: "Generate eks-cluster-config.yml"
  template:
    src: "eks-cluster-config.yml.j2"
    dest: /tmp/eks-cluster-config.yml

# populate a clusters_exist list with names of clusters devided by TAB "\t"
- name: "Getting existing clusters list"
  command: "aws --region {{ region }} eks list-clusters --query '[clusters'] --output text"
  register: clusters_exist

# create a list from the clusters_exist
- set_fact:
    found_clusters_list: "{{ clusters_exist.stdout.split('\t') }}"

- name: "Setting eksctl action to either Create or Update"
  set_fact:
    eksctl_action: "{{ 'create' if (eks_cluster_name not in found_clusters_list) else 'update' }}"

- name: "Running eksctl eksctl_action {{ eksctl_action | upper }} cluster with name {{ eks_cluster_name | upper }}"
  command: "eksctl {{ eksctl_action }} cluster -f /tmp/eks-cluster-config.yml"

Момент истины 🙂

Запускаем:

[simterm]

admin@jenkins-production:~/devops-kubernetes$ ansible-playbook --tags eks eks-cluster.yml
...

TASK [eksctl : cloudformation_info] ****
ok: [localhost]

TASK [eksctl : set_fact] ****
ok: [localhost]

TASK [eksctl : Generate eks-cluster-config.yml] ****
ok: [localhost]

TASK [eksctl : Getting existing clusters list] ****
changed: [localhost]

TASK [eksctl : set_fact] ****
ok: [localhost]

TASK [eksctl : Setting eksctl action to either Create or Update] ****
ok: [localhost]

TASK [eksctl : Running eksctl eksctl_action CREATE cluster with name BTTRM-EKS-DEV]

[/simterm]

Стек и кластер создаются, имя стека – eksctl-bttrm-dev-cluser – как мы хотели:

Ура.

Осталась ещё одна проверка.

TEST: CloudFormation && EKS Custom parameters

Последнее, что надо проверить – это “гибкость” всего, что написали.

Например – QA захотят развернуть свой кластер с именем qa-test и VPC 10.1.0.0/16.

В group_vars/all.yml меняем:

  • env: “dev” > “qa-test”
  • region: “eu-west-2” > “eu-west-3
  • vpc_cidr_block: “10.0.0.0/16” > “10.1.0.0/16
...
env: "qa-test"
...
region: "eu-west-3"
...
vpc_cidr_block: "10.1.0.0/16"

Упаковываем шаблон в корзину в этом регионе, что бы перегенерировать шаблон packed-eks-stack.json:

[simterm]

admin@jenkins-production:~/devops-kubernetes$ cd roles/cloudformation/files/
admin@jenkins-production:~/devops-kubernetes/roles/cloudformation/files$ aws --region eu-west-3 cloudformation package --template-file eks-root.json --output-template packed-eks-stacks.json --s3-bucket eks-cloudformation-eu-west-3 --use-json

[/simterm]

Запускаем Ansible – без --tags, так как нам надо создать и CloudFormation стеки, и EKS кластер:

[simterm]

admin@jenkins-production:~/devops-kubernetes$ ansible-playbook eks-cluster.yml
...
TASK [cloudformation : Create EKS EKSCTL-BTTRM-EKS-QA-TEST-STACK CloudFormation stack] ****
changed: [localhost]

TASK [eksctl : cloudformation_info] ****
ok: [localhost]

TASK [eksctl : set_fact] ****
ok: [localhost]

TASK [eksctl : Generate eks-cluster-config.yml] ****
changed: [localhost]

TASK [eksctl : Getting existing clusters list] ****
changed: [localhost]

TASK [eksctl : set_fact] ****
ok: [localhost]

TASK [eksctl : Setting eksctl action to either Create or Update] ****
ok: [localhost]

TASK [eksctl : Running eksctl eksctl_action CREATE cluster with name BTTRM-EKS-QA-TEST] ****

...

[/simterm]

Ждём, проверяем:

Бимба!

В целом – на этом всё.

Осталось создать Jenkins-джобу – это уже завтра, возможно – будет третьим постом, если будет время писать.

Блин. ConfigMap жеж)

P.S. aws-auth ConfigMap

Как не старайся – что-то всё-равно забудешь.

Последний штрих – добавить подключение юзера eks-root, которого мы создали в самом начале, к создаваемому кластеру.

Делается одной командой и максимум двумя переменными.

В group_vars/all.yml добавим ARN юзера:

...
eks_root_user_name: "eks-root"
eks_root_user_arn: "arn:aws:iam::534***385:user/eks-root"

И в roles/eksctl/tasks/main.yml – его добавление к кластеру:

...
- name: "Update aws-auth ConfigMap with the EKS root user {{ eks_root_user_name | upper }}"
  command: "eksctl create iamidentitymapping --cluster {{ eks_cluster_name }} --arn {{ eks_root_user_arn }} --group system:masters --username {{  eks_root_user_name }}"

Запускаем:

[simterm]

...
TASK [eksctl : Running eksctl eksctl_action UPDATE cluster with name BTTRM-EKS-QA-TEST] ****
changed: [localhost]

TASK [eksctl : Update aws-auth ConfigMap with the EKS root user EKS-ROOT] ****
changed: [localhost]
...

[/simterm]

Возвращаемся к серверу RTFM, на котором у нас настроен “голый” доступ только для юзера eks-root, обновляем ~/.kube/config:

[simterm]

root@rtfm-do-production:/home/setevoy# aws --region eu-west-3 eks update-kubeconfig --name bttrm-eks-qa-test
Added new context arn:aws:eks:eu-west-3:534***385:cluster/bttrm-eks-qa-test to /root/.kube/config

[/simterm]

Проверяем ноды:

[simterm]

root@rtfm-do-production:/home/setevoy# kubectl get node
NAME                                        STATUS   ROLES    AGE   VERSION
ip-10-1-40-182.eu-west-3.compute.internal   Ready    <none>   94m   v1.15.10-eks-bac369
ip-10-1-52-14.eu-west-3.compute.internal    Ready    <none>   94m   v1.15.10-eks-bac369

[/simterm]

И поды:

[simterm]

root@rtfm-do-production:/home/setevoy# kubectl get pod -n kube-system
NAME                       READY   STATUS    RESTARTS   AGE
aws-node-rgpn5             1/1     Running   0          95m
aws-node-xtr6m             1/1     Running   0          95m
coredns-7ddddf5cc7-5w5wt   1/1     Running   0          102m
coredns-7ddddf5cc7-v2sgd   1/1     Running   0          102m
...

[/simterm]

Готово.

Ссылки по теме

Kubernetes

Ansible

AWS

EKS
CloudFormation