Первая часть — 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



