Отже, вибрав все ж варіант з менеджментом бекендів через окремий проект 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
}
В цьому пості трохи подивимось на типи даних, які можемо використовувати в Terraform, щоб простіше розібратись з наступним постом – Terraform: цикли count, for_each та for.
map також може включати в себе list або інший map, але всі об’єкти мають бути одного типу (тобто, не можна мати map в якому будуть і list, і другий map):
Блог працює на WordPress – “так склалося історично”, і насправді він цілком влаштовує – все працює добре, море плагінів і можливостей кастомізації.
Хостинг – досі Digital Ocean.
Сьогодні переїхав з PHP 7.3 на 8.3 – і все добре, тільки поламалось декілька плагінів, і, на жаль, мій улюблений Simterm, який дозволяв красиво виводити консольні команди в постах.
Тепер старі пости будуть трохи некрасиві:
Написав девелоперу, може все ж пофіксить, хоча судячи з того, що останній раз плагін оновлювався 3 роки тому – то навряд чи.
Мови блогу
З 2012 пости писалися руською. Перший пост англійською з’явився о 2019, а українською – о 2022. Того ж року дефолтна мова блогу була переключена з рос. на українську.
Статистка постів, включаючи чернетки:
рос: 2,154
англ: 302
українська: 119
Коли додавав українську – робив опитування на тему “На якій мові вести блог”, і результат опитування був 50/50.
Але, як виявилося, переглядів українською майже втричі більше, ніш рос:
Тож врешті-решт вирішив, що не варто витрачати час на третій переклад, і тепер всі нові пости додаються українською та англійською мовами.
Статистка блогу
Взагалі, трафік дуже просів – якщо наприкінці 2021 було понад 5.000 відвідувачів, до зараз менше 1000.
Статистка по країнам – в топі Україна, другою йдуть Сполучені Штати:
На Cloudflare дропається трафік з білорусі та рф, але все одно якось є.
Стара тема оформлення блогу
Колись, до 2016, блог виглядав так:
Як я пишу в блог?
Часто питають – як пишу в блог.
Колись напишу про те, як пишу 🙂
Але якщо коротко, то – коли сетаплю щось нове, то накидую в блог покроково те, що роблю, з копіпастою команд з консолі та пару слів про те, що там було.
Потім, як вже є час – то привожу в читабельний вид, і додаю новий пост.
Саме складне, особливо коли знайомишся з якось новою системою – це зрозуміти, про що саме писати, і як це все зібрати до купи та створити структуру нового посту.
Приклад чернетки:
Потім вже на вихідних – роблю переклад на англійську.
Нещодавно створив групу в LinkedIn, але там поки тільки один пост, бо ще не робив нових перекладів. Спробую робити репости і туди, подивимось, чи зайде людям.
Заодно нагадаю, що є Телеграм-канал з апдейтами – @rtfm, та група для обговорень – @rtfm.
Про автора
А що про себе написати-то?
В IT працюю з 2005 – починав “системним адміністратором” в компанії, де було 4 чи 5 ПК. Потім тех. підтримка Freehost.ua – досі люблю цей хостинг, і всі домени реєструю там. Далі був дата-центр Воля-кабль (не люблю) – теж тех. підтримка, потім “провідний інженер тех. підтримки” – то вже більше було системне адміністрування.
А от у 2013 потрапив на перший проект у “великому IT”, як я його називаю. Спочатку Luxoft на посаді Release Engineer, потім два проекти в Ciklum, вже як DevOps Engineer, потім мій перший продукт і стартап – BetterMe, де пропрацював майже 5 років.
BetterMe вважаю моєю “історією успіху”, бо прийшов туди, коли там був один ЕС2 в AWS і команда з 14 людей, а пішов, коли мали штук 10 Kubernetes-кластерів в різних регіонах, близько 150 ЕС2-інастансів, штук 40 інстансів серверів баз даних в AWS RDS Aurora, і команду у 200 людей.
Хоча номінально позиції досі називаються “DevOps Engineer” (вже давненько Senior, та й лідом і Head Of Devops побув), але по факту я більше Cloud Infrastructure Enginner та Site Reability Engineer, бо в основному займаюсь AWS, моніторингом та якоюсь базовою кібербезпекою.
Зараз теж в продукті, знов стартап, бо це неймовірно круто, коли ти маєш змогу побудувати щось своє, з самого нуля. І почуття відповідальності за те, що будуєш – дуже драйвить, бо саме на тобі відповідальність за “фундамент” проекту, за його інфраструктуру.
Ну і мабуть, варто згадати, що з 2021 я став AWS Hero. Між іншим – перший з України.
Рішення по типу Terraform Cloud, Terragrunt, Spacelift, Atlantis та Cluster.dev поки лишимо осторонь – проект ще малий, і вносити додаткові утіліти не хочеться. Почнемо з простого, а як воно все взлетить – то вже будемо думати про подібні рішення.
Тепер спробуємо все зібрати в кучу, і набросати план майбутньї автоматизації.
Отже, про що треба подумати:
керування бекендом, або project bootstrap: бакет(и) для state-файлів та таблицю(і) DynamoDB для state lock:
можна створювати руками для кожного проекту
можна створити окремий проект/репозиторій, і в ньому менеджити всі бекенди
можна створювати в рамках кожного проекту на початку роботи в коді самого проекту
розділення по Dev/Prod оточенням:
Terraform Workspaces: built-in фіча Terraform, мінімум дублікації коду, але можуть бути складнощі з навігацією, може використовувати тільки один backend (проте з окремими директоріями в ньому), складноші роботи з модулями
Git branches: built-in фіча Git, простота навігації по коду, можливість мати окремі бекенди, але багато дублікації коду, морока с переносом коду між оточеннями, складнощі роботи з модулями
Separate Directories: максимальная ізоляція і можливість мати окремі бекенди та провайдери, але можлива дублікація коду
Third-party tools: Terragrunt, Spacelif, Atlantis тощо – чудово, але потребує додаткового часу на вивчення інженерами та імплементацію
Сьогодні спробуємо підхід з менеджементом бакету для бекенду з коду самого проекту, а Dev/Prod робити через окремі директорії.
Керування бекендом, або project bootstrap
Тут будемо використовувати підхід зі створенням бекенду в рамках кожного проекту на старті.
Тобто:
спочатку описуємо створення бакету та таблиці Динамо
створюємо ресурси
налаштовуємо блок terraform.backend{}
імпортуємо стейт
описуємо та створюємо всі інші ресурси
Розділення по Dev/Prod оточенням з окремими директоріями
Як все може виглядати з окремими каталогами?
Можемо створити структуру:
global
main.tf: створення ресурсів для бекенду – S3, Dynamo
environments
dev
main.tf: тут включаємо потрібні модулі (дублються з Prod, але відрізняється під час розробки та тестування нового модулю)
variables.tf: декларуємо змінні, загальні (дублюються з Prod) та специфічні до оточення
terraform.tfvars: значення змінних, загальні (дублюються з Prod) та специфічні до оточення
providers.tf: налаштування підключення до AWS/Kubernetes, специфічні до оточення (осолибво корисно, коли Dev/Prod це різні акаунти AWS)
backend.tf: налаштування зберігання state-файлів, специфічні до оточення
prod
<аналогічно Dev>
modules
vpc
main.tf – описуємо модулі
backend.hcl – загальні параметри для state backend
Тоді можемо деплоїти окремі оточення або виконуючи cd environments/dev && terraform aplly, або terraform aplly -chdir=environments/dev. Бекенд можемо передавати через terraform init -backend-config=backend.hcl.
Ну і давайте спробуємо, і подивимось, як воно може виглядати в роботі.
Створення бекенду
Тут будемо робити бекенд з коду самого проекту, але мені все ж вважається кращим менеджмент AWS ресурсів для бекендів винести окремим проектом в окремому репозиторії, бо зі схемою наведеною нижче створення нового проекту виглядає трохи complecated – якщо це будуть робити самі девелопери, то їм доведеться робити окремі кроки, і для цього потрібно буде писати окрему доку. Краще нехай при старті проекту передадуть нам його ім’я, “девопси” зроблять корзину та DynamoDB таблицю, а далі девелопери вже просто захардкодять їхні імена в свої конфіги.
Виконуємо ініціалізацію ще раз, та через -backend-config передаємо шлях до файлу с параметрами бекенду:
[simterm]
$ terraform init -backend-config=../backend.hcl
Initializing the backend...
Acquiring state lock. This may take a few moments...
Do you want to copy existing state to the new backend?
...
Enter a value: yes
...
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.14.0
Terraform has been successfully initialized!
[/simterm]
Перевіряємо корзину:
[simterm]
$ aws s3 ls tf-state-bucket-envs-management-test/global/
2023-08-30 16:57:10 8662 terraform.tfstate
[/simterm]
Перший стейт-файл є, чудово.
Створення та використання модулів
Додамо власний модуль для VPC. Тут просто для приклада, в продакшені будемо використовувати AWS VPC Terraform module.
Тепер у нас виходить така структура каталогів та файлів:
І тепер можемо деплоїти ресурси.
Спочатку Dev:
[simterm]
$ cd environments/dev/
$ terraform init -backend-config=../../backend.hcl
$ terraform apply
[/simterm]
І повторюємо для Prod:
[simterm]
$ cd ../prod/
$ terraform init -backend-config=../../backend.hcl
$ terraform apply
[/simterm]
Перевіряємо бакет стейтів:
[simterm]
$ aws s3 ls tf-state-bucket-envs-management-test/
PRE dev/
PRE global/
PRE prod/
Та самі стейти:
[simterm]
$ aws s3 ls tf-state-bucket-envs-management-test/dev/
2023-08-30 17:32:07 1840 terraform.tfstate
[/simterm]
І чи створились VPC:
Динамічні оточення
Добре – схема з окремими диреткоріями для Dev/Prod виглядає робочю.
Але як бути для динамічних оточень, тобто коли ми хочемо створити інфрастуктуру проекту під час створення Pull Request в Git, для тестів?
Тут можемо використати такий флоу:
бранчуємось від мастер-бранчу
робимо свої зміни в коді environments/dev/
ініціалізуємо новий бекенд
і деплоїмо з terraform apply -var з новими значеннями змінних
Ініціалізуємо новий стейт. Додаємо -reconfigure, бо робимо локально, і тут вже є .terraform. У випадку, коли це буде виконуватись з GitHub Actions – директорія буде чистою, і можна виконувати просто init.
У другому параметрі -backend-config передаємо ключ для стейту – в якій директорії корзини зберігати файл: