Первая часть – AWS: Elastic Kubernetes Service – автоматизация создания кластера, часть 1 – CloudFormation.
Напомню, что общая идея заключается в следующем:
- Ansible использует модуль cloudformation , создаёт инфрастуктуру
- используя Outputs созданного стека CloudFormation – Ansible из шаблона генерирует файл настроек для
eksctl
- 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?
- ключи доступа – AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY
- именованные профили доступа
- 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]
Попробуем добавить руками, что бы потом в автоматизации не столкнуться с неожиданностями:
- создаём IAM пользовалея
- “EKS Admin”-политику с разрешением на любые действия с EKS
- подключаем ему политику EKS Admin
- настраиваем локальный AWS CLI на профиль этого юзера
- настраиваем локальный
kubectl
на профиль этого юзера - выполняем
eksctl create iamidentitymapping
- выполняем
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.
Следовательно:
- общие параметры для всех:
- регион
- AWS access/secret (или IAM EC2 Instance Profile)
- отдельные параметры для каждого из окружений:
- 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:
- имя окружения – dev, stage, prod
- имя CloudFromation стека, который создаём мы (рутовый и его дочерние стеки)
- имя CloudFromation стека, который создаётся
eksctl
при создании кластера и стека для WorkerNodes - имя самого 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 стека.
Значит, надо:
- получить список уже имеющихся кластеров EKS в регионе
- найти имя создаваемого кластера в списке существующих кластеров
- если не найдено — то задаём 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 }}" # 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
- Introduction to Kubernetes Pod Networking
- Kubernetes on AWS: Tutorial and Best Practices for Deployment
- How to Manage Kubernetes With Kubectl
- Building large clusters
- Kubernetes production best practices
Ansible
AWS
EKS
- kubernetes cluster on AWS EKS
- EKS vs GKE vs AKS — Evaluating Kubernetes in the Cloud
- Modular and Scalable Amazon EKS Architecture
- Build a kubernetes cluster with eksctl
- Amazon EKS Security Group Considerations
CloudFormation
- Managing AWS Infrastructure as Code using Ansible, CloudFormation, and CodeBuild
- Nested CloudFormation Stack: a guide for developers and system administrators
- Walkthrough with Nested CloudFormation Stacks
- How do I pass CommaDelimitedList parameters to nested stacks in AWS CloudFormation?
- How do I use multiple values for individual parameters in an AWS CloudFormation template?
- Two years with CloudFormation: lessons learned
- Shrinking Bloated CloudFormation Templates With Nested Stack
- CloudFormation Best-Practices
- 7 Awesome CloudFormation Hacks
- AWS CloudFormation Best Practices – Certification
- Defining Resource Properties Conditionally Using AWS::NoValue on CloudFormation