Що треба зробити зараз – це описати створення чотирьох таких функцій, по одній на кожен компонент проекту. Функції мають бути розміщені в приватних мережах VPC, щоб мати доступ до Ingress Loki у вигляді Internal Load Balancer.
Підготовка
Використаємо “flat-layout” – всі файли Terraform будуть в корні проекту, а значення змінних для Dev та Prod передамо через окремі файли tfvars, див. Terraform Dev/Prod – Helm-like “flat” approach.
Значення key та dynamodb_table передамо під час виконання terraform init, бо для Dev і Prod вони будуть різними.
Додаємо файл variables.tf з поки що двома змінними:
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "project_name" {
description = "A project name to be used in resources"
type = string
default = "atlas-lambda"
}
variable "component" {
description = "A team using this project (backend, web, ios, data, devops)"
type = string
}
variable "environment" {
description = "Dev/Prod, will be used in AWS resources Name tag, and resources names"
type = string
}
variable "eks_version" {
description = "Kubernetes version, will be used in AWS resources names and to specify which EKS version to create/update"
type = string
}
Значенням можно передати в defaults, але мені більш подобається задавати значення явно, а не в дефолтах, тому додаємо файл envs/dev/dev.tfvars:
eks_version використовуємо, щоб створювати окремі ресурси під кожну версію EKS-кластеру, бо під час оновлення версій спокійніше буде створити новий кластер і мігрувати workloads, ніж оновлювати живий Production.
Додаємо файл versions.tf з версіями Terraform та провайдерів:
І третє, що треба буде мати – це Security-група, яка дозволить трафік від та до цих Lambd у приватних Subnets нашої VPC. Для її створення візьмемо ще один модуль Антона – terraform-aws-modules/security-group/aws.
В egress_cidr_blocks та ingress_cidr_blocks вносимо адреси приватних мереж, а в egress_rules та ingress_rules – значення з auto_groups, де вже маємо готовий набор правил.
Ще раз виконуємо terraform init, щоб додати модуль SecurityGroup, та деплоїмо:
$ terraform apply -var-file=envs/dev/dev.tfvars
...
module.security_group_lambda.aws_security_group.this_name_prefix[0]: Creating...
module.security_group_lambda.aws_security_group.this_name_prefix[0]: Creation complete after 3s [id=sg-006d09a7a0ff0beb5]
module.security_group_lambda.aws_security_group_rule.ingress_rules[0]: Creating...
module.security_group_lambda.aws_security_group_rule.egress_rules[0]: Creating...
module.security_group_lambda.aws_security_group_rule.ingress_rules[0]: Creation complete after 1s [id=sgrule-1358616028]
module.security_group_lambda.aws_security_group_rule.egress_rules[0]: Creation complete after 2s [id=sgrule-892435573]
Releasing state lock. This may take a few moments...
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Тут в циклі for_each перебираємо всі Components, для яких треба створити фукцію, в package_type вказуємо, що функція буде запускатись з Docker-образу, в environment_variables.EXTRA_LABELS задаємо лейблу component, яка буде додана до логів в Loki.
У vpc_subnet_ids знов використовуємо outputs проекту VPC, в vpc_security_group_ids вказуємо SecurityGroup, яку створили вище.
Для створення EKS – йому треба передати VPC ID, щоб потім з data "aws_subnets" отримати список сабнетів, в яких буде створено кластер Kubernetes.
Можна захаркодити значення VPC – просто задати змінну string, в якій зберігати значення, і в данному випадку це більш-менш робоче рішення, бо VPC ID навряд чи буде часто змінюватись. Проте якщо у вас досить багато значень, або вони динамічні – то є сенс використати terraform_remote_state, який зможе “сходити” в AWS S3 бакет іншого проекту, і отримати актуальні значення прямо зі стейт-файлу.
Отже, що маємо: проект з VPC модулем, який має output:
output "vpc_id" {
value = module.vpc.vpc_id
}
Він вже задеплоїний, і ми можемо отримати цей ID зі стейту за допомогою terraform output:
Тепер, як маємо готовий код для розгортання кластеру AWS Elastic Kubernetes Service (див. Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints і наступні частини), прийшов час подумати про автоматизацію, тобто – про створення пайплайнів в CI/CD, які би виконували створення нових енвів для тестування фіч, або деплоїли апдейти на Dev/Prod оточення Kubernetes.
І тут знову поговоримо про менеджмент Dev/Prod оточень з Terraform.
То чому б не спробувати аналогічний підхід з Terraform?
В будь-якому разі те, що описано в цьому пості не треба сприймати як приклад “це робиться саме так”: тут я більше експерементую і пробую можливості GitHub Actions та Terraform. А в якому вигляді воно піде в продакшен – подивимось.
Проте вийшло доволі цікаво.
Terraform Dev/Prod – Helm-like “flat” approach
Отже, конфігурація для Terraform може бути такою:
аутентифікацію та атворизацію робимо локально зі змінною AWS_PROFILE, в якому виконується AssumeRole, а в GitHub Actions – через OIDC і AssumeRole – таким чином зможемо мати різні параметри для AWS Provider для Dev та Prod оточень
весь код Terraform зберігається в корні
tfvars для Dev/Prod в окремих директоріях envs/dev та envs/prod
загальні параметри backend описуємо у файлі backend.hcl, а ключі і таблицю Дінамо передаємо через -backend-config
А флоу розробки та деплою – таким:
бранчуємось від мастер-бранчу
робимо зміни в коді, тестуємо локально або через GitHub Actions, передаючи потрібні параметри через -var (ім’я енву, якісь версії і т.д.)
як закінчили розробку і тести – пушимо в репозиторій, і робимо Pull Request
при створенні Pull Request можемо задати спеціиальну лейблу, і тоді GitHub Actions виконують деплой на feature-енв, тобто створюється тимчасова інфрастуктура в AWS
Dev-оточення, щоб перевірити, що все працює
після мержу Pull Request можемо додатково задеплоїти на Dev-оточення, щоб перевірити, що зміни між строю версією та новою працюють (такий собі staging env)
якщо треба задеплоїти на Прод – створюємо Git Release
вручну або автоматично тригерим GitHub Actions на деплой з цим релізом
…
profit!
А що там Terragrunt?
Звісно, для управління оточеннями і бекендами можна було б просто взяти Terragrunt, але я поки не впевнений в тому, яка модель управління інфрастуктурою з Terraform складеться на проекті: якщо цим будуть займатись виключно девопси – то, скоріш за все, візьмемо Terragrunt. Якщо діло зайде і девелоперам, і вони захочуть самі щось менеджити – то, можливо, для управління візьмемо якесь рішення на кштал Atlantis або Gaia.
Тому поки це все тільки починається – робимо все максимально просто і “ванільним” Terraform, а далі подивимось, що і як буде краще.
Terraform layout for multiple environments
Зараз структура файлів і каталогів виглядає так – в environments/dev весь код для Dev, в environments/prod – весь код для Prod (поки пусто):
Однак так як ми тут не використвуємо модулі, то код дуже дублюється, і менеджити його буде важко і як то кажусь, “error prone”.
Можна було б створити модулі, але тоді були б модулі в модулях, бо весь EKS-стек створюється через модулі – VPC, EKS, Subnets, IAM, etc, і ускладнювати код, виносячи код в модулі поки що не хочеться, бо знов-таки – подивимось, як підуть процеси. Поки це все на самому початку – то маємо змогу досить швидко переробити так, як потім буде краще в залежності від того, яку модель і систему управління оточеннями оберемо.
Тож що нам треба зробити:
перенести файли .tf в корень директорії terraform
оновити файл providers.tf – видалити з нього assume_role та прибрати --profile в args провайдерів Kubernetes, Helm та kubectl
створити файл backend.hcl для common-конфігурації
у файлі backend.tf лишити тільки backend "s3"
environments перейменуємо в envs
всередені будуть каталоги dev та prod, в яких будуть зберігатись terraform.tfvars для кожного енва
Тестування з terraform init && plan для різних оточень
Маємо AWS Profile в ~/.aws/config:
[profile work]
region = us-east-1
output = json
[profile tf-assume]
role_arn = arn:aws:iam::492***148:role/tf-admin
source_profile = work
Перевіряємо існуючий ключ в S3 – бо вже маємо задеплоїний Dev:
$ aws s3 ls tf-state-backend-atlas-eks/dev/
2023-09-14 15:49:15 450817 atlas-eks.tfstate
Задаємо змінну AWS_PROFILE, і пробуємо terraform init з backend-config:
$ export AWS_PROFILE=tf-admin
$ terraform init -backend-config=backend.hcl -backend-config="key=dev/atlas-eks.tfstate"
Initializing the backend...
Successfully configured the backend "s3"! Terraform will automatically
...
Terraform has been successfully initialized!
Наче ОК – модулі загрузились, існуючий стейт-файл знайшло, все гуд.
Пробуємо terraform plan – в коді нічого не мінялось, тож нічого не має змінитись і в AWS:
$ terraform plan -var-file=envs/dev/dev.tfvars
...
Terraform will perform the following actions:
# helm_release.karpenter will be updated in-place
~ resource "helm_release" "karpenter" {
id = "karpenter"
name = "karpenter"
~ repository_password = (sensitive value)
# (28 unchanged attributes hidden)
# (6 unchanged blocks hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
...
Найс! Змінить тільки пароль для AWS ECR, це ОК, бо там токен, що міняється.
Спробуємо зробити теж саме, але для Prod – копіюємо файл dev.tfvars як prod.tfvars в каталог envs/prod, міняємо в ньому данні – тільки VPC CIDR та environment:
І пробуємо новий terraform init з -reconfigure, бо робимо це все локально. В backend-config передаємо новий ключ для стейт-файлу в S3:
$ terraform init -reconfigure -backend-config=backend.hcl -backend-config="key=prod/atlas-eks.tfstate"
Initializing the backend...
Successfully configured the backend "s3"! Terraform will automatically
...
Terraform has been successfully initialized!
Перевіряємо з terraform plan:
$ terraform plan -var-file=envs/prod/prod.tfvars
...
Plan: 109 to add, 0 to change, 0 to destroy.
...
Добре! Збирається створити купу нових ресурсів, бо Prod ще не робив, тобто все, як задумано.
Для feature-енвів все теж саме, тільки в -var передаємо ще environment і в backend-config задаємо новий ключ по імені енва:
Для plan та apply використовуємо параметри з файлу dev.tfvars, бо фіча-енви в принципі більш-менш ~= dev-оточенню:
$ terraform plan -var-file=envs/dev/dev.tfvars -var="environment=feat111"
...
Якщо потрібно задати наприклад нову VPC, яка у нас object, в якому є string vpc_cidr:
...
variable "vpc_params" {
description = "AWS VPC for the EKS cluster parameters. Note, that this VPC will be environment-wide used, e.g. all Dev/Prod AWS resources must be placed here"
type = object({
vpc_cidr = string
enable_nat_gateway = bool
one_nat_gateway_per_az = bool
single_nat_gateway = bool
enable_vpn_gateway = bool
enable_flow_log = bool
})
}
...
Тож її передаємо як -var='vpc_params={vpc_cidr="10.3.0.0/16"}'. Але в такому випадку потрібно передавати всі значення об’єкту vpc_params, тобто:
$ terraform plan -var-file=envs/dev/dev.tfvars -var="environment=feat111" \
> -var='vpc_params={vpc_cidr="10.3.0.0/16",enable_nat_gateway=true,one_nat_gateway_per_az=true,single_nat_gateway=false,enable_vpn_gateway=false,enable_flow_log=false}'
Окей – з цим розібрались, можна переходити до GitHub Actions.
Авторизація і аутентифікація – OIDC та IAM
Для доступу з GitHub Actions до AWS ми використовуємо OpenID Connect – тут GitHub виступає в ролі Identity Provider (IDP), а AWS – Service Prodider (SP), тобто на GitHub ми проходимо аутентифікацю, після чого GitHub “передає” нас до AWS, кажучи, що “це дійсно Вася Пупкін”, а вже AWS виконує авторизацію – тобто, AWS перевіряє, чи може цей Вася Пупкін створювати нові ресурси.
Для авторизації в AWS ми в Terraform виконуємо IAM Role Assume, і вже від імені цієї ролі і IAM Policy, які підключені до неї, ми проходимо авторизацію.
Щоб мати можливість виконувати AsuumeRole цієї ролі використовуючи GitHub Identity Provider – треба змінити її Trusted Policy, бо зараз вона дозволяє AssumeRole тільки для юзерів з AWS акаунту:
GitHub дозволяє налаштувати декілька оточень для роботи, і в кожному мати свій набір Variables та Secrets. Крім того, для них можна налаштувати правила, згідно з якими має відбуватись деплой. наприклад – дозволити деплой на Prod тільки з master-гілки.
і створювати feature-енви динамічно під час деплою
GitHub Actions Reusable Workflow та Composite Actions
З Reusable Workflow ми можемо використати вже описаний Workflow в іншому workflow, а з Composite Actions – створити власну Action, яку потім включимо в steps наших workflow-файлів.
при створенні Workflow ви групуєте декілька Jobs в єдиний файл і використовувати тільки в Jobs, тоді як в Actions будуть тільки steps, які групуються в єдиний Action, який потім можна використовувати тільки як step
Workflow не можуть викликати інші Workflow, тоді як в Actions ви можете викликати інші Actions
І в ній створюємо файл воркфлоу – testing-terraform.yaml.
Так як код Terraform лежить в каталозі terraform, то в workflow додамо defaults.run.working-directory та умову on.push.paths, тобто трігерити білд тільки якщо зміни відбулися в каталозі terraform, див. paths.
Пушимо зміни в репозиторій, запускаємо вручну, бо змін в коді Terraform не було:
І маємо наш перший ран:
Note: аби з’явилась кнопка для ручного запуску (workflow_dispatch) – зміни в workflow-файлі мають бути змержені з дефолтним бранчем репозиторія
GitHub Actions TFLint step
Тепер давайте додамо степ з Terraform linter – TFLint.
Спочатку глянемо, як воно працює локально:
$ yay -S tflint
$ tflint
3 issue(s) found:
Warning: module "ebs_csi_irsa_role" should specify a version (terraform_module_version)
on iam.tf line 1:
1: module "ebs_csi_irsa_role" {
Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.1.1/docs/rules/terraform_module_version.md
Warning: module "karpenter" should specify a version (terraform_module_version)
on karpenter.tf line 1:
1: module "karpenter" {
Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.1.1/docs/rules/terraform_module_version.md
Слушні зауваження – забув додати версії модулів, треба пофіксити.
Тепер можна подумати про те, як нам побудувати workflow.
Які ми маємо дії в GitHub-репозиторії, і які дії з Terraform нам можуть знадобитись?
В GitHub:
Push в будь-який бранч
створення Pull Request на merge в master
закриття Pull Request на merge в master
З Terraform:
тестування: fmt, validate, lint
деплой: plan та apply
дестрой: plan та destroy
І як ми можемо це все скомбінувати в різні workflow?
при Push – виконувати тест
при створенні PR в мастер з лейблою “deploy” – деплоїти feature-енв (тест + деплой)
при закритті PR – видаляти feature-енв (дестрой)
вручну мати можливість:
задеплоїти на Dev будь-який бранч або реліз (тест + деплой)
задеплоїти на Prod реліз з мастер-бранчу (тест + деплой)
Тобто виглядає, як п’ять воркфлоу, при чому майже в усіх маємо дії, які повторюються – логін, init та test, тож їх має сенс винести в окремі “функції” – використати Reusable Workflow або Composite Actions.
AWS Login та init можемо об’єднати, і додатково створити “функцію” test. Крім того – додати “функції” “deploy” та “destroy”, і потім “модульно” їх використовувати у наших workflow:
terraform-init:
AWS Login
terraform init
test:
terraform validate
terraform fmt
tflint
deploy:
terraform plan
terraform apply
destroy:
terraform plan
terraform destroy
Тож якщо планувати з п’ятьма окремими воркфлоу з використанням GitHub Environments та “функціями”, то це будуть:
тест-он-пуш:
запускається на push event
запускається з environment Dev, де маємо змінні для Dev-оточення
викликає “функції” terraform-init та test
деплой-дев:
запускається вручну
запускається з environment Dev, де маємо змінні для Dev-оточення
викликає “функції” terraform-init, test та deploy
деплой-прод:
запускається вручну
запускається з environment Prod, де маємо змінні для Prod-оточення та protection rules
викликає “функції” terraform-init, test та deploy
деплой-фіча-енв:
запускається при створенні ПР з лейблою “деплой”
запускається з динамічним оточенням (ім’я генерується з Pull Request ID і створюється під час деплою)
викликає “функції” terraform-init, test та deploy
дестрой-фіча-енв:
запускається при закритті ПР з лейблою “деплой”
запускається з динамічним оточенням (ім’я генерується з Pull Request ID і під час деплою оточення вибирається з існуючих)
викликає “функції” terraform-init, test та destroy
Як саме робити ці “функції” – з Reusable Workflow чи Composite Actions? В прицнипі, в данному випадку мабуть різниці великої нема, бо будемо мати один рівень вкладеності, але тут є нюанс з GitHub Runners: якщо в Workflow робити задачі “terraform-init”, “test” та “deploy” в різних jobs (а Reusable Workflow мають бути тільки в jobs, більш того – ці jobs не можуть мати інших steps) – то вони скоріш за все будуть запускатись на різних Runners, і в такому випадку для виконання terraform plan && apply && destroy нам доведеться кілька раз виконувати step з AWS Login та terraform init.
Тому виглядає більш правильним запускати всі задачі в рамках однієї job, тобто для створення наших “функцій” використати Composite Actions.
Єдине, що в цьому пості я все ж не буду описувати деплой feature-енвів, бо по-перше – пост і так вже досить довгий і містить багато нвоої (принаймні для мене) інформації, по-друге – створення додаткових тестових кластерів AWS EKS буде скоріш виключенням, і буде робитись мною, а я поки що це можу зробити і без додаткової автоматизації. Втім, скоріш за все опишу створення динамічних оточень, коли буду робити GitHub Actions Workflow для деплоїв Helm-чартів.
Окей – давайте пробувати деплоїти Dev та Prod.
Підготовка деплоїв
Створення Actions secrets and variables
У нас буде декілька змінних, які будуть однакові для всіх оточень – AWS_REGION та TF_AWS_PROFILE, тож їх задамо на рівні всього репозиторію.
Переходимо в Settings – Actions secrets and variables > Variables, і додаємо Repository variables:
Створення GitHub Environments
Додаємо два оточення – Dev та Prod:
Аналогічно для Prod.
Створення test-on-push Workflow
Створення Composite Action “terraform-init”
В директорії .github додаємо каталог actions, в якому створюємо каталог terraform-init з файлом action.yaml, в якому описуємо сам Action.
Тут є нюанс з working-directory, який в Composite Action треба задавати окремо для кожного step, бо сам Action в workflow запускається в корні репозиторія, а використати defaults, як це можна зробити в workflow не можна, хоча feature request є давно.
Створення Composite Action “test”
В директорії actions створюємо ще один каталог – terraform-test з файлом action.yaml, в якому описуємо другий Action:
В job: test-terraform задаємо використання environment: 'dev' і його змінних (у нас там вона одна – ENVIRONMENT=dev, а в steps “AWS Login and Terraform init” і “Test and validate” викликаємо наші файли з Actions, які через with передаємо параметри зі значеннями із змінних.
Пушимо зміни в репозиторій, і маємо наш білд:
Можемо переходити до деплою.
Створення deploy-dev Workflow
Отже, що ми хочемо?
деплоїти на Дев з бранчу або тегу
деплоїти на Прод вручну з релізу
Давайте почнемо з Дев-деплою.
Створення Composite Action “deploy”
Створюємо ще один каталог для нового Action – actions/terraform-apply, в ньому у файлі action.yaml описуємо сам Action:
В concurrency задаємо умову “ставити в чергу, а не виконувати паралельно, якщо запущено іншу job, яка використовує такий же concurrency.group“, див. Using concurrency.
Тобто, якщо ми робимо перевірку по group: deploy-${{ vars.ENVIRONMENT }} – то якщо після запуску Dev запустити деплой на Prod – то він буде очікувати, поки не завершиться деплой на Dev:
Пушимо, мержимо в master, щоб з’вилась кнопка “Run workflow“, і перевіряємо:
І після деплою:
Створення deploy-prod Workflow та захист від випадкового запуску
Composite Action ми вже маємо, тож створювати її не треба.
В принципі, тут маємо все аналогчно до деплою на Dev – concurrency, ручний запуск, jobs, едине, що тут має бути інша умова запуску – по тегам замість бранчів і тегів.
Тобто на Дев можемо деплоїти будь-який бранч або тег, а на Прод – тільки тег.
Взагалі я тут намагаюсь уникнути автоматичного деплою, бо це все ж інфрастуктура і все ще в процессі роботи напильником, хоча якщо б у нас був GitHub Enterprise – то можна було б використати Deployment protection rules і Required reviewers в них, а в умові on для запуску воркфлоу деплою на Прод використати створення релізів:
on:
release:
types: [published]
Тобто – деплоїти на Prod тільки коли створено новий GitHub Release, і тільки після того, як, наприклад, деплой апрувнуто кимось з DevOps-тіми.
Ще один варіант забезпечення безпеки Prod-оточення – це дозволяти мерж в master тільки від Code Owners в Branch protection rules:
А Code Owners описати у файлі CODEOWNERS, і дозволити деплой на Prod тільки з мастер-гілки:
Але я хочу тут використати флоу з деплояіми тільки з релізів або тегів, щоб мати щось на кшталт версіонювання.
Ну і можна комбінувати умови, наприклад – додатково через github.actor або github.triggering_actor перевіряти хто саме запускає деплой, і дозволити тільки певним юзерам. Див. всі типи в github context.
Як додатковий захист від випадкового запуску – можна додати input, до треба явно вказати “yes”:
...
on:
workflow_dispatch:
inputs:
confirm-deploy:
type: boolean
required: true
description: "Set to confirm deploy to the PRODUCTION environment"
...
А потім перевірити в if обидві умови – github.ref_type та github.event.inputs.confirm-deploy.
І при деплої – треба по-перше вибрати тег, а не бранч, по-друге – поставити відмітку “Confirm”:
А вже по ходу діла подивимось, як все буде складатись.
Ну і поки що на цьому все.
В цілому – в GitHub Actions зробили дуже багато всього, і коли починав робити ці деплої – прям не очікував, що буде стільки цікавих можливостей.
Так що – далі буде ще.
Пару моментів на додачу, доробити потім:
Action setup-terraform приймає інпут terraform_version, але не вміє це робити з versions-файлу; поки варіант робити через міні-костиль з додатковим step, див. цей коммент
в jobs має сенс додати параметр timeout-minutes, щоб джоби при проблемах не зависали на довгий час, і не вичерпували Runners time
Отже, вибрав все ж варіант з менеджментом бекендів через окремий проект Terraform, де в змінних маємо список проектів, яким треба мати AWS S3 bucket та таблицю DynamoDB, та їхніх оточень – Dev/Prod.
Потім в циклі for_each проходимось по елементам списку проектів, і створюємо необхідні ресурси.
В такому випадку девелоперам, щоб запустити новий проект, не треба мати справу зі створенням ресурсів для бекенду state-файлів взагалі – вони або самі можуть просто додати нове значення в змінну і виконати terraform apply, чи попросити когось з DevOps-тіми, а потім просто додати значення да власного backend.tf.
Бекенд для самого проекту який менеджить всі бекенди створюється ним же – в перший раз з локальним бекендом, а після створення проекту – його стейт туди імпортується, і надалі вже використовується цей remote state.
Файл backend.tf поки не описуємо – робимо все локально.
Виконуємо terraform init, і переходимо до змінних.
Variables – список проектів і оточень
Тут нам потрібна по факту одна змінна з типом map(list(string)), в якій ми описуємо список проектів, для яких будемо створювати ресурси, в тому числі включаємо в неї сам проект, який буде створювати всі ці ресурси.
І для кожного елементу з іменем проекту в значення включаємо список з іменами оточень цього проекту:
variable "projects" {
description = "Project names with their environments to be used in S3 and DynamoDB resources"
type = map(list(string))
default = {
atlas-tf-backends-test = [
"prod"
]
atlas-eks-test = [
"dev", "prod"
]
}
}
Resources
Створення AWS S3 бакетів
Що нам треба, це для кожного проекту створити AWS S3 Bucket, включити йому Versioning, додати Encryption, і заборонити публічний доступ до об’єктів через S3 Bucket ACL.
Щодо Dev/Prod оточень: можна створювати окремі корзини на кожен Env кожного проекту, чи один бакет на проект, а вже в самому проекті використовувати різні ключі, тобто:
проект atlas-eks-test
корзина atlas-eks-test
при terraform init Dev-оточення використовуємо -backend-config="key=dev/atlas-eks.tfstate"
при terraform init для Prod-оточення використовуємо -backend-config=key=prod/atlas-eks.tfstate
Для таблиць DynamoDB створимо окремі таблиці для Dev/Prod, а всякі feature-енви можна буде деплоїти або без State Lock, бо вони тимчасові, і будуть деплоїтись з якогось одного Pull Request з GitHub Actions, або при потребі – створювати таблицю під час деплою проекту командою AWS CLI create-table.
Отже – в змінних маємо map зі списком проектів.
Для S3 використовуємо for_each, з якого отримуємо each.key, який буде містити ім’я проекту, тобто “atlas-eks-test” або “atlas-tf-backends-test“:
# create state-files S3 buket
resource "aws_s3_bucket" "state_backend" {
for_each = var.projects
bucket = "tf-state-backend-${each.key}"
# to drop a bucket, set to `true`
force_destroy = false
lifecycle {
# to drop a bucket, set to `false`
prevent_destroy = true
}
tags = {
environment = var.environment
}
}
Далі, для ресурсів aws_s3_bucket_versioning, aws_s3_bucket_server_side_encryption_configuration та aws_s3_bucket_public_access_block знов використовуємо for_each, але тепер ітерацію виконуємо по списку ресурсів aws_s3_bucket.state_backend, тобто весь код буде таким:
# create state-files S3 buket
resource "aws_s3_bucket" "state_backend" {
for_each = var.projects
bucket = "tf-state-backend-${each.key}"
# to drop a bucket, set to `true`
force_destroy = false
lifecycle {
# to drop a bucket, set to `false`
prevent_destroy = true
}
tags = {
environment = var.environment
}
}
resource "aws_kms_key" "state_backend_kms_key" {
description = "This key is used to encrypt bucket objects"
deletion_window_in_days = 10
}
# enable S3 bucket versioning
resource "aws_s3_bucket_versioning" "state_backend_versioning" {
for_each = aws_s3_bucket.state_backend
bucket = each.value.id
versioning_configuration {
status = "Enabled"
}
}
# enable S3 bucket encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "state_backend_encryption" {
for_each = aws_s3_bucket.state_backend
bucket = each.value.id
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.state_backend_kms_key.arn
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true
}
}
# block S3 bucket public access
resource "aws_s3_bucket_public_access_block" "state_backend_acl" {
for_each = aws_s3_bucket.state_backend
bucket = each.value.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
В ouputs додаємо відображення створених бакетів, використовуючи цикл for та String Templates:
output "state_backend_bucket_names" {
value = "AWS S3 State Buckets:\n%{for name in aws_s3_bucket.state_backend}- ${name.bucket}\n%{endfor}"
}
І виконуємо terraform init ще раз, щоб перенести власний стейт з локального файлу terraform.tfstate до створеного S3 бакету:
$ terraform init
Initializing the backend...
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the previous "local" backend to the
newly configured "s3" backend. No existing state was found in the newly
configured "s3" backend. Do you want to copy this state to the new "s3"
backend? Enter "yes" to copy and "no" to start with an empty state.
Enter a value: yes
...
Terraform has been successfully initialized!
Створення таблиць DynamdoDB
Якщо для S3 ми робили одну корзину на кожен проект, то для DynamoDB буде окрема таблиця на кожен Env кожного проекту (хоча можна мати і одну – тоді Terraform при створенні ключів сам задасть значення Env, див. Backends S3).
Якщо якийсь проект більш не актуальний, і треба видалити його ресурси – то це буде робитись в три етапи apply:
міняємо параметри aws_s3_bucket:
включаємо force_destroy – це потрібно, щоб видалити корзини, в яких включено Versioning і які мають об’єкти
відключаємо prevent_destroy – щоб дозволити видалення
виконуємо apply, щоб застосувати зміни
видаляємо проект з var.projects
виконуємо apply, щоб видалити корзину та пов’язані ресурси
повертаємо значення параметрів aws_s3_bucket – force_destroy та prevent_destroy
виконуємо apply, щоб застосувати зміни
Тобто:
# create state-files S3 buket
resource "aws_s3_bucket" "state_backend" {
for_each = var.projects
bucket = "tf-state-backend-${each.key}"
# to drop a bucket, set to `true`
force_destroy = true
lifecycle {
# to drop a bucket, set to `false`
prevent_destroy = false
}
tags = {
environment = var.environment
}
}
...
Потім видаляємо ім’я проекту зі значень змінної projects:
variable "projects" {
description = "Project names list with its environments to be used in S3 and DynamoDB nresources"
type = map(list(string))
default = {
atlas-tf-backends-test = [
"prod"
]
}
}
Цей аддон можна встановити з Amazon EKS Blueprints Addons, котрий далі будемо використовувати для ExternalDNS, але раз уж ставимо аддони через cluster_addons в модулі EKS, то давайте і цей зробимо таким же чином.
Для aws-ebs-csi-driver ServiceAccount нам знадобиться окрема IAM Role – створимо її за допомогою IRSA Terraform Module.
$ kk get pod
NAME READY STATUS RESTARTS AGE
pvc-pod 1/1 Running 0 106s
$ kk get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-dynamic Bound pvc-a83b3021-03d8-458f-ad84-98805ec4963d 1Gi RWO gp2 119s
$ kk get pv pvc-a83b3021-03d8-458f-ad84-98805ec4963d
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-a83b3021-03d8-458f-ad84-98805ec4963d 1Gi RWO Delete Bound default/pvc-dynamic gp2 116s
З цим все готово.
Terraform та ExternalDNS
Для ExternalDNS спробуємо Amazon EKS Blueprints Addons. На відміну від того, як ми робили EBS CSI, тут нам не потрібно буде окремо створювати IAM Role, бо модуль створить її сам.
Правда, в документації чомусь вказана передача параметрів для чарту external_dns_helm_config (UPD – поки писав цей пост, вже видалили сторінку взагалі), хоча на ділі це призводить до помилки “An argument named “external_dns_helm_config” is not expected here“.
Щоб знайти, як жеж нам передати параметри – йдемо на сторінку модулю в eks-blueprints-addons, і дивимось які інпути є для external_dns:
Далі перевіряємо файл main.tf модулю, де бачимо змінну var.external_dns, в якій можна передати всі параметри.
Дефолтні версії чартів задаються у тому ж файлі, але вони місцями застарілі, теж задамо свої.
Знаходимо останню версію для ЕxternalDNS:
$ helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
"external-dns" has been added to your repositories
$ helm search repo external-dns/external-dns --versions
NAME CHART VERSION APP VERSION DESCRIPTION
external-dns/external-dns 1.13.1 0.13.6 ExternalDNS synchronizes exposed Kubernetes Ser...
...
Додаємо змінну для версій чартів:
variable "helm_release_versions" {
description = "Helm Chart versions to be deployed into the EKS cluster"
type = map(string)
}
Так як ми використовуємо VPC Endpoint для STS, то в аннотації ServiceAccount передаємо eks.amazonaws.com/sts-regional-endpoints="true" – аналогічно тому, як робили для Karpenter.
У external_dns.values передаємо бажані параметри – policy, в domainFilters наш домен, та задаємо tolerations, щоб под запускався на наших дефолтних нодах:
...
time="2023-09-13T12:01:39Z" level=info msg="Desired change: CREATE cname-test-dns.dev.example.co TXT [Id: /hostedzone/Z09***BL9]"
time="2023-09-13T12:01:39Z" level=info msg="Desired change: CREATE test-dns.dev.example.co A [Id: /hostedzone/Z09***BL9]"
time="2023-09-13T12:01:39Z" level=info msg="Desired change: CREATE test-dns.dev.example.co TXT [Id: /hostedzone/Z09***BL9]"
time="2023-09-13T12:01:39Z" level=info msg="3 record(s) in zone dev.example.co. [Id: /hostedzone/Z09***BL9] were successfully updated
Готово.
Terraform та AWS Load Balancer Controller
Робимо аналогічно з ExternalDNS – будемо встановлювати з модуля Amazon EKS Blueprints Addons.
Спочатку нам треба протегати публічні і приватні сабнети – див. Subnet Auto Discovery.
Також перевірте, щоб на них був тег kubernetes.io/cluster/${cluster-name} = owned (має бути, якщо деплоїли з Terraform модулем EKS, як це робили в першій частині).
Додаємо теги через public_subnet_tags та private_subnet_tags:
Деплоїмо, перевіряємо Ingress – чи додався до нього Load Balancer:
$ kk get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
hello-ingress alb test-dns.dev.example.co k8s-kubesyst-helloing-***.us-east-1.elb.amazonaws.com 80 45s
Ніяк налаштувань тут не треба – тільки додати Tolerations.
Знаходимо версії:
$ helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
"secrets-store-csi-driver" has been added to your repositories
$ helm repo add secrets-store-csi-driver-provider-aws https://aws.github.io/secrets-store-csi-driver-provider-aws
"secrets-store-csi-driver-provider-aws" has been added to your repositories
$ helm search repo secrets-store-csi-driver/secrets-store-csi-driver
NAME CHART VERSION APP VERSION DESCRIPTION
secrets-store-csi-driver/secrets-store-csi-driver 1.3.4 1.3.4 A Helm chart to install the SecretsStore CSI Dr...
$ helm search repo secrets-store-csi-driver-provider-aws/secrets-store-csi-driver-provider-aws
NAME CHART VERSION APP VERSION DESCRIPTION
secrets-store-csi-driver-provider-aws/secrets-s... 0.3.4 A Helm chart for the AWS Secrets Manager and Co...
Додаємо нові значення до змінної helm_release_versions у terraform.tfvars:
Для IAM Roles, які ми потім будемо додавати до сервісів, якім потрібен доступ до AWS SecretsManager/ParameterStore треба буде підключати політику, яка дозволяє доступ до відповідних AWS API викликів.
Деплоїмо (у default неймспейс, бо в Trust policy маємо перевірку subject на "system:serviceaccount:default:ascp-test-serviceaccount"), і перевіряємо файл в поді:
$ kk exec -ti pod/ascp-test-pod -- cat /mnt/ascp-secret/eks-test-param
paramLine
Terraform та Metrics Server
Тут теж зробимо з Amazon EKS Blueprints Addons – див. metrics_server.
Теж ніяк додаткових налаштувань не треба – просто включити, і перевірити. Навіть версію можна не задавати, тільки tolerations.
$ kk get pod | grep metr
metrics-server-76c55fc4fc-b9wdb 1/1 Running 0 33s
І перевіряємо з kubectl top:
$ kk top node
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
ip-10-1-32-148.ec2.internal 53m 2% 656Mi 19%
ip-10-1-49-103.ec2.internal 56m 2% 788Mi 23%
...
Terraform та Vertical Pod Autoscaler
Щось забув, що для Horizontal Pod Autoscaler окремого контроллеру не треба, тож тут нам знадобиться тільки додати Vertical Pod Autoscaler.
Беремо знову з Amazon EKS Blueprints Addons, див. vpa.
Знаходимо версію:
$ helm repo add vpa https://charts.fairwinds.com/stable
"vpa" has been added to your repositories
$ helm search repo vpa/vpa
NAME CHART VERSION APP VERSION DESCRIPTION
vpa/vpa 2.5.1 0.14.0 A Helm chart for Kubernetes Vertical Pod Autosc...
$ kk get vpa
NAME MODE CPU MEM PROVIDED AGE
hamster-vpa 100m 104857600 True 61s
Та статус подів:
$ kk get pod
NAME READY STATUS RESTARTS AGE
hamster-8688cd95f9-hswm6 1/1 Running 0 60s
hamster-8688cd95f9-tl8bd 1/1 Terminating 0 90s
Готово.
Додавання Subscription Filter до Cloudwatch Log Group з Terraform
Майже все зробили. Залишилось дві дрібниці.
Спершу – додати форвадніг логів з EKS Cloudwatch Log Groups до Lambda-функції, в якій працює Promtail, який буде ці логи пересилати до інстансу Grafana Loki.
Наш EKS модуль створює CloudWatch Log Group /aws/eks/atlas-eks-dev-1-27-cluster/cluster зі стрімами:
І виводить ім’я цієї групи через output cloudwatch_log_group_name, який ми можемо використати у aws_cloudwatch_log_subscription_filter, щоб до цієї лог-групи додати фільтр, в якому треба передати destination_arn з ARN нашої Lambda.
Lambda-функція у нас вже є, створюється окремою автоматизацію для моніторинг-стеку. Щоб отримати її ARN – використаємо data "aws_lambda_function", в який передамо ім’я функції, а саме ім’я винесемо у змінні:
variable "promtail_lambda_logger_function_name" {
type = string
description = "Monitoring Stack's Lambda Function with Promtail to collect logs to Grafana Loki"
}
Щоб наш Subscription Filter мав змогу звератись до цієї функції – потрібно додати aws_lambda_permission, де в source_arn передаємо ARN нашої лог-групи. Тут зверніть увагу, що ARN передається як arn::name:*.
У principal треба вказати logs.AWS_REGION.amazonaws.com – AWS_REGION отримаємо з data "aws_region".
Це вже третя частина по розгортанню кластеру AWS Elastic Kubernetes Service з Terraform, в якій будемо додавати в наш кластер Karpenter. Вирішив винести окремо, бо виходить досить довгий пост. І вже в останній (сподіваюсь), четвертій частині, додамо решту – всякі контроллери.
На додачу вже існуючим у нас провайдерам AWS та Kubernetes нам здобляться ще два – Helm та Kubectl.
Helm, очевидно що для встановлення Helm-чартів – а контролери ми будемо встановлювати саме з чартів, а kubectl – для деплою ресурсів з власних Kubernetes-маніфестів.
Отже додаємо їх в наш файл providers.tf. Авторизацію робимо аналогічно тому, як робили для Kubernetes-провайдера – через AWS CLI, і в args передаємо ім’я профілю:
Виконуємо terraform init для установки провайдерів.
Варіанти установки Karpenter в AWS EKS з Terraform
Є декілька варіантів установки Karpenter з Terraform:
написати все самому – IAM-ролі, SQS для interruption-handling, оновлення aws-auth ConfigMap, встановлення Helm-чарту з Karpenter, створення Provisioner та AWSNodeTemplate
Для роботи Karpenter використвує тег karpenter.sh/discovery з SecurityGroup наших WorkerNodes та приватних VPC Subnets щоб знати які SecurityGroups додавати на Nodes, та в яких subnets ці ноди запускати.
В значені тегу задається ім’я кластеру, якому належить SecurityGroup чи Subnet.
Додамо нову локальну змінну eks_cluster_name до main.tf:
locals {
# create a name like 'atlas-eks-dev-1-27'
env_name = "${var.project_name}-${var.environment}-${replace(var.eks_version, ".", "-")}"
eks_cluster_name = "${local.env_name}-cluster"
}
І у файлі eks.tf додаємо параметр node_security_group_tags:
...
output "karpenter_irsa_arn" {
value = module.karpenter.irsa_arn
}
output "karpenter_aws_node_instance_profile_name" {
value = module.karpenter.instance_profile_name
}
output "karpenter_sqs_queue_name" {
value = module.karpenter.queue_name
}
І переходимо до Helm – додаємо чарт з самим Karpenter.
Тут у variables можна винести версію модулю:
...
variable "karpenter_chart_version" {
description = "Karpenter Helm chart version to be installed"
type = string
}
Да terraform.tfvars додаємо значення з останнім релізом чарта:
...
karpenter_chart_version = "v0.30.0"
Для доступу Helm до репозиторія oci://public.ecr.aws нам буде потрібен токен. Отримаємо його з aws_ecrpublic_authorization_token. Тут є нюанс, що він працює тільки для регіону us-east-1, бо токени AWS ECR видаються саме там. Див. aws_ecrpublic_authorization_token breaks if region != us-east-1.
Помилка “cannot unmarshal bool into Go struct field ObjectMeta.metadata.annotations of type string”
Також, так як ми використовуємо VPC Endpoint для AWS STS, то нам до ServiceAccount, який буде створюватись для Karpenter, треба додати аннотацію eks.amazonaws.com/sts-regional-endpoints=true.
Проте якщо додати аннотацію у set як:
set {
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/sts-regional-endpoints"
value = "true"
}
То Terraform видаває помилку “cannot unmarshal bool into Go struct field ObjectMeta.metadata.annotations of type string“.
Рішення нагуглилось ось у цьому коментарі до GitHub Issue – “просто додай води type=string“.
В тексті помилки говориться, що Terraform не може “unmarshal bool” (“розпаковати значення з типом bool”) у поле spec.tolerations.value, яке має тип string.
Рішення – це вказати тип значення явно, через type = "string".
До karpenter.tf додаємо aws_ecrpublic_authorization_token та ресурс helm_release, в якому встановлюємо чарт Karpenter:
...
data "aws_ecrpublic_authorization_token" "token" {}
resource "helm_release" "karpenter" {
namespace = "karpenter"
create_namespace = true
name = "karpenter"
repository = "oci://public.ecr.aws/karpenter"
repository_username = data.aws_ecrpublic_authorization_token.token.user_name
repository_password = data.aws_ecrpublic_authorization_token.token.password
chart = "karpenter"
version = var.karpenter_chart_version
set {
name = "settings.aws.clusterName"
value = local.eks_cluster_name
}
set {
name = "settings.aws.clusterEndpoint"
value = module.eks.cluster_endpoint
}
set {
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
value = module.karpenter.irsa_arn
}
set {
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/sts-regional-endpoints"
value = "true"
type = "string"
}
set {
name = "settings.aws.defaultInstanceProfile"
value = module.karpenter.instance_profile_name
}
set {
name = "settings.aws.interruptionQueueName"
value = module.karpenter.queue_name
}
}
Karpenter Provisioner та Terraform templatefile
Поки що у нас буде тільки один Karpernter Provisioner, проте згодом скоріш за все будемо додавати ще, тому давайте це відразу зробимо через шаблони.
На старому кластері маніфест нашого дефолтного Provisioner виглядає так:
Створюємо сам файл шаблону – спочатку додаємо локальний каталог configs, де будуть всі шаблони, і в ньому додаємо файл karpenter-provisioner.yaml.tmpl.
Для параметрів, в які будуть передаватись елементи з типом list додаємо jsonencode(), щоб перетворити їх на стрінги:
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: ${name}
spec:
%{ if taints != null ~}
taints:
- key: ${taints.key}
value: ${taints.value}
effect: ${taints.effect}
%{ endif ~}
%{ if labels != null ~}
labels:
%{ for k, v in labels ~}
${k}: ${v}
%{ endfor ~}
%{ endif ~}
requirements:
- key: karpenter.k8s.aws/instance-family
operator: In
values: ${jsonencode(instance-family)}
- key: karpenter.k8s.aws/instance-size
operator: In
values: ${jsonencode(instance-size)}
- key: topology.kubernetes.io/zone
operator: In
values: ${jsonencode(topology)}
providerRef:
name: default
consolidation:
enabled: true
kubeletConfiguration:
maxPods: 100
ttlSecondsUntilExpired: 2592000
Далі, в karpenter.tf додаємо ресурс kubectl_manifest з циклом for_each, в якому перебираємо всі елементи мапи karpenter_provisioner:
Вказуємо depends_on, бо якщо все це деплоїться перший раз, то Terraform повідомляє про помилку “resource [karpenter.sh/v1alpha5/Provisioner] isn’t valid for cluster, check the APIVersion and Kind fields are valid“, бо в кластері ще немає самого Karpenter.
AWSNodeTemplate у нас навряд чи буде змінюватись, тому його можемо створити просто ще одним kubectl_manifest:
Вже давно і досить часто просять розказати як пишу пости в блог. Ну і раз вже така тема, і я нарешті таки зібрався про це написати – то давайте поглянемо навіщо взагалі вести блог, і як його вести.
Навіщо вести свій IT блог?
Цей блог я починав головним чином як такий собі блокнот для себе самого – просто записувати як і що я робив, аби потім не шукати в інтернеті якісь мануали, чи записувати те, чого в інтернеті не було взагалі.
Згодом, це трансформувалося в бажання поділитись тим, який я не в біса крутий спеціаліст, бо коли ти вперше збираєш ядро FreeBSD, то здається, що ти бог 🙂 Це, звісно, та ще бугогашенька, бо вперше ядро я зібрав ще у 2007, і робив це як та “мавпа з мануалом” (привіт, Хом’як!) – читав, копіпастив, але дуже мало що розумів.
Насправді коли я вже трохи набрався досвіду, то ведення блогу дало ще один важливий бонус – він допомагає боротись з власним “синдромом самозванця”, бо навіть після (ОМГ!) 18 років в IT цей синдром нікуди не дівався, і я досі іноді думаю “Блін, а нє хєрню лі я написав?”. Проте коли твої пости вже лайкають солюшен архітектори з якогось Oracle – то це дуже допомагає відчувати себе людиною.
Власний бренд
По важливості я б це поставив на друге місце, але почати чомусь хочеться саме з цього.
Ваш блог – це ваш бренд.
Дуже часто, особливо вже за останні років 6-7, приходячи на співбесіду я чув “О! То ви адмін того самого RTFM? Круто – я там дуже багато для себе знайшов!”.
Ось це і є брендом – коли тебе впізнають, як спеціаліста.
Хтось виступає на конференціях – але я надто боюся публіки. А хтось тихенько пише собі бложег, де, звісно, теж публіка, і теж іноді ставлять “мінуси” – але це принаймні “не особисто”.
І от тут ми підходимо до другого пункту:
Власний розвиток
Я вже не представляю як можна виконувати якусь складну задачу, і не вести нотатки в чернетці блогу.
Бо, по-перше, коли ти пишеш – ти структуруєш інформацію, яку отримуєш. Тобі потрібно подати матеріал так, щоб він був зрозумілий і іншим, а для цього потрібно добре зуміти пов’язати всі “компоненти” посту у власній голові.
Це прям дуже, дуже допомагає краще зрозуміти щось нове, запам’ятати це, краще усвідомити всі ті “moving parts” нової системи.
І тут є ще один важливий момент. Колись (я навіть пам’ятаю цей пост – Apache: MPM – worker, prefork или event?, 2012 рік) я показав свій новий пост другу-сисадміну. Дуже прошарений чувак (привіт, Андрій!).
Після того, як він його прочитав, він сказав мені щось на кшталт “Ну, так, прикольно, але ось тут і ось тут відчувається, що ти не дуже шариш в темі”.
І саме відтоді я усвідомив, що, блін – треба копати. Не можна просто “на тяп-ляп” взяти, і написати. Треба писати вдумуючись, усвідомлюючи що і навіщо ти робиш.
А ця потреба призводить до того, що коли ти розбираєшся з новим матеріалом – ти вже не можеш скіпнути якусь не дуже зрозумілу частину – “А, потім розберусь, ілі взагалі фіг з нею”.
Ніт! Ти мусиш сісти, і розібратись. І саме це дуже допомогає у власному розвитку, як спеціаліста, бо потім, коли на якійсь співбесіді тебе питають по якійсь темі – то ти можеш розкрити якісь деталі цієї теми, показати, що ти дійсно розбираєшся в темі, а не просто “навкоси” прочитав документацію, скопіпастив команду, запустив сервіс, і вважаєш, що ти його знаєш.
Взагалі, уважність до деталей, уміння розібратись з тим, “а що там під капотом” дуже допомогає в роботі. Бо тільки знаючи ЯК система працює, що відбувається у неї всередині, ти можеш зрозуміти куди копати, щоб потім цю систему пофіксити.
Повертаючись трохи назад до “усвідомлюючи що і навіщо ти робиш” – така звичка у веденні блогу вже стала звичкою і в роботі: не можна просто “взяти, зробити, і забити”. Ти маєш розуміти що і як ти зробив, бо, по-перше – тобі ж цю систему потім і підтримувати, по-друге – ти несеш відповідальність за те, що ти робиш. А враховуючи те, що наша, девопсів, робота дуже багато пов’язана з інфраструктурою, з цим “фундаментом” будь-якого проекту, з усіма його даними – то відчуття відповідальності тут вкрай необхідно.
Власна документація
Часто, прям дуже часто я повертаюсь до якихось старих постів, щоб подивитись що і як я робив. Це допомагає і під час сетапу якоїсь вже знайомої системи на новому проекті, і під час спроб зрозуміти що зламалось на поточному.
Навіть більше – у твоїх колег є доступ до інформації як саме ти піднімав якусь систему, і коли я був тім-лідом – то хлопці дуже часто використовували РТФМ, щоб розібратись з чимось, що я колись сетапив.
Ведення блогу, звісно, не значить, що можна не вести “локальну документацію” десь в проектному Confluence, але в своєму блозі ти можеш набагато краще описати що і навіщо ти робив, і чому зробив саме так, а не інакше.
Як вести свій блог?
Перше, і головне, над чим я замислювався раніше, коли цей блог тільки починався, це:
А про що, власне, писати?
І зараз тут відповідь та ж сама, що була тоді: про те, що ти робиш, з чим ти стикаєшся, або про те, що не дуже розумієш – а тобі треба розібратись.
Але саме, мабуть, важке, це саме створити структуру поста – зрозуміти про що саме писати, і як саме це висловити – що до чого відноситься, про що написати в першу чергу, про що в другу. Що винести окремою частиною – а про що достатньо буде написати пару речень.
Наприклад ось, як я “морально готувався” до написання цього поста:
Структура матеріалу
Навіть зараз, коли я пишу цей матеріал – я його перечитую, і розділяю на частини (під)заголовками, щоб вони якось логічно розбивали все те, про що тут написано.
Ось ще один приклад зі старих чернеток:
Ти починаєш писати про щось одне, а потім розумієш, що треба розказати і про щось ще, а потім ще про щось… І в результаті сидиш перед мільйоном вкладок, і кашею в голові. Але тобі все одно треба зібратись, і довести це до кінця, і подати так, щоб людина, яка буде читати цей матеріал, зрозуміла що ж насправді ти тут робиш.
Або ось такий приклад:
Тут на початку статті я накидую текст того, про що саме буде йти мова – бо це потім допомагає в голові тримати “нить повествования”.
Шарь в темі!
Так – треба добре розуміти, про що ти пишеш. Але й боятися писати (бо “що ж подумають люди?!?”) – теж не треба.
Ось ще приклад, як іноді виглядає процес написання деяких постів:
Бо знову ж таки – не можна “на тяп-ляп”, а треба розібратись.
Ще, мабуть, варто сказати про довжину постів: краще уникати “полотенець”. Краще розбити пост на декілька частин, і в кожній описати окремо якусь частину теми, ніж намагатись все впихнути в один пост.
Це допоможе і при написанні – бо все ж менша “каша в голові”, і при читанні, бо знов – менше каша в голові у читача.
Мови блогу
Якщо у вас є змога – то краще писати відразу на англійській.
По-перше – це круто.
По-друге – ви не обмежуєте себе читачами тільки з України.
По-третє – це дуже класна практика англійської.
Щодо правопису і помилок – головне, щоб вас зрозуміли. Крім того, можна скористатись плагінами типу Grammarly або LanguageTool, і навіть Google Translate.
Тут вибір вкрай широкий – від готових платформ типу Medium – до власного VPS з WordPress.
Я колись вибрав саме WordPress, і саме на виділеному VPS, щоб мати змогу отримати додатковий досвід з адміністрування Linux та всяких Apache/Nginx/PHP/MySQL – і це було дійсно дуже корисним.
Ще один момент, котрий треба мати на увазі: враховуйте той момент, що ведучи свій блог на платформах типу Medium ви фактично довіряєте всю інформацію йому, це такий собі “вендер-лок”, бо потім мігрувати з одної платформи на іншу може бути дуже боляче, тим більш, якщо у вас буде пару тисяч постів.
В принципі, теж стосується і якихось self-hosted платформ типу Jekyll – якщо розробники Jekyll його закинуть, або змінять цінову політику, то ви можете залишитись у “розбитого корита”.
І в цьому плані WordPress мене більш, ніж влаштовує, бо платформа слава богу існує вже багато років, а зараз навіть пропонує оформити реєстрацію на сто років наперед (The 100-Year Plan on WordPress) – оптимісти 🙂
Висновки
Чи є сенс у ведені свого блогу? Для мене відповідь очевидна, бо це дійсно дуже допомагає і в роботі, і у власному розвитку, і у кар’єрі.
Проте треба усвідомлювати, чи на це потрібен час. Деякі пости на РТФМ пишуться тиждень, а то й більше, а потім ще день-два для перекладу на англійську.
Також треба розуміти, що зовсім не відразу блог будуть читати, і що перші місяці у вас може бути пару випадкових відвідувачів на день.
Втім ті плюси, які дає ведення блогу, однозначно варті того, щоб витрачати на це свій час.
Навіть якщо вас ніхто не буде читати – ви навчитесь висловлювати свої думки, подавати матеріал, або прокачаєте свою англійську. У вас завжди буде ваша власна документація. Ви набагато краще будете розуміти те, що робили, коли писали якийсь новий пост.
Для кластеру також використаємо модуль, знову від @Anton Babenko – Terraform EKS module. Проте й інші модулі, наприклад – terraform-aws-eks від Cookpad – я ним теж трохи користвувався, працював добре, але порівнювати не візьмусь.
І AWS CLI Profile tf-admin як раз теж виконує IAM Role Assume:
...
[profile work]
region = us-east-1
output = json
[profile tf-admin]
role_arn = arn:aws:iam::492***148:role/tf-admin
source_profile = work
...
Error: The configmap “aws-auth” does not exist
Досить часта помилка, принаймні я неодноразово з нею стикався – коли під час виконання terraform apply в кінці отримуємо цю помилку, а сама aws-auth в кластері не створена.
Це призводить по-перше до того, що до кластеру не підключаються дефолтні WokrerNodes, по-друге – ми не можемо отримати доступ до кластеру з kubectl, бо хоча aws eks update-kubeconfig створює новий контекст в локальному ~/.kube/config, сам kubectl повертає помилку авторизації в кластері.
Продебажити це допомогло включення дебаг-логу Terraform через змінну TF_LOG=INFO, де була сама помилка аутентифиікації провайдеру:
Помилка виникала саме через те, що в args провайдеру не було задано правильний локальний профайл.
Є інший варіант аутентифікації – через token, див. цей коментар в GitHub Issues.
Але з ним були проблеми при створенні кластеру з нуля, бо Терраформ не міг виконанти data "aws_eks_cluster_auth". Треба ще якось спробувати, бо в цілому ідея з токеном мені подобається більше, ніж через AWS CLI. З іншого боку – у нас ще будуть провайдери kubectl та helm, і не факт, що їх можна аутентифікувати через токен (хоча, скоріш за все можно, але треба покопатись).
Terraform Kubernetes module
Окей, з провайдером розібрались – давайте додавати сам модуль.
Спочатку запустимо сам кластер с однією NodeGroup, а потім вже будемо додавати всякі контроллери.
Типи EKS NodeGroups
AWS має два типи NodeGroups – Self-Managed, та Amazon Managed, див. Amazon EKS nodes.
Головна, як на мене, перевага Amazon Managed це те, що ви не маєте перейматись оновленнями – все, що стосується операційної системи і компонентів самого Kubernetes, бере на себе Амазон:
Хоча якщо робити Self-managed Nodes використовуючи AMI від самого AWS з Amazon Linux – то там все вже буде налаштовано, і навіть для апдейтів достатньо ребутнути чи перестворити ЕС2 – тоді він запуститься з AMI з останніми патчами.
Також, Managed NodeGroups не потребують окремих налаштувать у aws-auth ConfigMap – EKS сам додасть необхідні записи.
Anayway, щоб полегшити собі життя – будемо використовувати Amazon Managed Nodes. На цих нодах будуть жити тільки контроллери – “Critical Addons”, а ноди для ворклоадів будуть менеджитись Karpenter-ом.
Terraform EKS variables
Спершу нам потрібні будуть змінні.
Взагалі добре пройтись по всім inputs, і подивитись що можна налаштувати під себе.
Для мінімального конфігу нам знадобляться:
cluster_endpoint_public_access – bool
cluster_enabled_log_types – list
eks_managed_node_groups:
min_size, max_size та desired_size – number
instance_types – list
capacity_type – string
max_unavailable_percentage – number
aws_auth_roles – map
aws_auth_users – map
Поділимо змінні на три групи – одна для самого EKS, друга – з параметрами для NodeGroups, і третя – для IAM Users.
Описуємо першу змінну – с параметрами для самого EKS:
Далі, параметри для NodeGroups. Створимо об’єкт типу map, в якому зможемо додавати конфігурції для декількох груп, які будемо тримати в елементах з типом object, бо параметри будуть різних типів:
...
variable "eks_managed_node_group_params" {
description = "EKS Managed NodeGroups setting, one item in the map() per each dedicated NodeGroup"
type = map(object({
min_size = number
max_size = number
desired_size = number
instance_types = list(string)
capacity_type = string
taints = set(map(string))
max_unavailable_percentage = number
}))
}
Приклад додавання Taints є тут>>>, тож описуємо їх та інші параметри у tfvars:
І третя группа – список IAM юзерів, котрі будуть додані до aws-auth ConfgiMap для доступу до кластеру. Тут використовуємо тип set з ще одним object, бо для юзера потрібно буде передавати list зі список RBAC-груп:
...
variable "eks_aws_auth_users" {
description = "IAM Users to be added to the aws-auth ConfigMap, one item in the set() per each IAM User"
type = set(object({
userarn = string
username = string
groups = list(string)
}))
}
Окремо створимо IAM Role з політикою eks:DescribeCluster, і підключимо її до кластеру в групу system:masters – використовуючи цю роль, інші юзери зможуть проходити авторизацію в кластері.
В роль нам потрібно буде передати AWS Account ID, щоб в Principal обмежити можливість виконання AssumeRole тільки юзерами цього акаунту.
$ kubectl auth can-i get pod
yes
$ kubectl get pod -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system aws-node-99gg6 2/2 Running 0 41h
kube-system aws-node-bllg2 2/2 Running 0 41h
...
В наступній частині вже встановимо решту – Karpenter та різні Controllers.
Помилка Get “http://localhost/api/v1/namespaces/kube-system/configmaps/aws-auth”: dial tcp: lookup localhost on 10.0.0.1:53: no such host
Під час тестів перестворював кластер, щоб впевнитись, що весь код, описаний тут, працює.
І при видаленні кластеру Terraform видавав помилку:
...
Plan: 0 to add, 0 to change, 34 to destroy.
...
╷
│ Error: Get "http://localhost/api/v1/namespaces/kube-system/configmaps/aws-auth": dial tcp: lookup localhost on 10.0.0.1:53: no such host
│
...
Рішення – видалити aws-auth зі стейт-файлу:
$ terraform state rm module.eks.kubernetes_config_map_v1_data.aws_auth[0]
Ясна річ, що робити це треба тільки для тестового кластеру, а не Production.
Отже, з Терраформом трохи розібрались, згадали що до чого – час робити щось реальне.
Перше, що будемо розгортати з Terraform – це кластер AWS Elastic Kubernretes Service та всі пов’язані з ним ресурси, бо зараз це зроблено з AWS CDK, і окрім інших проблем з CDK, вимушені мати EKS 1.26, бо 1.27 в CDK ще не підтримується, а в Terraform є.
Як все буде готово на Dev – скопіюємо до Prod, і оновимо файл terraform.tfvars.
Terraform debug
При виникненні проблем – включаємо дебаг-лог через змінну TF_LOG та вказуємо рівень:
$ export TF_LOG=INFO
$ terraform apply
Підготовка Terraform
Описуємо AWS Provider, і відразу задаємо default_tags, які будуть додані до всіх ресурсів, створені за допомогою провайдера. Потім окремо ще в самих ресурсах додамо теги типу Name.
А аутентифікацію в самому AWS – через змінні оточення AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY та AWS_REGION.
Створюємо файл backend.tf – корзина та DynamoDB таблиця вже створені з іншого проекту (я таки вирівшив винести управління S3 та DynamoDB окремим проектом Terraform в окремому репозиторії):
variable "project_name" {
description = "A project name to be used in resources"
type = string
default = "atlas-eks"
}
variable "component" {
description = "A team using this project (backend, web, ios, data, devops)"
type = string
}
variable "environment" {
description = "Dev/Prod, will be used in AWS resources Name tag, and resources names"
type = string
}
variable "eks_version" {
description = "Kubernetes version, will be used in AWS resources names and to specify which EKS version to create/update"
type = string
}
І додаємо terraform.tfvars. Сюди вносимо всі не-sensitive дані, а sensitive будемо передавати через -var або змінні оточення в CI/CD у формі TF_VAR_var_name:
З project_name, environment та eks_version далі зможемо створювати ім’я як:
locals {
# create a name like 'atlas-eks-dev-1-27'
env_name = "${var.project_name}-${var.environment}-${replace(var.eks_version, ".", "-")}"
}
Поїхали.
Створення AWS VPC з Terraform
Для VPC нам потрібні будуть AvailabilityZones, отримаємо їх за допомогою data "aws_availability_zones", бо в майбутньому скоріш за все будемо мігрувати в інші регіони AWS.
Для створення VPC з Terraform візьмемо модуль від @Anton Babenko – terraform-aws-vpc.
VPC Subnets
Для модулю нам потрібно буде передати публічні та приватні сабнети у вигляді CIDR-блоків.
Обидва інструменти досить цікаві, бо в IP Calculator дуже добре відображає інформацію в тому числі у binary виді, а в Visual Subnet Calculator дуже наглядно показується як саме блок розбивається на менші блоки:
Інший підхід – створювати блоки прямо в коді за допомогою функції cidrsubnets, яка використовується в модулі terraform-aws-vpc.
І третій підхід – зробити менеджмент адрес через ще один модуль, наприклад subnets. Спробуємо його (насправді під капотом він теж використовує ту ж саму функцію cidrsubnets).
В принципі все, що в ньому треба задати – це кількість біт для сабнетів. Чим більше біт задається – тим більше “зміщення” по масці, і тим менше буде виділено на підмережу, тобто:
subnet-1: 8 біт
subnet-2: 4 біт
Якщо VPC CIDR буде мати /16, то це буде виглядати як:
11111111.11111111.00000000.00000000
Відповідно для subnet-1 маска буде 16+8, тобто 11111111.11111111.11111111.00000000 – /24 (24 біти “зайняті”, 8 останніх – “вільні”), а для subnet-2 буде 16+4, тобто 11111111.11111111.11110000.00000000 – /20, див. таблицю у IP V4 subnet masks.
Тоді у разі 11111111.11111111.11111111.00000000 ми маємо вільним для адресації останній октет, тобто 256 адрес, а у 11111111.11111111.11110000.00000000 – 4096 адрес.
Цього разу я вирішив відійти від практики створювати окремі VPC під кожен сервіс/компнент проекту, бо в подальшому це по-перше ускладнює менеджмент через необхідність створювати додаткові VPC Peerings і уважно продумувати блоки адрес, щоб уникнути перекриття адрес, по-друге – VPC Peering додатково будуть коштувати грошей за трафік між ними.
Отже, буде окрема VPC для Dev, та окрема – для Prod, а тому треба відразу задати великий пул адрес.
Тож саму VPC зробимо /16, а всередені “наріжемо” підмереж по /20 – в приватних будуть поди EKS і якісь internal сервіси AWS типу Lambda-функцій, а в публічних – NAT Gateways, Application Load Balancers і що там потім ще з’явиться.
Окремо створимо підмережі для Kubernetes Control Plane.
Для параметрів VPC створимо єдину varibale з типом object, бо тут будемо тримати не тільки CIDR, але й інші параметри з різними типами:
Та у main.tf описуємо отримання списку AvailabilityZones та створюємо локальну змінну env_name для тегів:
data "aws_availability_zones" "available" {
state = "available"
}
locals {
# create a name like 'atlas-eks-dev-1-27'
env_name = "${var.project_name}-${var.environment}-${replace(var.eks_version, ".", "-")}"
}
VPC та пов’язані ресурси винесемо в окремий файл vpc.tf, де описуємо сам модуль subnets з шістью сабнетами – 2 публічні, 2 приватні, і 2 маленькі – для EKS Control Plane:
module "subnet_addrs" {
source = "hashicorp/subnets/cidr"
version = "1.0.0"
base_cidr_block = var.vpc_params.vpc_cidr
networks = [
{
name = "public-1"
new_bits = 4
},
{
name = "public-2"
new_bits = 4
},
{
name = "private-1"
new_bits = 4
},
{
name = "private-2"
new_bits = 4
},
{
name = "intra-1"
new_bits = 8
},
{
name = "intra-2"
new_bits = 8
},
]
}
Перевіримо, що зараз вийде.
Або просто з terraform apply, або відразу додамо outputs.
У файлі outputs.tf додамо відображення VPC CIDR, змінної env_name, та сабнетів.
Нам потрібен network_cidr_blocks, бо в іменах маємо тип сабнету – private чи public.
Тож створюємо такі outputs:
output "env_name" {
value = local.env_name
}
output "vpc_cidr" {
value = var.vpc_params.vpc_cidr
}
output "vpc_public_subnets" {
value = [module.subnet_addrs.network_cidr_blocks["public-1"], module.subnet_addrs.network_cidr_blocks["public-2"]]
}
output "vpc_private_subnets" {
value = [module.subnet_addrs.network_cidr_blocks["private-1"], module.subnet_addrs.network_cidr_blocks["private-2"]]
}
output "vpc_intra_subnets" {
value = [module.subnet_addrs.network_cidr_blocks["intra-1"], module.subnet_addrs.network_cidr_blocks["intra-2"]]
}
В модуль vpc в параметри vpc_public_subnets, vpc_private_subnets та intra_subnets передаємо map з двома елементами – по кожній сабнет відповідного типу.
У модуля досить багато inputs для конфігурації, і є гарний приклад того, як його можна використати – examples/complete/main.tf.
Що нам тут може знадобитись:
putin_khuylo: must have з очевидним значенням true
public_subnet_names, private_subnet_names та intra_subnet_names: задати власні імена сабнетів – але по дефолту імена генеруються досить зручні, тож не бачу сенсу міняти (див. main.tf)
enable_nat_gateway, one_nat_gateway_per_az або single_nat_gateway: параметри для NAT Gateway – власне, будемо робити дефолтну модель, з окремим NAT GW на кожну приватну мережу, але відразу додамо можливість змінити в майбутньому (хоча можливо побудувати кластер взагалі без NAT GW, див. Private cluster requirements)
enable_vpn_gateway: поки не буде, але відразу додамо на майбутнє
Останнім для VPC нам потрібно налаштувати VPC Endpoints.
Це прям must have фіча і з точки зору безпеки, і з точки зору вартості інфрастуктури, бо в обох випадках ваш трафік ходить всередені мережі замість того, щоб відправлятись в мандрівку через інтернет на зовнішні ендпонти AWS типу s3.us-east-1.amazonaws.com.
Ендпоінти можна створити за допомогою внутрішнього модуля vpc-endpoints, який включено в сам модуль VPC.
Приклад ендпоінтів є в тому ж файлі examples/complete/main.tf або на сторінці сабмодуля, і вони нам потрібні всі окрім ECS та AWS RDS – в конкретно моєму випадку RDS на проекті нема, але є DynamoDB.
Також додамо ендпоінт для AWS STS, але на відміну від інших, щоб трафік йшов через цей ендпоінт, сервіси мають використовувати AWS STS Regionalized endpoints. Зазвичай це можна задати в Helm-чартах через values або для ServiceAccount задати аннотацію eks.amazonaws.com/sts-regional-endpoints: "true".
Майте на увазі, що використання Interface Endpoints коштує грошей, бо під капотом використовується AWS PrivateLink, а Gateway Endpoints безкоштовні, але доступні тільки для S3 та DynamoDB.
Проте це все одно набагато вигідніше, ніж “ходити” через NAT Gateways, де трафік коштує 4.5 центи за гігабайт (плюс вартість за годину самого гейтвея), тоді як через Interface Ednpoint ми будемо платити лише 1 цент за гігабайт трафіку. Див. Cost Optimization: Amazon Virtual Private Cloud та Interface VPC Endpoint.
В модулі відразу можемо створити і IAM Policy для ендпоінтів. Але так как у нас в цій VPC буде тільки Kubernetes з його подами, то поки не бачу сенсу в додаткових політиках. До того ж, для Interface Endpoints можна додати Security Group.
Ендпоінти для STS та ECR будуть Interface типу, тому їм задаємо ID приватних мереж, а для S3 та DynamoDB – передаємо ID таблиць маршрутизації, бо вони будуть Gateway Endpoint.
Ендпоінти S3 та DynamoDB робимо Gateway type, бо вони бескоштовні, а інші – Interface.
count: самий простий, використовується з заданим числом або з фукнцією length(); використовує індекси list або map для ітерації
підходить для створення однакових ресурсів, які не будуть змінюватись
for_each: має більше можливостей, використовується з map або set, використовує іммена ключів послідовності для ітерації
підходить для створення однотипних ресурсів, але з можливістю задати різні параметри
for: використовується для фільтрації та трансмормації об’єктів з lists, sets, tuples або maps; може бути використано разом з такими функціями, як if, join, replace, lower або upper
Terraform count
Отже, count самий базовий і перший метод для виконання задач в циклі.
Аргументом приймає або number, або list чи map, виконує ітерацію, і кожному об’єкту задає індекс відповідвідно до його позиції в послідовності.
В результаті Terraform створить масив (array) з трох корзин з іменами bucket-0, bucket-1 та bucket-2.
Ми також можемо передати список і використати функцію length(), щоб отримати кількість елементів в цьому списку, і потім пройтись по кожному з них, використовуючи їхні індекси:
В такому випадку будуть створені три корзини з іменами “bucket-test-project-1“, “bucket-test-project-2” та “bucket-test-project-3“.
Щоб отримати значеня імен корзин, які створювались таким чином, можемо використати “*” для вибору всіх індекісів з масиву aws_s3_bucket.bucket:
...
output "bucket_names" {
value = aws_s3_bucket.bucket[*].id
}
Але у count є один важливий нюанс: саме через прив’язку елементів до індексів, ви може отримати несподіваний результат.
Наприклад, якщо створити ці три корзини, а потім додати новий проект на початку або всередені списку, то Terraform видалить коризини для проектів після доданого, бо в списку зміняться індекси об’єктів.
Тобто якщо enabled = true, то створюємо 1 корзину, якщо false – то 0.
Terraform for_each
for_each довзляє виконувати ітерації більш гнучко.
Він приймає map або set, і для ітерації замість індексів використовує кожен key та value з послідовності. В такому випадку саме кількість key буде визначати кількість ресурсів, котрі будуть створені.
Завдяки тому, що кожен key являється унікальним, зміна значень в set/map не впливає на те, як ресурси будуть створені.
Крім set та map ви можете використати тип list, але його треба буде “загорнути” у фунцію toset(), щоб перетворити на set, з якого for_each зможе отримати пару key:value – в такому випадку значення key буде == значенню value.
for_each з set та list
Отже, якщо взяти той же ресурс aws_s3_bucket, то з for_each ми можемо створити корзини так:
Або можна використати навіть map of maps, і для кожної корзини передавати набір параметрів, і потім звертатись до параметра через each.value.PARAM_NAME.
Наприклад, в одному параметрі задамо тег Name, а в іншому – object_lock_enabled:
На відміну від count та for_each, метод for використовується не для створення ресурсів, а для операцій фільтрування та трансформації над значеннями змінних.
Ситнаксис для for виглядає так:
[for <ITEM> in <LIST> : <OUTPUT>]
Тут ITEM – ім’я локальної до циклу змінної, LIST – список, в якому буде виконуватись ітерація, а OUTPUT – результат трансформації.
Наприклад, можемо вивести імена бакетів як UPPERCASE таким чином:
...
output "bucket_names" {
value = [for a in values(aws_s3_bucket.bucket)[*].id : upper(a)]
}
for та conditionals expressions
Також перед OUTPUT можемо додати фільтр, тобто виконати дію тільки над деякими об’єктами зі списку, наприклад:
output "bucket_names" {
value = [for a in values(aws_s3_bucket.bucket)[*].id : upper(a) if can(regex(".*-1", a))]
}
Тут ми за допомогою функцій can() та regex() перевіряємо значення змінної a, і якщо вона закінчується на “-1”, то виконуємо upper(a):
...
bucket_names = [
"BUCKET-TEST-PROJECT-1",
]
for та ітерація по map
Можно виконати ітерацію над key:value з map variable:
variable "common_tags" {
type = map(string)
default = {
"Team" = "devops",
"CreatedBy" = "terraform"
}
}
output "common_tags" {
value = [for a, b in var.common_tags : "Key: ${a} value: ${b}" ]
}
В результаті отримаємо об’єкт типу list зі значеннями:
Можна зробити єдину змінну, яка буде мати різні типи даних для різних значень, а потім виконати ітерацію з for_each та for разом.
Наприклад, створимо variable з типом list, в якому будуть значення типу object, а в object будуть два поля типу string, та одне для списку тегів з типом list:
Потім в ресурсі aws_s3_bucket в цикл for_each передаємо значення var.projects.name, а для тегів робимо цикл по кожному ресурсу з list, і в кожному ресурсі створюємо key:value з each.value.tags.
Nested for loops для map of lists
Для роботи з багаторівневими об’єктами в одному циклі for можна визивати інший.
Наприклад, маємо список проектів, для кожного є один чи кілька “dev/prod” оточень:
variable "projects" {
description = "project names list to be used in S3 and DynamoDB names"
type = map(list(string))
default = {
atlas-tf-backends-test = [
"prod"
]
atlas-eks-test = [
"dev", "prod"
]
}
}
Щоб побудувати list з елементами, які будуть містити ім’я проекту + ім’я оточення – використовуємо два for:
locals {
table_names = [
for project, envs in var.projects : [
for env in envs :
"${project}-${env}"
]
]
}
output "dynamodb_table_names" {
value = local.table_names
}
А щоб побудувати map, де ключами будуть ім’я проекту + ім’я, а в значенні інший map – можно використати функцію merge та оператор “...“, як наведено в цьому коментарі на GitHub:
locals {
table_names_map = merge([
for project, envs in var.projects : {
for env in envs :
"${project}-${env}" => {
"project" = project
"env" = env
}
}
]...)
}
output "dynamodb_table_names" {
value = local.table_names_map
}