используя Outputs созданного стека CloudFormation — Ansible из шаблона генерирует файл настроек для eksctl
Ansible вызывает eksctl, передавая ему конфиг кластера, и создаёт или обновляет кластер
Запускаться всё будет из Jenkins-джоб, используя Docker с AWS CLI, Ansible и eksctl. Возможно — это будет третья часть серии.
Все получившиеся в результате написания файлы доступны в репозитории eksctl-cf-ansible — тут ссылка на бранч с результатами, получившимися именно в конце написания этого и предыдущего постов. Возможно потом будут ещё изменения.
Содержание
AWS Authentification
Лучше продумать сразу, что бы потом не переделывать всё с нуля (я переделывал).
Нам потребуется какой-то продуманный механизм аутентификации в AWS, т.к. туда будут обращаться:
Ansible для роли cloudformation
Ansible для роли eksctl
Ansible для вызова AWS CLI
Самая неочевидная проблема тут это то, что IAM-пользователь, который создаёт стек EKS становится его «супер-администратором» — и изменить это потом нельзя никак.
К примеру, в предыдущем посте мы создали кластер с помощью:
В CloudWatch проверяем логи аутентификатора, самые первые записи — и видим нашего корневого пользователя из группы system:masters в правами kubernetes-admin:
Что мы можем использовать для аутентифицикации в AWS?
ключи доступа — AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY
В целом — всё будет запускаться с EC2 с Jenkins, которому мы можем подключить IAM роль с доступами.
Пока у меня вырисовывается такая схема:
руками создаём IAM пользователя, назовём его eks-root, ему подключаем политику EKS Admin
Jenkins создаёт ресурсы, используя свою IAM Instance роль, к которой подключена политика EKS Admin, и становится «супер-админом» всех создаваемых кластеров
Попробуем добавить руками, что бы потом в автоматизации не столкнуться с неожиданностями:
создаём IAM пользовалея
«EKS Admin»-политику с разрешением на любые действия с EKS
подключаем ему политику EKS Admin
настраиваем локальный AWS CLI на профиль этого юзера
настраиваем локальный kubectl на профиль этого юзера
выполняем eksctl create iamidentitymapping
выполняем kubectl get nodes
Выполняем — создаём пользователя:
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"
}
}
Создаём ему ключи доступа:
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"
}
}
Настраиваем локальный AWS CLI профиль:
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
Пробуем доступ — должно вернуть ошибку:
aws --profile eks-roo eks list-clusters
An error occurred (AccessDeniedException) when calling the ListClusters operation [...]
Создаём политику — примеры тут>>> (позже её создание можно добавить в отдельный дочерний стек CloudFormation наашей роли cloudformation, аналогично будут создаваться SecurityGroups), сохраним пока в отдельном файле ../../cloudformation/files/eks-root-policy.json:
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 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, а по ходу дела посмотрим как их лучше раскидать.
Создаём каталог:
mkdir group_vars
В нём файл 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 роль
После окончания написания шаблонов из предыдущего поста у нас получилась такая структура каталогов и файлов:
tree
.
└── roles
├── cloudformation
│ ├── files
│ │ ├── eks-azs-networking.json
│ │ ├── eks-region-networking.json
│ │ └── eks-root.json
│ ├── tasks
│ └── templates
└── eksctl
├── tasks
└── templates
└── eks-cluster-config.yml
В корне репозитория создаём файл 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:
Из CloudFormation стека, который создаётся в первой роли, нам надо в этот шаблон передать пачку значений — VPC ID, ID подсетей и т.д, что бы потом сгенерировать файл настроек для eksctl.
Используем Ansible модуль cloudformation_info, что бы получить информацию о только что созданном стеке.
Добавим, сохраним всё в переменную stack_info, и потом глянем её содержимое.
Перед тем, как вызывать eksctl — надо сгенерировать файл конфига кластера из шаблона.
В конце roles/eksctl/tasks/main.yml добавляем вызов template, и используя шаблон role/eksctl/templates/eks-cluster-config.yml.j2 генерируем файл eks-cluster-config.yml в /tmp:
Кстати, вопрос — а если кластер уже есть? Джоба сфейлится, т.к. мы тут явно передаём 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«, т.к. нашего кластера ещё нет (мы ведь переопределили переменные — и у нас теперь новый стек, и будет новый кластер):
…
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»}
…
Отлично.
Теперь добавим создание переменной 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
...
Проверяем:
...
TASK [eksctl : debug] ****
ok: [localhost] => {
"eksctl_action": "create"
}
...
А теперь — надо прикрутить это к задаче с вызовом 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"