RTFM: як і для чього пишеться цей блог?
0 (0)

9 Вересня 2023

Вже давно і досить часто просять розказати як пишу пости в блог. Ну і раз вже така тема, і я нарешті таки зібрався про це написати – то давайте поглянемо навіщо взагалі вести блог, і як його вести.

Навіщо вести свій IT блог?

Цей блог я починав головним чином як такий собі блокнот для себе самого – просто записувати як і що я робив, аби потім не шукати в інтернеті якісь мануали, чи записувати те, чого в інтернеті не було взагалі.

Згодом, це трансформувалося в бажання поділитись тим, який я не в біса крутий спеціаліст, бо коли ти вперше збираєш ядро FreeBSD, то здається, що ти бог 🙂 Це, звісно, та ще бугогашенька, бо вперше ядро я зібрав ще у 2007, і робив це як та “мавпа з мануалом” (привіт, Хом’як!) – читав, копіпастив, але дуже мало що розумів.

Насправді коли я вже трохи набрався досвіду, то ведення блогу дало ще один важливий бонус – він допомагає боротись з власним “синдромом самозванця”, бо навіть після (ОМГ!) 18 років в IT цей синдром нікуди не дівався, і я досі іноді думаю “Блін, а нє хєрню лі я написав?”. Проте коли твої пости вже лайкають солюшен архітектори з якогось Oracle – то це дуже допомагає відчувати себе людиною.

Власний бренд

По важливості я б це поставив на друге місце, але почати чомусь хочеться саме з цього.

Ваш блог – це ваш бренд.

Дуже часто, особливо вже за останні років 6-7, приходячи на співбесіду я чув “О! То ви адмін того самого RTFM? Круто – я там дуже багато для себе знайшов!”.

Ось це і є брендом – коли тебе впізнають, як спеціаліста.

Хтось виступає на конференціях – але я надто боюся публіки. А хтось тихенько пише собі бложег, де, звісно, теж публіка, і теж іноді ставлять “мінуси” – але це принаймні “не особисто”.

І от тут ми підходимо до другого пункту:

Власний розвиток

Я вже не представляю як можна виконувати якусь складну задачу, і не вести нотатки в чернетці блогу.

Бо, по-перше, коли ти пишеш – ти структуруєш інформацію, яку отримуєш. Тобі потрібно подати матеріал так, щоб він був зрозумілий і іншим, а для цього потрібно добре зуміти пов’язати всі “компоненти” посту у власній голові.

Це прям дуже, дуже допомагає краще зрозуміти щось нове, запам’ятати це, краще усвідомити всі ті “moving parts” нової системи.

І тут є ще один важливий момент. Колись (я навіть пам’ятаю цей пост – Apache: MPM – worker, prefork или event?, 2012 рік) я показав свій новий пост другу-сисадміну. Дуже прошарений чувак (привіт, Андрій!).

Після того, як він його прочитав, він сказав мені щось на кшталт “Ну, так, прикольно, але ось тут і ось тут відчувається, що ти не дуже шариш в темі”.

І саме відтоді я усвідомив, що, блін – треба копати. Не можна просто “на тяп-ляп” взяти, і написати. Треба писати вдумуючись, усвідомлюючи що і навіщо ти робиш.

А ця потреба призводить до того, що коли ти розбираєшся з новим матеріалом – ти вже не можеш скіпнути якусь не дуже зрозумілу частину – “А, потім розберусь, ілі взагалі фіг з нею”.

Ніт! Ти мусиш сісти, і розібратись. І саме це дуже допомогає у власному розвитку, як спеціаліста, бо потім, коли на якійсь співбесіді тебе питають по якійсь темі – то ти можеш розкрити якісь деталі цієї теми, показати, що ти дійсно розбираєшся в темі, а не просто “навкоси” прочитав документацію, скопіпастив команду, запустив сервіс, і вважаєш, що ти його знаєш.

Взагалі, уважність до деталей, уміння розібратись з тим, “а що там під капотом” дуже допомогає в роботі. Бо тільки знаючи ЯК система працює, що відбувається у неї всередині, ти можеш зрозуміти куди копати, щоб потім цю систему пофіксити.

Повертаючись трохи назад до “усвідомлюючи що і навіщо ти робиш” – така звичка у веденні блогу вже стала звичкою і в роботі: не можна просто “взяти, зробити, і забити”. Ти маєш розуміти що і як ти зробив, бо, по-перше – тобі ж  цю систему потім і підтримувати, по-друге – ти несеш відповідальність за те, що ти робиш. А враховуючи те, що наша, девопсів, робота дуже багато пов’язана з інфраструктурою, з цим “фундаментом” будь-якого проекту, з усіма його даними – то відчуття відповідальності тут вкрай необхідно.

Власна документація

Часто, прям дуже часто я повертаюсь до якихось старих постів, щоб подивитись що і як я робив. Це допомагає і під час сетапу якоїсь вже знайомої системи на новому проекті, і під час спроб зрозуміти що зламалось на поточному.

Навіть більше – у твоїх колег є доступ до інформації як саме ти піднімав якусь систему, і коли я був тім-лідом – то хлопці дуже часто використовували РТФМ, щоб розібратись з чимось, що я колись сетапив.

Ведення блогу, звісно, не значить, що можна не вести “локальну документацію” десь в проектному Confluence, але в своєму блозі ти можеш набагато краще описати що і навіщо ти робив, і чому зробив саме так, а не інакше.

Як вести свій блог?

Перше, і головне, над чим я замислювався раніше, коли цей блог тільки починався, це:

А про що, власне, писати?

І зараз тут відповідь та ж сама, що була тоді: про те, що ти робиш, з чим ти стикаєшся, або про те, що не дуже розумієш – а тобі треба розібратись.

Але саме, мабуть, важке, це саме створити структуру поста – зрозуміти про що саме писати, і як саме це висловити – що до чого відноситься, про що написати в першу чергу, про що в другу. Що винести окремою частиною – а про що достатньо буде написати пару речень.

Наприклад ось, як я “морально готувався” до написання цього поста:

Структура матеріалу

Навіть зараз, коли я пишу цей матеріал – я його перечитую, і розділяю на частини (під)заголовками, щоб вони якось логічно розбивали все те, про що тут написано.

Ось ще один приклад зі старих чернеток:

Ти починаєш писати про щось одне, а потім розумієш, що треба розказати і про щось ще, а потім ще про щось… І в результаті сидиш перед мільйоном вкладок, і кашею в голові. Але тобі все одно треба зібратись, і довести це до кінця, і подати так, щоб людина, яка буде читати цей матеріал, зрозуміла що ж насправді ти тут робиш.

Або ось такий приклад:

Тут на початку статті я накидую текст того, про що саме буде йти мова – бо це потім допомагає в голові тримати “нить повествования”.

Шарь в темі!

Так – треба добре розуміти, про що ти пишеш. Але й боятися писати (бо “що ж подумають люди?!?”) – теж не треба.

Ось ще приклад, як іноді виглядає процес написання деяких постів:

Бо знову ж таки – не можна “на тяп-ляп”, а треба розібратись.

Ще, мабуть, варто сказати про довжину постів: краще уникати “полотенець”. Краще розбити пост на декілька частин, і в кожній описати окремо якусь частину теми, ніж намагатись все впихнути в один пост.

Це допоможе і при написанні – бо все ж менша “каша в голові”, і при читанні, бо знов – менше каша в голові у читача.

Мови блогу

Якщо у вас є змога – то краще писати відразу на англійській.

По-перше – це круто.

По-друге – ви не обмежуєте себе читачами тільки з України.

По-третє – це дуже класна практика англійської.

Щодо правопису і помилок – головне, щоб вас зрозуміли. Крім того, можна скористатись плагінами типу Grammarly або LanguageTool, і навіть Google Translate.

Ще для допомоги з перекладами є чудові рішення типу Reverso Context, Reverso Grammar Checker & Rephraser та Deepl.

Де писати?

Тут вибір вкрай широкий – від готових платформ типу Medium – до власного VPS з WordPress.

Я колись вибрав саме WordPress, і саме на виділеному VPS, щоб мати змогу отримати додатковий досвід з адміністрування Linux та всяких Apache/Nginx/PHP/MySQL – і це було дійсно дуже корисним.

Ще один момент, котрий треба мати на увазі: враховуйте той момент, що ведучи свій блог на платформах типу Medium ви фактично довіряєте всю інформацію йому, це такий собі “вендер-лок”, бо потім мігрувати з одної платформи на іншу може бути дуже боляче, тим більш, якщо у вас буде пару тисяч постів.

В принципі, теж стосується і якихось self-hosted платформ типу Jekyll – якщо розробники Jekyll його закинуть, або змінять цінову політику, то ви можете залишитись у “розбитого корита”.

І в цьому плані WordPress мене більш, ніж влаштовує, бо платформа слава богу існує вже багато років, а зараз навіть пропонує оформити реєстрацію на сто років наперед (The 100-Year Plan on WordPress) – оптимісти 🙂

Висновки

Чи є сенс у ведені свого блогу? Для мене відповідь очевидна, бо це дійсно дуже допомагає і в роботі, і у власному розвитку, і у кар’єрі.

Проте треба усвідомлювати, чи на це потрібен час. Деякі пости на РТФМ пишуться тиждень, а то й більше, а потім ще день-два для перекладу на англійську.

Також треба розуміти, що зовсім не відразу блог будуть читати, і що перші місяці у вас може бути пару випадкових відвідувачів на день.

Втім ті плюси, які дає ведення блогу, однозначно варті того, щоб витрачати на це свій час.

Навіть якщо вас ніхто не буде читати – ви навчитесь висловлювати свої думки, подавати матеріал, або прокачаєте свою англійську. У вас завжди буде ваша власна документація. Ви набагато краще будете розуміти те, що робили, коли писали якийсь новий пост.

Loading

Terraform: створення EKS, частина 2 – EKS кластер, WorkerNodes та IAM
0 (0)

9 Вересня 2023

Продовжуємо тему розгортання кластеру AWS Elastic Kubernetes Service за допомогою Terraform.

У першій частині підготували AWS VPC  – див. Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints.

В цій частині розгорнемо сам кластер і налаштуємо AIM для нього, а в наступній – встановимо Karpenter та решту контроллерів.

Планування

В цілому, список TODO наразі виглядає так:

Для кластеру також використаємо модуль, знову від @Anton Babenko – Terraform EKS module. Проте й інші модулі, наприклад – terraform-aws-eks від Cookpad – я ним теж трохи користвувався, працював добре, але порівнювати не візьмусь.

Як і для модуля VPC, у Terraform EKS module теж маємо приклад кластеру та пов’язаних ресурсів – examples/complete/main.tf.

Terraform Kubernetes provider

Для роботи модулю з aws-auth ConfigMap потрібно буде додати ще один провайдер – kubernetes.

У файлі providers.tf додаємо його:

...
provider "kubernetes" {
  host                   = module.eks.cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)

  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args        = ["--profile", "tf-admin", "eks", "get-token", "--cluster-name", module.eks.cluster_name]
  }
}

Тут зверніть увагу, що в args передається AWS-профайл, бо сам кластер створюється Terraform від імені IAM Role:

...
provider "aws" {
  region = "us-east-1"
    assume_role {
    role_arn = "arn:aws:iam::492***148:role/tf-admin"
  }
...

І 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, де була сама помилка аутентифиікації провайдеру:

...
[DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5:  "kind": "Status",
[DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5:  "apiVersion": "v1",
[DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5:  "metadata": {},
[DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5:  "status": "Failure",
[DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5:  "message": "Unauthorized",
[DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5:  "reason": "Unauthorized",
[DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5:  "code": 401
[DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5: }
...

Помилка виникала саме через те, що в 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 з останніми патчами.

Окремо варто загадти Fargate – див. AWS: Fargate – можливості, порівняння з Lambda/EC2 та використання з AWS EKS, але я не бачу в них якогось великого сенсу, тим більш на них не зможемо створювати DaemonSets з, наприклад, Promtail для логів.

Також, 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:

...
variable "eks_params" {
  description = "EKS cluster itslef parameters"
  type = object({
    cluster_endpoint_public_access = bool
    cluster_enabled_log_types      = list(string)
  })
}

Та terraform.tfvars зі значеннями – поки включимо всі логи, потім залишимо тільки реально потрібні:

...
eks_params = {
  cluster_endpoint_public_access = true
  cluster_enabled_log_types      = ["audit", "api", "authenticator", "controllerManager", "scheduler"]
}

Далі, параметри для 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:

...
eks_managed_node_group_params = {
  default_group = {
    min_size       = 2
    max_size       = 6
    desired_size   = 2
    instance_types = ["t3.medium"]
    capacity_type  = "ON_DEMAND"
    taints = [
      {
        key    = "CriticalAddonsOnly"
        value  = "true"
        effect = "NO_SCHEDULE"
      },
      {
        key    = "CriticalAddonsOnly"
        value  = "true"
        effect = "NO_EXECUTE"
      }
    ]
    max_unavailable_percentage = 50
  }
}

І третя группа – список 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)
  }))
}

Значення в tfvars:

...
eks_aws_auth_users  = [
  {
    userarn  = "arn:aws:iam::492***148:user/arseny"
    username = "arseny"
    groups   = ["system:masters"]
  }
]

Як і з NodeGroups, тут ми зможемо задати кілька юзерів, і всі вони потім будуть передані до aws_auth_users модулю EKS.

Створення кластеру

Створюємо файл eks.tf, додаємо модуль:

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 19.0"

  cluster_name    = "${local.env_name}-cluster"
  cluster_version = var.eks_version

  cluster_endpoint_public_access = var.eks_params.cluster_endpoint_public_access

  cluster_enabled_log_types = var.eks_params.cluster_enabled_log_types

  cluster_addons = {
    coredns = {
      most_recent = true
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent = true
    }
  }

  vpc_id                   = module.vpc.vpc_id
  subnet_ids               = module.vpc.private_subnets
  control_plane_subnet_ids = module.vpc.intra_subnets

  manage_aws_auth_configmap = true

  eks_managed_node_groups = {
    default = {

      min_size       = var.eks_managed_node_group_params.default_group.min_size
      max_size       = var.eks_managed_node_group_params.default_group.max_size
      desired_size   = var.eks_managed_node_group_params.default_group.desired_size
      instance_types = var.eks_managed_node_group_params.default_group.instance_types
      capacity_type  = var.eks_managed_node_group_params.default_group.capacity_type

      taints = var.eks_managed_node_group_params.default_group.taints

      update_config = {
        max_unavailable_percentage = var.eks_managed_node_group_params.default_group.max_unavailable_percentage
      }
    }
  }

  cluster_identity_providers = {
    sts = {
      client_id = "sts.amazonaws.com"
    }
  }

  aws_auth_users = var.eks_aws_auth_users
  #aws_auth_roles = TODO
}

Якщо для Addons треба додати параметри – можна зробити з configuration_values, див. приклад тут>>>.

Додамо трохи outputs:

...
output "eks_cloudwatch_log_group_arn" {
  value = module.eks.cloudwatch_log_group_arn
}

output "eks_cluster_arn" {
  value = module.eks.cluster_arn
}

output "eks_cluster_endpoint" {
  value = module.eks.cluster_endpoint
}

output "eks_cluster_iam_role_arn" {
  value = module.eks.cluster_iam_role_arn
}

output "eks_cluster_oidc_issuer_url" {
  value = module.eks.cluster_oidc_issuer_url
}

output "eks_oidc_provider" {
  value = module.eks.oidc_provider
}

output "eks_oidc_provider_arn" {
  value = module.eks.oidc_provider_arn
}

Перевіряємо з terraform plan, деплоїмо, та перевіряємо сам кластер:

Створюємо ~/.kube/config:

$ aws --profile work --region us-east-1 eks update-kubeconfig --name atlas-eks-dev-1-27-cluster --alias atlas-eks-dev-1-27-work-profile
Updated context atlas-eks-dev-1-27-work-profile in /home/setevoy/.kube/config

Та перевіряємо доступ з can-i:

$ kubectl auth can-i get pod
yes

Додаткова IAM Role

Окремо створимо IAM Role з політикою eks:DescribeCluster, і підключимо її до кластеру в групу system:masters – використовуючи цю роль, інші юзери зможуть проходити авторизацію в кластері.

В роль нам потрібно буде передати AWS Account ID, щоб в Principal обмежити можливість виконання AssumeRole тільки юзерами цього акаунту.

Щоб не виносити це окремою змінною в variables.tf – в eks.tf додамо ресурс data "aws_caller_identity":

...
data "aws_caller_identity" "current" {}

І далі описуємо саму роль з assume_role_policy – кому буде дозволено assume цієї ролі, та inline_policy з дозволом на виконання eks:DescribeCluster:

...
resource "aws_iam_role" "eks_masters_access_role" {
  name = "${local.env_name}-masters-access-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          AWS: "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
      }
    ]
  })

  inline_policy {
    name = "${local.env_name}-masters-access-policy"

    policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action   = ["eks:DescribeCluster*"]
          Effect   = "Allow"
          Resource = "*"
        },
      ]
    })
  }  

  tags = {
    Name  = "${local.env_name}-access-role"
  }
}

Повертаємось до module "eks" і в aws_auth_roles додаємо маппінг цієї ролі:

...
  aws_auth_users = var.eks_aws_auth_users
  aws_auth_roles = [
    {
      rolearn  = aws_iam_role.eks_masters_access_role.arn
      username = aws_iam_role.eks_masters_access_role.arn
      groups   = ["system:masters"]
    }
  ]
...

Додамо output:

...
output "eks_masters_access_role" {
  value = aws_iam_role.eks_masters_access_role.arn
}

Деплоїмо зміни:

$ terraform apply
...
Outputs:

...
eks_masters_access_role = "arn:aws:iam::492***148:role/atlas-eks-dev-1-27-masters-access-role"
...

Перевіряємо саму aws-auth ConfigMap:

$ kk -n kube-system get cm aws-auth -o yaml
apiVersion: v1
data:
...
  mapRoles: |
    - "groups":
      - "system:bootstrappers"
      - "system:nodes"
      "rolearn": "arn:aws:iam::492***148:role/default-eks-node-group-20230907145056376500000001"
      "username": "system:node:{{EC2PrivateDNSName}}"
    - "groups":
      - "system:masters"
      "rolearn": "arn:aws:iam::492***148:role/atlas-eks-dev-1-27-masters-access-role"
      "username": "arn:aws:iam::492***148:role/atlas-eks-dev-1-27-masters-access-role"
  mapUsers: |
    - "groups":
      - "system:masters"
      "userarn": "arn:aws:iam::492***148:user/arseny"
      "username": "arseny"
...

Додаємо новий профайл до ~/.aws/confing:

...
[profile work]
region = us-east-1
output = json

[profile eks-1-27-masters-role]
role_arn = arn:aws:iam::492***148:role/atlas-eks-dev-1-27-masters-access-role
source_profile = work

Додаємо новий контекст для kubectl:

$ aws --profile eks-1-27-masters-role --region us-east-1 eks update-kubeconfig --name atlas-eks-dev-1-27-cluster --alias eks-1-27-masters-role
Updated context eks-1-27-masters-role in /home/setevoy/.kube/config

І перевіряємо доступ:

$ 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.

Loading

Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints
0 (0)

7 Вересня 2023

Отже, з Терраформом трохи розібрались, згадали що до чого – час робити щось реальне.

Перше, що будемо розгортати з Terraform – це кластер AWS Elastic Kubernretes Service та всі пов’язані з ним ресурси, бо зараз це зроблено з AWS CDK, і окрім інших проблем з CDK, вимушені мати EKS 1.26, бо 1.27 в CDK ще не підтримується, а в Terraform є.

В цій, першій частині, буде описано створення ресурсів AWS, в другій – створення кластеру (Terraform: створення EKS, частина 2 – EKS кластер, WorkerNodes та IAM), а в третій – встановлення Karpenter та інших контроллерів.

Планування

Що треба зробити – це описати розгортання EKS кластеру і встановити різні дефолтні штуки типу контроллерів:

Будемо використовувати Terraform modules для VPC та EKS від Антона Бабенко, бо в них вже реалізована більша частина того, що треба буде створити.

Dev/Prod оточення

Тут використаємо підхід з розділенням по окремим директоріям з використанням модулів, див. Terraform: динамічний remote state з AWS S3 та multiple environments по директоріям.

Тобто зараз структура каталогів/файлів виглядає так:

$ tree terraform
terraform
└── environments
    ├── dev
    │   ├── backend.tf
    │   ├── main.tf
    │   ├── outputs.tf
    │   ├── providers.tf
    │   ├── terraform.tfvars
    │   └── variables.tf
    └── prod

4 directories, 6 files

Як все буде готово на Dev – скопіюємо до Prod, і оновимо файл terraform.tfvars.

Terraform debug

При виникненні проблем – включаємо дебаг-лог через змінну TF_LOG та вказуємо рівень:

$ export TF_LOG=INFO
$ terraform apply

Підготовка Terraform

Описуємо AWS Provider, і відразу задаємо default_tags, які будуть додані до всіх ресурсів, створені за допомогою провайдера. Потім окремо ще в самих ресурсах додамо теги типу Name.

Авторизацію провайдера робимо через IAM Role (див. Authentication and Configuration), бо саме вона буде потім додана як “прихований root-юзер EKS-кластеру”, див. Enabling IAM principal access to your cluster:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.14.0"
    }
  }
}

provider "aws" {
  region  = "us-east-1"
  assume_role {
    role_arn = "arn:aws:iam::492***148:role/tf-admin"
  }
  default_tags {
    tags = {
      component = var.component
      created-by = "terraform"
      environment = var.environment
    }
  }  
}

А аутентифікацію в самому AWS – через змінні оточення AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY та AWS_REGION.

Створюємо файл backend.tf – корзина та DynamoDB таблиця вже створені з іншого проекту (я таки вирівшив винести управління S3 та DynamoDB окремим проектом Terraform в окремому репозиторії):

terraform {
  backend "s3" {
    bucket = "tf-state-backend-atlas-eks"
    key    = "dev/atlas-eks.tfstate"
    region = "us-east-1"
    dynamodb_table = "tf-state-lock-atlas-eks"
    encrypt = true
  }
}

Додаємо перші variables:

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 = "atlas-eks"
environment        = "dev"
component          = "devops"
eks_version        = "1.27"
vpc_cidr           = "10.1.0.0/16"

З 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-блоків.

Є варіант порахувати їх самому і передавати через variables. Для цього можемо використати або IP Calculator, або Visual Subnet Calculator.

Обидва інструменти досить цікаві, бо в 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, але й інші параметри з різними типами:

variable "vpc_params" {
  type        = object({
    vpc_cidr  = string
  })
}

До terraform.tfvars додаємо значення:

...
vpc_params  = {
  vpc_cidr  = "10.1.0.0/16"
}

Та у 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, та сабнетів.

Модуль subnets має два типи outputsnetwork_cidr_blocks поверне map з іменами мереж в ключах, а networks повертає list (див. Terraform: знайомство з типами даних – primitives та complex).

Нам потрібен 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 з двома елементами – по кожній сабнет відповідного типу.

Перевіряємо з terraform plan:

...
Changes to Outputs:
  + env_name            = "atlas-eks-dev-1-27"
  + vpc_cidr            = "10.1.0.0/16"
  + vpc_intra_subnets   = [
      + "10.1.64.0/24",
      + "10.1.65.0/24",
    ]
  + vpc_private_subnets = [
      + "10.1.32.0/20",
      + "10.1.48.0/20",
    ]
  + vpc_public_subnets  = [
      + "10.1.0.0/20",
      + "10.1.16.0/20",
    ]

Наче виглядає ОК?

Переходимо до самої VPC.

Terraform VPC module

У модуля досить багато 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: поки не буде, але відразу додамо на майбутнє
  • enable_flow_log: дуже корисна штука (див. AWS: Grafana Loki, InterZone трафік в AWS, та Kubernetes nodeAffinity), але це додаткові кости, тому додамо, але поки не включатимо

Додаємо параметри до нашої змінної vpc_params:

variable "vpc_params" {
  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
  })
}

І додаємо значення до tfvars:

...
vpc_params = {
  vpc_cidr               = "10.1.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
}

Щодо тегів: можна задати окремо теги з inputs vpc_tags та/або private/public_subnet_tags.

Також можна додати теги через tags самого ресурсу VPC – тоді вони будуть додані до всіх ресурсів цієї VPC (плюс default_tags з AWS провайдера)

Далі, описуємо саму VPC у vpc.tf:

...
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.1.1"

  name = "${local.env_name}-vpc"
  cidr = var.vpc_params.vpc_cidr

  azs = data.aws_availability_zones.available.names

  putin_khuylo = true

  public_subnets  = [module.subnet_addrs.network_cidr_blocks["public-1"], module.subnet_addrs.network_cidr_blocks["public-2"]]
  private_subnets = [module.subnet_addrs.network_cidr_blocks["private-1"], module.subnet_addrs.network_cidr_blocks["private-2"]]
  intra_subnets   = [module.subnet_addrs.network_cidr_blocks["intra-1"], module.subnet_addrs.network_cidr_blocks["intra-2"]]

  enable_nat_gateway = var.vpc_params.enable_nat_gateway
  enable_vpn_gateway = var.vpc_params.enable_vpn_gateway

  enable_flow_log = var.vpc_params.enable_flow_log
}

І ще раз перевіряємо з terraform plan:

Якщо виглядає ОК – то деплоїмо:

$ terraform apply
...
Apply complete! Resources: 23 added, 0 changed, 0 destroyed.

Outputs:

env_name = "atlas-eks-dev-1-27"
vpc_cidr = "10.1.0.0/16"
vpc_intra_subnets = [
  "10.1.64.0/24",
  "10.1.65.0/24",
]
vpc_private_subnets = [
  "10.1.32.0/20",
  "10.1.48.0/20",
]
vpc_public_subnets = [
  "10.1.0.0/20",
  "10.1.16.0/20",
]

І перевіряємо сабнети:

Додавання VPC Endpoints

Останнім для VPC нам потрібно налаштувати VPC Endpoints.

Це прям must have фіча і з точки зору безпеки, і з точки зору вартості інфрастуктури, бо в обох випадках ваш трафік ходить всередені мережі замість того, щоб відправлятись в мандрівку через інтернет на зовнішні ендпонти AWS типу s3.us-east-1.amazonaws.com.

VPC Endpoint створить Route Table з маршрутами до відповідного ендпоінту всередині VPC (у випадку з Gateway Endpoint), або створить Elastic Network Interface та змінить налаштування VPC DNS (у випадку з Interface Endpoints), і весь трафік буде йти всередині мережі AWS. Див. також VPC Interface Endpoint vs Gateway Endpoint in AWS.

Ендпоінти можна створити за допомогою внутрішнього модуля 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.

Отже, додаємо до нашого vpc.tf:

...
module "endpoints" {
  source  = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints"
  version = "~> 5.1.1"

  vpc_id = module.vpc.vpc_id

  create_security_group = true

  security_group_description = "VPC endpoint security group"
  security_group_rules = {
    ingress_https = {
      description = "HTTPS from VPC"
      cidr_blocks = [module.vpc.vpc_cidr_block]
    }
  }

  endpoints = {
    dynamodb = {
      service         = "dynamodb"
      service_type    = "Gateway"
      route_table_ids = flatten([module.vpc.intra_route_table_ids, module.vpc.private_route_table_ids, module.vpc.public_route_table_ids])
      tags = { Name = "${local.env_name}-vpc-ddb-ep" }
    }
    s3 = {
      service         = "s3"
      service_type    = "Gateway"
      route_table_ids = flatten([module.vpc.intra_route_table_ids, module.vpc.private_route_table_ids, module.vpc.public_route_table_ids])
      tags = { Name = "${local.env_name}-vpc-s3-ep" }
    },
    sts = {
      service             = "sts"
      private_dns_enabled = true
      subnet_ids          = module.vpc.private_subnets
      tags = { Name = "${local.env_name}-vpc-sts-ep" }
    },
    ecr_api = {
      service             = "ecr.api"
      private_dns_enabled = true
      subnet_ids          = module.vpc.private_subnets
      tags = { Name = "${local.env_name}-vpc-ecr-api-ep" }
    },
    ecr_dkr = {
      service             = "ecr.dkr"
      private_dns_enabled = true
      subnet_ids          = module.vpc.private_subnets
      tags = { Name = "${local.env_name}-vpc-ecr-dkr-ep" }
    }
  }
}

У source задаємо шлях з двома слешами, бо:

The double slash (//) is intentional and required. Terraform uses it to specify subfolders within a Git repo

Виконуємо ще раз terraform init, перевіряємо з plan, деплоїмо, і перевіряємо самі ендпоінти:

І перевіримо таблиці маршрутизації – куди вони ведуть? Наприклад, Route Table atlas-eks-dev-1-27-vpc-intra має три роути:

Префікс-лист pl-63a5400a буде відправляти трафік через ендпоінт vpce-0c6ced56ea4f58b70, тобто atlas-eks-dev-1-27-vpc-s3-ep.

Зміст pl-63a5400a:

І якщо ми зробимо dig на адресу s3.us-east-1.amazonaws.com, то отримаємо:

$ dig s3.us-east-1.amazonaws.com +short
52.217.161.80
52.217.225.240
54.231.195.64
52.216.222.32
16.182.66.224
52.217.161.168
52.217.140.224
52.217.236.168

Адреси з цього листа, тобто всі запити всередені VPC на URL s3.us-east-1.amazonaws.com будуть виконуватись через наш VPC S3 Endpoint.

Забігаючи наперед, коли вже був запущений EKS кластер, то перевірив, як працюють Interface Endpoints, наприклад для STS.

З робочої машини в офісі:

18:46:34 [setevoy@setevoy-wrk-laptop ~]  $ dig sts.us-east-1.amazonaws.com +short
209.54.177.185

Та з Kubernetes Pod в приватній мережі нашої VPC:

root@pod:/# dig sts.us-east-1.amazonaws.com +short
10.1.55.230
10.1.33.247

Тут начебто все.

Можемо переходити до наступної задачі – створення самого кластеру та його WorkerNodes.

Loading

Terraform: цикли count, for_each та for
0 (0)

4 Вересня 2023

Продовжуємо розбиратись з можливостями Terraform.

В попредньому пості познайомились з типами даних – Terraform: знайомство з типами даних – primitives та complex. Тепер подивимось, як ці типи можна використовувати в циклах.

Terraform підтримує три типи циклів:

  • count: самий простий, використовується з заданим числом або з фукнцією length(); використовує індекси list або map для ітерації
    • підходить для створення однакових ресурсів, які не будуть змінюватись
  • for_each: має більше можливостей, використовується з map або set, використовує іммена ключів послідовності для ітерації
    • підходить для створення однотипних ресурсів, але з можливістю задати різні параметри
  • for: використовується для фільтрації та трансмормації об’єктів з lists, sets, tuples або maps; може бути використано разом з такими функціями, як if, join, replace, lower або upper

Terraform count

Отже, count самий базовий і перший метод для виконання задач в циклі.

Аргументом приймає або number, або list чи map, виконує ітерацію, і кожному об’єкту задає індекс відповідвідно до його позиції в послідовності.

Наприклад, ми можемо створити три корзини так:

resource "aws_s3_bucket" "bucket" {
  count = 3

  bucket = "bucket-${count.index}"
}

В результаті Terraform створить масив (array) з трох корзин з іменами bucket-0, bucket-1 та bucket-2.

Ми також можемо передати список і використати функцію length(), щоб отримати кількість елементів в цьому списку, і потім пройтись по кожному з них, використовуючи їхні індекси:

variable "projects" {
  type        = list(string)
  default     = ["test-project-1", "test-project-2", "test-project-3"]
}

resource "aws_s3_bucket" "bucket" {
  count = length(var.projects)

  bucket = "bucket-${var.projects[count.index]}"
}

В такому випадку будуть створені три корзини з іменами “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 видалить коризини для проектів після доданого, бо в списку зміняться індекси об’єктів.

Тобто:

variable "projects" {
  type        = list(string)
  default     = ["test-project-1", "another-test-project", "test-project-2", "test-project-3"]
}

Приведе до:

$ terraform apply
...
  # aws_s3_bucket.bucket[1] must be replaced
-/+ resource "aws_s3_bucket" "bucket" {
...
      ~ bucket                      = "bucket-test-project-2" -> "bucket-another-test-project" # forces replacement
...
  # aws_s3_bucket.bucket[2] must be replaced
-/+ resource "aws_s3_bucket" "bucket" {
...
      ~ bucket                      = "bucket-test-project-3" -> "bucket-test-project-2" # forces replacement
...
  # aws_s3_bucket.bucket[3] will be created
  + resource "aws_s3_bucket" "bucket" {
...
      + bucket                      = "bucket-test-project-3"
...
Plan: 3 to add, 0 to change, 2 to destroy.

І якщо в корзинах є дані, то деплой зупиниться з помилкою BucketNotEmpty, бо Terraform буде намагатись видалити бакети.

Проте count чудово підійде, якщо вам треба перевірити умову на кшталт “створювати ресурс чи ні”. Це можна зробити таким чином:

variable "enabled" {
  type    = bool
  default = true
}

resource "aws_s3_bucket" "bucket" {
  count = var.enabled ? 1 : 0

  bucket = "bucket-test"
}

Тобто якщо 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 ми можемо створити корзини так:

variable "projects" {
  type        = set(string)
  default     = ["test-project-1", "test-project-2", "test-project-3"]
}

resource "aws_s3_bucket" "bucket" {
  for_each  = var.projects

  bucket = "bucket-${each.value}"
}

Або з variable з типом list і toset() для for_each:

variable "projects" {
  type        = list(string)
  default     = ["test-project-1", "test-project-2", "test-project-3"]
}

resource "aws_s3_bucket" "bucket" {
  for_each  = toset(var.projects)

  bucket = "bucket-${each.value}"
}

Але так як в результаті ми отримаємо не масив даних, а map з окремими об’єктами:

...
  # aws_s3_bucket.bucket["test-project-1"] will be created
...

І тоді в outputs просто викликати aws_s3_bucket.bucket[*].id ни вийде.

Натомість, ми можемо використати функцію values() щоб отримати всі значення ресурсів aws_s3_bucket.bucket:

...
output "bucket_names" {
  value       = values(aws_s3_bucket.bucket)[*].id 
}

for_each з map

Або приклад з map для створення тегу Name:

variable "projects" {
  type  = map(string)
  default = {
    "test-project-1" = "Test Project 1",
    "test-project-2" = "Test Project 2",
    "test-project-3" = "Test Project 3",
  }
}

resource "aws_s3_bucket" "bucket" {
  for_each  = var.projects

  bucket   = "bucket-${each.key}"
  tags = {
    "Name" = each.value
  }
}

Або з використанням merge(), щоб додавати загальні теги + тег Name (див. також default_tags):

variable "projects" {
  type  = map(string)
  default = {
    "test-project-1" = "Test Project 1",
    "test-project-2" = "Test Project 2",
    "test-project-3" = "Test Project 3",
  }
}

variable "common_tags" {
  type    = map(string)
  default = {
    "Team"      = "devops",
    "CreatedBy" = "terraform"
  }
}

resource "aws_s3_bucket" "bucket" {
  for_each  = var.projects

  bucket   = "bucket-${each.key}"
  tags  = merge(var.common_tags, {Name = each.value})
}

В результаті отримаємо три теги:

...
  ~ resource "aws_s3_bucket" "bucket" {
        id                          = "bucket-test-project-1"
      ~ tags                        = {
          + "CreatedBy" = "terraform"
          + "Name"      = "Test Project 1"
          + "Team"      = "devops"
        }
...

for_each з map of maps та атрибутами

Або можна використати навіть map of maps, і для кожної корзини передавати набір параметрів, і потім звертатись до параметра через each.value.PARAM_NAME.

Наприклад, в одному параметрі задамо тег Name, а в іншому – object_lock_enabled:

variable "projects" {
  type  = map(map(string))
  default = {
    "test-project-1" = {
      tag_name = "Test Project 1", object_lock_enabled = true 
    },
    "test-project-2" = {
      tag_name = "Test Project 2", object_lock_enabled = false
    },
    "test-project-3" = {
      tag_name = "Test Project 3", object_lock_enabled = false
    }
  }
}

variable "common_tags" {
  type    = map(string)
  default = {
    "Team"      = "devops",
    "CreatedBy" = "terraform"
  }
}

resource "aws_s3_bucket" "bucket" {
  for_each  = var.projects

  bucket   = "bucket-${each.key}"
  object_lock_enabled = each.value.object_lock_enabled
  tags  = merge(var.common_tags, {Name = each.value.tag_name})
}

Результат:

Terraform for

На відміну від 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 зі значеннями:

...
common_tags = [
  "Key: CreatedBy; Value: terraform;",
  "Key: Team; Value: devops;",
]

А за допомогою  => можемо перетворити list на map. Крім того, для map замість [] цикл записуємо в {}:

output "common_tags" {
  value       = { for a, b in var.common_tags : upper(a) => b }
}

Отримуємо:

...
common_tags = {
  "CREATEDBY" = "terraform"
  "TEAM" = "devops"
}

for та for_each для ітерації над complex objects

Можна зробити єдину змінну, яка буде мати різні типи даних для різних значень, а потім виконати ітерацію з for_each та for разом.

Наприклад, створимо variable з типом list, в якому будуть значення типу object, а в object будуть два поля типу string, та одне для списку тегів з типом list:

variable "projects" {
  type        = list(object({
      name = string
      object_lock_enabled = string
      tags = map(string)
  }))

  default = [
    {
      name  = "test-project-1"
      object_lock_enabled = "true"
      tags  =         {
          "Name" = "Test Project 1"
          "Team"      = "devops"
          "CreatedBy" = "terraform"          
        }
    },
    {
      name  = "test-project-2",
      object_lock_enabled = true,
      tags  =         {
          "Name" = "Test Project 2",
          "Team"      = "devops",
          "CreatedBy" = "terraform"          
        }
    },
    {
      name  = "test-project-3",
      object_lock_enabled = true,
      tags  =         {
          "Name" = "Test Project 3",
          "Team"      = "devops",
          "CreatedBy" = "terraform"          
        }
      
    }        
  ]
}

resource "aws_s3_bucket" "bucket" {
  for_each  = { for a in var.projects : a.name => a }

  bucket   = "bucket-${each.key}"
  object_lock_enabled = each.value.object_lock_enabled
  tags = { for key,value in each.value.tags : key => value }
}

Потім в ресурсі 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
}

В результаті отримаємо:

Changes to Outputs:
  + dynamodb_table_names = [
      + [
          + "atlas-eks-test-dev",
          + "atlas-eks-test-prod",
        ],
      + [
          + "atlas-tf-backends-test-prod",
        ],
    ]

А щоб створити єдиний list замість list[list, list] – можна використати фунцію flatten:

locals {
  table_names = flatten([
    for project, envs in var.projects : [
      for env in envs : 
        "${project}-${env}"
    ]
  ])
}

В результаті отримаємо:

Changes to Outputs:
  + dynamodb_table_names = [
      + "atlas-eks-test-dev",
      + "atlas-eks-test-prod",
      + "atlas-tf-backends-test-prod",
    ]

А щоб побудувати 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
}

Результат:

Changes to Outputs:
  + dynamodb_table_names = {
      + atlas-eks-test-dev          = {
          + env     = "dev"
          + project = "atlas-eks-test"
        }
      + atlas-eks-test-prod         = {
          + env     = "prod"
          + project = "atlas-eks-test"
        }
      + atlas-tf-backends-test-prod = {
          + env     = "prod"
          + project = "atlas-tf-backends-test"
        }
    }

 

for та String Templates

Документація – Strings and Templates.

Синтаксис для ітерації по map буде таким:

%{ for <KEY>, <VALE> in <COLLECTION> }<RESULTED_TEXT>%{ endfor }

Тобто, можемо створити текствий файл зі змістом значень змінної:

resource "local_file" "foo" {
  content  = "%{ for a, b in var.common_tags }Key: ${a}\nValue: ${b}\n%{ endfor }"
  filename = "foo.txt"
}

Результат:

$ cat foo.txt 
Key: CreatedBy
Value: terraform
Key: Team
Value: devops

Готово.

Посилання по темі

Loading

Terraform: знайомство з типами даних – primitives та complex
0 (0)

4 Вересня 2023

В цьому пості трохи подивимось на типи даних, які можемо використовувати в Terraform, щоб простіше розібратись з наступним постом – Terraform: цикли count, for_each та for.

Документація – Type Constraints та Types and Values.

Маємо наступні типи поділені на групи:

  • Primitive Types:
    • string: послідовність Unicode символів, простий текст
    • number: числові значення
    • bool: true або false
  • Complex Types:
    • Collection Types:
      • list: список – тип структури для зберігання простої колекції значень одного типу, доступних по індексам
      • map: колекція key:value значень одного типу
      • set: аналогічна list, але без індексів та сортування
    • Structural Types:
      • object: для зберігання значені різних типів даних – набір іменованих атрибутів, кожен з власним типом даних
      • tuple: послідовність елементів, кожен з власним типом даних, з індексами як у list

Primitive types

Найпростіший тип, в якому можемо зберігати тільки одне значення певного типу.

string

Приклад:

variable "var_string" {
  type        = string
  default     = "a string"
}

output "string" {
  value = var.var_string
}

Результат очевидний:

...
Outputs:

string = "a string"

number

Аналогічно, але для integer значень:

variable "var_number" {
  type        = number
  default     = 1
}

output "number" {
  value = var.var_number
}

Результат:

...
Outputs:

number = 1

bool

Використовується для Conditional Expressions:

variable "var_bool" {
  type        = bool 
  default     = true
}

output "number" {
  value = var.var_bool ? "True" : "False"
}

Результат:

...
Outputs:

number = "True"

Або створення ресурсу, якщо умова дійсна:

resource "local_file" "file" {
  count = var.var_bool ? 1 : 0
  
  filename = "file.txt"
  content = var.var_string
}

Collection Types

list

Послідовність значень одного типу з індексами, починаючи з нуля.

При створенні list можна або не вказувати тип (default == any), або обмежити одним певним типом:

variable "var_list_any" {
  type = list 
  default = ["a string", 10]
}

variable "var_list_string" {
  type = list(string)
  default = ["first string", "second string"]
}

resource "local_file" "file" {
  filename = "file-${var.var_list_any[1]}.txt"

  content = var.var_list_string[0]
}

output "list_any" {
  value = var.var_list_any
}

output "list_string" {
  value = var.var_list_string
}

Результат:

...
Outputs:

list_any = tolist([
  "a string",
  "10",
])
list_string = tolist([
  "first string",
  "second string",
])

І файл:

$ cat file-10.txt 
first string

В list можна використовувати інші типи даних – інші list, map тощо.

При цьому в одному list можуть бути різні типи примітивів (string, number, bool), але однаковий тип для інших типів, тобто:

variable "var_list_any" {
  type = list
  default = ["a", true, 1]
}

variable "var_list_lists" {
  type = list
  default = [
    ["a", "b"],
    ["c", "d"]
  ]
}

output "list_any" {
  value = var.var_list_any
}

output "list_lists" {
  value = var.var_list_lists
}

Результат:

...
Outputs:

list_any = tolist([
  "a",
  "true",
  "1",
])
list_lists = tolist([
  [
    "a",
    "b",
  ],
  [
    "c",
    "d",
  ],
])

Зі списками можемо використовувати цикли, наприклад:

variable "var_list_any" {
  type = list 
  default = ["a string", 10]
}

variable "var_list_string" {
  type = list(string)
  default = ["first string", "second string"]
}

resource "local_file" "file" {
  for_each = toset(var.var_list_any)

  filename = "file-${each.key}.txt"
  content = each.value
}

output "list_string" {
  value = [ for a in var.var_list_string : upper(a)]
}

Результат:

...
Outputs:

list_string = [
  "FIRST STRING",
  "SECOND STRING",
]

Та файли:

$ ls -1
file-10.txt
'file-a string.txt'

$ cat file-a\ string.txt  
a string

map

Значення у формі key:value з доступом до значення по імені ключа:

variable "var_map" {
  type        = map
  default     = {
    "one" = "first",
    "two" = "second"
  }
}

output "map_one" {
  value = var.var_map["one"]
}

output "map_two" {
  value = var.var_map["two"]
}

Також в outputs можемо вивести атрибут, тобто value = var.var_map.one.

Результат:

...
Outputs:

map_one = "first"
map_two = "second"

Також з map можемо використати lookup() для пошуку значення по ключу:

output "map_lookup" {
  value = lookup(var.var_map, "one", "None")
}

Результат:

...
Outputs:

map_lookup = "first"
map_one = "first"
map_two = "second"

Або більш складний приклад – вибір кількості інстансів за ціною в залежності від типу:

variable "instance_cost" {
  type    = map
  default = {
    "t3.medium" = "0.04USD",
    "t3.large" = "0.08USD",
  }
}

variable "instance_number" {
  type    = map
  default = {
    "0.04USD" = 2,
    "0.08USD" = 1,
  }
}

output "instances_count" {
  value = lookup(var.instance_number, var.instance_cost["t3.medium"], 0)
}

Результат:

...
Outputs:

instances_count = 2

map також може включати в себе list або інший map, але всі об’єкти мають бути одного типу (тобто, не можна мати map в якому будуть і list, і другий map):

variable "var_map_of_maps" {
  type        = map
  default     = {
    "out-map-key-1" = {
      "in-map-key-1" = "inner map 1 key one",
      "in-map-key-2" = "inner map 1 inner key two",
    },
    "out-map-key-2" = {
      "in-map-key-1" = "inner map 2 key one",
      "in-map-key-2" = "inner map 2 key two",
    },
  }
}

output "map_of_maps" {
  value = var.var_map_of_maps
}

Результат:

...
Outputs:

map_of_maps = tomap({
  "out-map-key-1" = {
    "in-map-key-1" = "inner map 1 key one"
    "in-map-key-2" = "inner map 1 inner key two"
  }
  "out-map-key-2" = {
    "in-map-key-1" = "inner map 2 key one"
    "in-map-key-2" = "inner map 2 key two"
  }
})

set

Послідовність значень одного або різних типів як в list, але без індексів та сортування:

variable "var_set_any" {
  type    = set(any)
  default = ["string", 1]
}

variable "var_set_string" {
  type    = set(string)
  default = ["string1", "string2"]
}

output "set_any" {
  value = var.var_set_any
}

output "set_string" {
  value = var.var_set_string
}

Результат:

...
...
Outputs:

set_any = toset([
  "1",
  "string",
])
set_string = toset([
  "string1",
  "string2",
])

Як і list або map, set може мати вкладені типи:

variable "var_set_lists" {
  type    = set(list(any))
  default = [
    ["a", "b"],
    ["c", "d"]
  ]
}

output "set_any" {
  value = var.var_set_lists
}

Результат:

...
set_any = toset([
  tolist([
    "a",
    "b",
  ]),
  tolist([
    "c",
    "d",
  ]),
])

Structural Types

object

На відміну від map та list, object є структурним типом, який може мати значення різних типів, в тому числі включати в себе типи list та map.

Схож на Struct в C або Golang:

variable "var_object" {
  type        = object({
    name      = string,
    id        = number,
    data      = list(string)
    data_map  = map(any)
  })

  default = {
    name      = "one",
    id        = 10,
    data      = ["first", "second"],
    data_map  = {
      "one" = "first",
      "two" = "second"
    }
  }
}

output "object" {
  value = var.var_object
}

output "object_map" {
  value = var.var_object.data_map
}

Результат:

...
Outputs:

object = {
  "data" = tolist([
    "first",
    "second",
  ])
  "data_map" = tomap({
    "one" = "first"
    "two" = "second"
  })
  "id" = 10
  "name" = "one"
}
object_map = tomap({
  "one" = "first"
  "two" = "second"
})

tuple

Подібний до object, але з індексами замість імен ключів:

variable "var_tuple" {
  type = tuple ([
    string,
    number,
    list(string),
    map(any)
  ] )

  default = [
    "one",
    10,
    ["first", "second"],
    {
      "one" = "first",
      "two" = "second"
    }
  ]
}

output "tuple" {
  value = var.var_tuple
}

output "tuple_map" {
  value = var.var_tuple[3]
}

Результат:

Outputs:

tuple = [
  "one",
  10,
  tolist([
    "first",
    "second",
  ]),
  tomap({
    "one" = "first"
    "two" = "second"
  }),
]
tuple_map = tomap({
  "one" = "first"
  "two" = "second"
})

В наступному пості подивимось на цикли.

Посилання по темі

Loading

RTFM: День народження – 12 років. Що нового?
0 (0)

2 Вересня 2023

В 2022 із зрозумілих причин пропустив День народження блогу, але давайте у 2023 про це згадаємо.

В цьому році нам вже цілих 12 років! ^_^

День народження рахую з дати реєстрації самого домену:

Domain Name:RTFM.CO.UA
Created On:14-Aug-2011 09:40:17 UTC

Останній День народження записував в пості RTFM: День рождения и годовщина – 10 лет. Обзор изменений, і найголовніше, що змінилось з того часу – це українська мова блогу.

На жаль, більшість постів залишаються на рос. мові, бо перекласти 2000 постів просто фізично не вийде.

Попередні пости про Дні народження:

Хостинг і сам блог

Блог працює на 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, блог виглядав так:

Як я пишу в блог?

Часто питають – як пишу в блог.

Колись напишу про те, як пишу 🙂

Але якщо коротко, то – коли сетаплю щось нове, то накидую в блог покроково те, що роблю, з копіпастою команд з консолі та пару слів про те, що там було.

Потім, як вже є час – то привожу в читабельний вид, і додаю новий пост.

Саме складне, особливо коли знайомишся з якось новою системою – це зрозуміти, про що саме писати, і як це все зібрати до купи та створити структуру нового посту.

Приклад чернетки:

Потім вже на вихідних – роблю переклад на англійську.

RTFM на інших платформах

Англійські пости репостяться на Medium, Dev.to.

В цьому році додались платформи Substack та Hashnode.

Нещодавно створив групу в 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. Між іншим – перший з України.

AWS Hero: анонс June, 2021.

Ось наче й все.

Loading

Terraform: динамічний remote state з AWS S3 та multiple environments по директоріям
0 (0)

31 Серпня 2023

Виходить такая собі серія постів про підготовку до використання Terraform на проекті.

Отже, в першій частині думалось про те, як організувати підготовку backend для проекту, тобто виконати його bootstrap, та трохи – як менеджити Dev/Prod оточення в цілому, див. Terraform: початок роботи та планування нового проекту – Dev/Prod та bootsrap. В другій – як налаштувати State Lock та про remote state в цілому, див. Terraform: remote state з AWS S3 та state locking з DynamoDB.

Рішення по типу 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

Тут будемо використовувати підхід зі створенням бекенду в рамках кожного проекту на старті.

Тобто:

  1. спочатку описуємо створення бакету та таблиці Динамо
  2. створюємо ресурси
  3. налаштовуємо блок terraform.backend{}
  4. імпортуємо стейт
  5. описуємо та створюємо всі інші ресурси

Розділення по 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 таблицю, а далі девелопери вже просто захардкодять їхні імена в свої конфіги.

Створюємо директорії:

[simterm]

$ mkdir -p envs_management_test/{global,environments/{dev,prod},modules/vpc}

[/simterm]

Получаємо таку структуру:

[simterm]

$ tree envs_management_test/
envs_management_test/
├── environments
│   ├── dev
│   └── prod
├── global
└── modules
    └── vpc

[/simterm]

У каталозі envs_management_test/global нам треба описати створення бакету та таблиці для локів.

Тут теж питання: робити одну корзини під кожен енв – чи одну, і стейти в ній розділяти ключами?

Multiple S3 buckets

Якщо робити по корзині на кожен енв, то можна зробити наступним чином:

  • створюємо змінну з типом list, в цей список вносимо імена оточень
  • потім при створенні ресурсів – використовуємо цей список, щоб в циклі пройтись по кожному індексу в ньому

Тобто, variables.tf може бути таким:

variable "environments" {
  description = "Environments names"
  type        = set(string)
  default     = ["dev", "prod", "global"]
}

А у файлі main.tf створюємо ресурси так:

resource "aws_kms_key" "state_backend_kms_key" {
  description             = "This key is used to encrypt bucket objects"
  deletion_window_in_days = 10
}

# create state-files S3 buket 
resource "aws_s3_bucket" "state_backend_bucket" {
  for_each = var.project_names
  bucket = "tf-state-backend-${each.value}"

  # 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
  }
}

# enable S3 bucket versioning
resource "aws_s3_bucket_versioning" "state_backend_versioning" {
  for_each = aws_s3_bucket.state_backend_bucket
  bucket  = each.value.id

  versioning_configuration {
    status = "Enabled"
  }
}
...

Єдиний S3 для оточень

Але щоб не ускладнювати код – поки зробимо одну корзину, а потім для кожного оточення задамо власний key в його backend.

Використаємо змінну для імені:

variable "project_name" {
  description = "The project name to be used in global resources names"
  type        = string
  default     = "envs-management-test"
}

І в main.tf описуємо самі ресурси – тут код той самий, щоб використовувався в попередньому пості:

resource "aws_kms_key" "state_backend_bucket_kms_key" {
  description             = "Encrypt the state bucket objects"
  deletion_window_in_days = 10
}

# create state-files S3 bukets per each Env
resource "aws_s3_bucket" "state_backend_bucket" {
  bucket = "tf-state-bucket-${var.project_name}"

  lifecycle {
    prevent_destroy = true
  }
}

# enable S3 bucket versioning per each Env's bucket
resource "aws_s3_bucket_versioning" "state_backend_bucket_versioning" {
  bucket = aws_s3_bucket.state_backend_bucket.id

  versioning_configuration {
    status = "Enabled"
  }
}

# enable S3 bucket encryption per each Env's bucket
resource "aws_s3_bucket_server_side_encryption_configuration" "state_backend_bucket_encryption" {
  bucket = aws_s3_bucket.state_backend_bucket.id

  rule {
    apply_server_side_encryption_by_default {
      kms_master_key_id = aws_kms_key.state_backend_bucket_kms_key.arn
      sse_algorithm = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

# block S3 bucket public access per each Env's bucket
resource "aws_s3_bucket_public_access_block" "state_backend_bucket_acl" {
  bucket = aws_s3_bucket.state_backend_bucket.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# create DynamoDB table per each Env
resource "aws_dynamodb_table" "state_dynamo_table" {
  name = "tf-state-lock-${var.project_name}"

  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Створюємо ресурси:

[simterm]

$ terraform init && terraform apply

[/simterm]

Налаштування динамічного State Backend

Далі нам треба налаштувати бекенд для global.

Але щоб потім не повторювати один і той самий конфіг для Dev && Prod – загальні параметри бекенду винесемо окремим файлом.

В корні проекту створюємо backend.hcl:

bucket         = "tf-state-bucket-envs-management-test"
region         = "us-east-1"
dynamodb_table = "tf-state-lock-envs-management-test"
encrypt        = true

В директорії global додаємо backend.tf:

terraform {
  backend "s3" {
    key = "global/terraform.tfstate"
  }  
}

Виконуємо ініціалізацію ще раз, та через -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.

В файлі modules/vpc/main.tf описуємо саму VPC:

resource "aws_vpc" "vpc" {
  cidr_block       = var.vpc_cidr

  tags = {
    environment = var.environment
    created-by  = "terraform"
  }   
}

Там же додаємо файл modules/vpc/variables.tf:

variable "vpc_cidr" {
  description = "VPC CIDR"
  type        = string
}

variable "environment" {
  type = string
}

Далі описуємо змінні vpc_cidr та environment в файлах environments/dev/variables.tf та environments/prod/variables.tf:

variable "vpc_cidr" {
  description = "VPC CIDR"
  type        = string
}

variable "environment" {
  type = string
}

У файлі environments/dev/terraform.tfvars значення для них:

vpc_cidr    = "10.0.1.0/24"
environment = "dev"

І в environments/prod/terraform.tfvars інші значення:

vpc_cidr    = "10.0.2.0/24"
environment = "prod"

В обох environments створюємо main.tf, де включаємо модуль VPC:

module "vpc" {
  source      = "../../modules/vpc"
  vpc_cidr    = var.vpc_cidr
  environment = var.environment
}

Додаємо providers.tf аналогічний тому, що маємо в global:

[simterm]

$ cp global/providers.tf environments/dev/
$ cp global/providers.tf environments/prod/

[/simterm]

І в кожному створюємо власний backend.tf, але з різними key.

Dev:

terraform {
  backend "s3" {
    key = "dev/terraform.tfstate"
  }  
}

Та Prod:

terraform {
  backend "s3" {
    key = "prod/terraform.tfstate"
  }  
}

Тепер у нас виходить така структура каталогів та файлів:

І тепер можемо деплоїти ресурси.

Спочатку 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, для тестів?

Тут можемо використати такий флоу:

  1. бранчуємось від мастер-бранчу
  2. робимо свої зміни в коді environments/dev/
  3. ініціалізуємо новий бекенд
  4. і деплоїмо з terraform apply -var з новими значеннями змінних

Ініціалізуємо новий стейт. Додаємо -reconfigure, бо робимо локально, і тут вже є .terraform. У випадку, коли це буде виконуватись з GitHub Actions – директорія буде чистою, і можна виконувати просто init.

У другому параметрі -backend-config передаємо ключ для стейту – в якій директорії корзини зберігати файл:

[simterm]

$ terraform init -reconfigure -backend-config=../../backend.hcl -backend-config="key=pr1111/terraform.tfstate"

[/simterm]

Тепер деплоїмо з -var або передаємо через змінні як TF_VAR_vpc_cidr, див. Environment Variables – в пайплайні це можна досить просто зробити:

[simterm]

$ terraform apply -var vpc_cidr=10.0.3.0/24 -var environment=pr1111

[/simterm]

Перевіряємо стейти – маємо новий каталог pr1111:

[simterm]

 $ aws s3 ls tf-state-bucket-envs-management-test/
                           PRE dev/
                           PRE global/
                           PRE pr1111/

[/simterm]

Готово.

Корисні посилання

Loading

Terraform: remote state з AWS S3 та state locking з DynamoDB
0 (0)

29 Серпня 2023

Готуємось переводити управління інфрастуктурою з AWS CDK на Terraform.

Про планування того, як воно все може виглядати писав у Terraform: початок роботи та планування нового проекту – Dev/Prod та bootsrap, але тоді оминув одну досить важливу опцію – створення lock для state-файлів.

Блокування стейт-файлів використовується для того, щоб уникнути ситуацій, коли запускається кілька інстансів Terraform одночасно – інженерами або автоматично в CI/CD, і вони одночасно будуть намагатись внести зміни в один стейт-файл: при використанні lock, Terraform заблокує запуск іншого інстансу допоки перший інстанс не завершить свою роботу і не звільнить блокування.

У нашому випадку інфрастуктура вся в AWS, тому в ролі бекенду для зберігання стейтів буде використовуватись AWS S3, а для створення lock-файлів – таблиця в DynamoDB.

Документація – State Locking.

Отже, що ми зробимо:

  • таблиця DynamoDB та S3 бакет будуть менеджитись самим Терраформом
  • Terraform буде авторизуватись в AWS з AssumeRole
  • опишемо створення S3 bucket та таблиці DynamoDB
  • створимо ресурси використовуючи локальний стейт
  • імпортуємо локальний стейт в створений бакет
  • протестуємо, як працює State Lock

IAM Role

Terraform буде працювати через окрему IAM Role, див. Use AssumeRole to provision AWS resources across accounts.

Переходимо в IAM > Create Role, вибираємо Custom Trust Policy, і описуємо її:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Statement1",
      "Effect": "Allow",
      "Principal": {
          "AWS": "arn:aws:iam::123456789012:root"
          },
      "Action": "sts:AssumeRole"
    }
  ]
}

Замість 123456789012 вказуємо ID аккаунту, а в root позначаємо, що будь-який аутентифікований IAM User цього аккаунта зможете виконати sts:AssumeRole цієї ролі.

Поки задаємо AdministratorAccess, пізніше можна буде налаштувати права більш детально:

Зберігаємо роль:

Перевіримо, що вона працює.

В свій ~/.aws/config додаємо новий профайл:

[profile tf-admin]
role_arn = arn:aws:iam::492***148:role/tf-admin
source_profile = default

І виконуємо sts get-caller-identity з цим профайлом:

[simterm]

$ aws --profile tf-admin sts get-caller-identity
{
    "UserId": "ARO***ZEF:botocore-session-1693297579",
    "Account": "492***148",
    "Arn": "arn:aws:sts::492***148:assumed-role/tf-admin/botocore-session-1693297579"
}

[/simterm]

Окей, тепер можемо переходити до самого Terraform.

Налаштування Terraform-проекту

Додаємо версії модулів, котрі будемо використовувати.

Останню версію провайдеру AWS можна взяти тут>>>, а версію самого Terraform – тут>>>.

Створюємо файл versions.tf:

terraform {

  required_version = ">= 1.5"

  required_providers {
    aws = { 
      source  = "hashicorp/aws"
      version = ">= 5.14.0"
    }
  }
}

Додаємо файл providers.tf, де описуємо параметри підключення до AWS:

provider "aws" {
  region    = "us-east-1"
  assume_role {
    role_arn = "arn:aws:iam::492***148:role/tf-admin"
  }
}

Створюємо файл main.tf, поки що пустий, і перевіряємо, що Terraform може виконати AssumeRole.

Виконуємо ініціалізцію:

[simterm]

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching ">= 5.14.0"...
- Installing hashicorp/aws v5.14.0...
- Installed hashicorp/aws v5.14.0 (signed by HashiCorp)
...

[/simterm]

То робимо terraform plan:

[simterm]

$ terraform plan

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

[/simterm]

Все добре – Terraform підключився до нашого AWS-аккаунту.

Створення AWS S3 для бекенду

Для корзини, де будуть зберігатись state-файли, потрібно мати:

  1. encryption: для AWS S3 включено по дефолту, але можна налаштувати з власним ключем з AWS KMS
  2. access control: закрити публічний доступ до об’єктів в корзині
  3. versioning: налаштувати версіонування, щоб мати історію змін в стейт-файлах

Створюємо файл backend.tf, і описуємо створення KMS ключа та корзини:

resource "aws_kms_key" "tf_lock_testing_state_kms_key" {
  description             = "This key is used to encrypt bucket objects"
  deletion_window_in_days = 10
}

# create state-files S3 buket 
resource "aws_s3_bucket" "tf_lock_testing_state_bucket" {
  bucket = "tf-lock-testing-state-bucket"

  lifecycle {
    prevent_destroy = true
  }
}

# enable S3 bucket versioning
resource "aws_s3_bucket_versioning" "tf_lock_testing_state_versioning" {
  bucket = aws_s3_bucket.tf_lock_testing_state_bucket.id

  versioning_configuration {
    status = "Enabled"
  }
}

# enable S3 bucket encryption 
resource "aws_s3_bucket_server_side_encryption_configuration" "tf_lock_testing_state_encryption" {
  bucket = aws_s3_bucket.tf_lock_testing_state_bucket.id

  rule {
    apply_server_side_encryption_by_default {
      kms_master_key_id = aws_kms_key.tf_lock_testing_state_kms_key.arn
      sse_algorithm = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

# block S3 bucket public access
resource "aws_s3_bucket_public_access_block" "tf_lock_testing_state_acl" {
  bucket                  = aws_s3_bucket.tf_lock_testing_state_bucket.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Далі, там же додаємо створення DynamoDB таблиці для state lock:

...
# create DynamoDB table
resource "aws_dynamodb_table" "tf_lock_testing_state_ddb_table" {
  name         = "tf-lock-testing-state-ddb-table"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Перевіряємо, чи все правильно описали:

[simterm]

$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_dynamodb_table.tf_lock_testing_state_ddb_table will be created
  + resource "aws_dynamodb_table" "tf_lock_testing_state_ddb_table" {
      + arn              = (known after apply)
      + billing_mode     = "PAY_PER_REQUEST"
      + hash_key         = "LockID"
...

Plan: 5 to add, 0 to change, 0 to destroy.

[/simterm]

І виконуємо terraform apply, щоб створити ресурси:

[simterm]

$ terraform apply
...
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes
...
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

[/simterm]

Перевіряємо корзину:

Та таблицю DynamoDB:

Налаштування Terraform Backend та State Lock

Тепер можемо додати бекенд з параметром dynamodb_table для створення lock.

До файлу backend.tf додаємо блок terraform.backend.s3:

terraform {
  backend "s3" {
    bucket         = "tf-lock-testing-state-bucket"
    key            = "tf-lock-testing-state-bucket.tfstate"
    region         = "us-east-1"
    dynamodb_table = "tf-lock-testing-state-ddb-table"
    encrypt        = true
  }  
}
...

Виконуємо terraform init ще раз, та імпортуємо локальний state в корзину:

[simterm]

$ terraform init

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

Releasing state lock. This may take a few moments...

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]

Перевіряємо DynamoDB тепер – маємо ключ:

І стейт в S3:

Якщо переглянути таблицю DynamoDB під час виконання plan чи apply – можна побачити сам lock з полями Operation та хто саме виконує операцію:

Тестування State Lock

Додаємо файл main.tf с ресурсом EC2:

resource "aws_instance" "ec2_lock_test" {
    ami = "ami-0d2fcfe4f5c4c5b56"
    instance_type = "t2.micro"
    tags = {
      Name = "EC2 Instance with remote state"
    }
}

Копіюємо всі файли проекту в новий каталог:

[simterm]

$ mkdir test-lock
$ cp -r * test-lock/
cp: cannot copy a directory, 'test-lock', into itself, 'test-lock/test-lock'

[simterm]

В поточному каталозі запускаємо terraform apply, але не відповідаємо yes, щоб створений в DynamoDB lock залишався:

[simterm]

$ terraform apply 
Acquiring state lock. This may take a few moments...
...

[/simterm]

Переходимо в другий каталог, і там запускаємо init та apply ще раз:

[simterm]

$ cd test-lock/
$ terraform init && terraform apply
...
Acquiring state lock. This may take a few moments...
╷
│ Error: Error acquiring the state lock
│ 
│ Error message: ConditionalCheckFailedException: The conditional request failed
│ Lock Info:
│   ID:        98dd894b-065f-8f63-f695-d4dcea702807
│   Path:      tf-lock-testing-state-bucket/tf-lock-testing-state-bucket.tfstate
│   Operation: OperationTypeApply
...

[/simterm]

Та маємо помилку створення блокування, бо вже є процесс, який користується нашим state-файлом.

Terraform State Lock trics

force-unlock

Іноді буває, що Terraform не звільняє lock, наприклад, якщо при виконанні операції відвалився інтернет.

Тоді можемо звільти стейт за допомогою force-unlock, якому передаємо Lock ID:

[simterm]

$ terraform force-unlock 98dd894b-065f-8f63-f695-d4dcea702807
Do you really want to force-unlock?
  Terraform will remove the lock on the remote state.
...
  Enter a value: yes

Terraform state has been successfully unlocked!

[/simterm]

lock-timeout

Іноді треба, щоб Terraform не зупиняв роботу, як тільки побачить, що lock-запис вже є. Наприклад, в CI-пайплайні можуть бути одночасно запущені дві джоби, і тоді друга запиниться з полмилкою.

В такому випадку можемо додати lock-timeout – тоді Terraform зачекає заданий період часу, і спробує виконати lock ще раз:

[simterm]

$ terraform apply -lock-timeout=180s

[/simterm]

Готово.

Loading

VictoriaMetrics: VMAuth – проксі, аутентифікація та авторизація
5 (1)

23 Серпня 2023

Продовжуємо розвивати наш стек моніторингу. Див. VictoriaMetrics: створення Kubernetes monitoring stack з власним Helm-чартом.

Що хочеться: зробити доступ девелоперам, щоб вони могли в Alertmanager самі виставляти Silence для алертів аби не спамити в Slack, див. Prometheus: Alertmanager Web UI и Silence алертов.

Для того, щоб забезпечити безпечний доступ до нього можна використати рішення від VictoriaMetrics – компонент VMAuth, який дозволяє створити єдиний ендпоінт, через який будуть ходити всі юзери і налаштувати відповідні бекенди для інших компонентів кластеру.

Кратко – що можна з VMAuth:

  • створити єдину точку входу для сервісів з Basic або Bearer user аутентифікацією та авторизацією
  • в залежності від юзера та роута/URI направляти його до відповідного сервіса (фактично, ви можете створити один Ingress і всі запити обслуговувати через нього замість того, щоб створювати Ingress та аутентифікацію для кожного сервіса окремо)
  • мати простий round-robin load balancer
  • налаштувати IP фільтри з Allow та Deny листами
  • керувати додаванням власних хедерів до запитів

Деплоїти будемо у AWS EKS з Helm-чарту victoria-metrics-auth, але можна робити через yaml-маніфести, див. документацію та інші приклади на Authorization and exposing components та VMAuth.

Встановлення чарту VMAuth

Так як ми маємо umbrella-chart, то додаємо в Chart.yaml в блок dependecy новий сабчарт:

...
- name: victoria-metrics-auth
  version: ~0.3.3
  repository: https://victoriametrics.github.io/helm-charts/ 
...

Дефолтні вальюси – values.yaml.

У власних values.yaml описуємо конфіг VMAuth – створення Ingress, ім’я користувача, пароль, та куди перенаправляти його запити – тут це буде Kubernetes Service для Alertmanager:

...
victoria-metrics-auth:
  ingress:
    enabled: true
    annotations:
      kubernetes.io/ingress.class: alb
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:49***148:certificate/66e3050e-7f27-4f0c-8ad4-0733a6d8071a
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=600    
    hosts:
      - name: vmauth.dev.example.co
        path: /
        port: http
  config:
    users:
      - username: "vmadmin"
        password: "p@ssw0rd"
        url_prefix: "http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093"
...

Вновлюмо Helm dependency:

[simterm]

$ helm dependency update

[/simterm]

І деплоїмо чарт:

[simterm]

$ helm -n dev-monitoring-ns upgrade --install atlas-victoriametrics . -f values/dev/atlas-monitoring-dev-values.yaml

[/simterm]

Перевіряємо чи додався Ingress і AWS ALB до нього:

[simterm]

$ kk -n dev-monitoring-ns get ingress
NAME                                          CLASS    HOSTS                   ADDRESS                   PORTS   AGE
atlas-victoriametrics-victoria-metrics-auth   <none>   vmauth.dev.example.co   k8s-***elb.amazonaws.com  80      3m12s

[/simterm]

Чекаємо поки оновляться DNS, і відкриваємо https://vmauth.dev.example.co:

Логінимось, і попадаємо прямо в Алертменеджер:

Конфіг в Kubernetes Secret

Замість того, щоб тримати конфіг в values чарту можно створити Kubernetes Secret. Це додатково дасть можливість передавати пароль, якщо він у вас один, через helm install --set:

apiVersion: v1
kind: Secret
metadata:
  name: vmauth-config-secret
stringData:
  auth.yml: |-
    users:
      - username: vmadmin
        password: {{ .Values.vmauth_password }}
        url_map:
        url_prefix: http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093/

VMAuth, users та routes

Є можливість створити одного користувача, і з url_map йому налаштувати кілька роутів – в залежності від URI запиту, він буде перенаправлений на відповідний бекенд, а з default_url задати URL, куди будуть перенаравлені запроси, для яких не задано роута. При цьому в роутах можна використовувати регулярки.

Наприклад:

...
    users:
      - username: vmadmin
        password: {{ .Values.vmauth_password }}
        url_map:
        - src_paths:
          - /alertmanager.*
          url_prefix: http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093/
        - src_paths:
          - /vmui.*
          url_prefix: http://vmsingle-vm-k8s-stack.dev-monitoring-ns.svc:8429
        default_url:
          - https://google.com

Якщо плануєте додавати доступ до інстансу VMSingle – додайте блок для Prometheus, бо інакше будуть помилки виду:

{“ts”:”2023-08-22T14:37:43.363Z”,”level”:”warn”,”caller”:”VictoriaMetrics/app/vmauth/main.go:159″,”msg”:”remoteAddr: \”10.0.0.74:25806, X-Forwarded-For: 217.***.***.253\”; requestURI: /prometheus/vmui/custom-dashboards; missing route for \”/prometheus/v
mui/custom-dashboards\””}
{“ts”:”2023-08-22T14:37:43.396Z”,”level”:”warn”,”caller”:”VictoriaMetrics/app/vmauth/main.go:159″,”msg”:”remoteAddr: \”10.0.0.74:25806, X-Forwarded-For: ***.***.165.253\”; requestURI: /prometheus/api/v1/label/__name__/values; missing route for \”/promet
heus/api/v1/label/__name__/values\””}

Для Prometheus блок виглядає аналогічно:

...
      - src_paths:
        - /prometheus.*
        url_prefix: http://vmsingle-vm-k8s-stack.dev-monitoring-ns.svc:8429

Для того, щоб сам Alertmanager працював через URI /alertmanager – в його values налаштовуємо routePrefix:

...
  alertmanager:
    enabled: true 
    spec:
      configSecret: "alertmanager-config"
      routePrefix: "/alertmanager"
...

І не забудьте в такому випадку змінити дефолтний URL для VMAlert у його values:

...
  vmalert:
    annotations: {}
    enabled: true
    spec:  
      notifier:
        url: "http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093/alertmanager"
...

Деплоїмо зміни, а щоб застосувати зміни конфіг в самому інстансі VMAuth, виконуємо запит до ендпоінту /-/reload, тобто https://vmauth.dev.example.co/-/reload.

Тепер Alertmanager доступний за адресою https://vmauth.dev.example.co/alertmanager:

Насправді, настройка src_paths може бути трохи геморною, бо, наприклад, в документації роути вказані просто як /uri/path:

url_map:
- src_paths:
  - /api/v1/query
  - /api/v1/query_range

Але коли я почав це робити, то виявилось, що при виконанні редіректу з VMAuth на внутрішній сервіс в кінці додається зайвий слеш, і доступ до Alertmanager не працював.

Саме тому в моїх прикладах вище роути задані з “.*“.

Години дві спілкувався з саппортом в VictoriaMetrcis Slack, намагались знайти причину проблем з доступом к Alertmanager, наче знайшли, завів GitHub issue, подивимось, як воно буде далі.

Взагалі, підтримку VictoriaMetrics варто згадати окремо, бо працює вона чудово і досить швидко. Є Slack, є Telegram-канал.

Basic Auth vs Bearer token

Замість звичайного логіна:пароля можемо використати ServiceAccount токен.

Створюємо ServiceAccount та Secret для нього з типом kubernetes.io/service-account-token:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: vmauth-sa
  namespace: dev-monitoring-ns
secrets:
- name: vmauth-token-secret
---
apiVersion: v1
kind: Secret
metadata:
  name: vmauth-token-secret
  namespace: dev-monitoring-ns
  annotations:
    kubernetes.io/service-account.name: vmauth-sa
type: kubernetes.io/service-account-token

Деплоїмо, отримуємо токен для цього ServicAccount:

[simterm]

$ kk -n dev-monitoring-ns create token vmauth-sa 
eyJhbGciOi***gfeNGWVjJn5-LWd2aslxAwnUTpQ

[/simterm]

Додаємо bearer_token в конфіг VMAuth:

...
    users:
    - username: vmadmin
      password: {{ .Values.vmauth_password }}
      url_map:
      - src_paths:
        - /alertmanager.*
        url_prefix: http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093
      - src_paths:
        - /vmui.*
        url_prefix: http://vmsingle-vm-k8s-stack.dev-monitoring-ns.svc:8429
      - src_paths:
        - /prometheus.*
        url_prefix: http://vmsingle-vm-k8s-stack.dev-monitoring-ns.svc:8429
    - bearer_token: "eyJhbGciOiJSUzI1NiIsImtpZ***gfeNGWVjJn5-LWd2aslxAwnUTpQ"
      url_prefix: http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093

Деплоїмо, знов робимо /-/reload, та перевіряємо доступ.

Заносимо токен в змінну:

[simterm]

$ token="eyJhbGciOiJSUzI1NiIsImt***-LWd2aslxAwnUTpQ"wnUTpQ

[/simterm]

І з curl відкриваємо ендпоінт:

[simterm]

$ curl -H "Authorization: Bearer ${token}" https://vmauth.dev.example.co/
<a href="/alertmanager">Found</a>.

[/simterm]

VMAuth та “AnyService”

Ну і на останнє – VMAuth можна використовувати для аутентифікації не тільки VictoriaMetrics та її сервісів, а (майже) будь-яких.

Наприклад, маємо под з Nginx Demo:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  labels:
    app: my-pod
spec:
  containers:
    - name: my-container
      image: nginxdemos/hello
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-pod
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

У VMAuth додаємо роут:

...
      - src_paths:
        - /nginxdemo.*
        url_prefix: http://my-service.default.svc:80

І тепер за адресою https://vmauth.dev.example.co/nginxdemo попадаемо на Nginx:

А от для стандартної Kubernetes Dashboard так не вийде, бо вона використовує self-signed TLS сертифиікат, і VMAuth не підключається до відповідного сервісу, бо не може провалідувати сертификат. Можливо, є рішення, але не шукав, бо в принципі не потрібно.

VMAuth Self-Security

Див. документацію.

Закрийте аутентифікацією “службові” роути самого VMAuth.

У values.yaml додаємо ключі:

...
  extraArgs:
    reloadAuthKey: password
    flagsAuthKey: password
    metricsAuthKey: password
    pprofAuthKey: password
...

Деплоїмо, і тепер якщо викликати /-/reload без ключа – буде помилка:

[simterm]

$ curl https://vmauth.dev.example.co/-/reload
The provided authKey doesn't match -reloadAuthKey

[/simterm]

Щоб передати ключ для аутентифікації – використовуємо форму /-/reload?authKey=password:

[simterm]

$ curl -I https://vmauth.dev.example.co/-/reload?authKey=password
HTTP/2 200 

[/simterm]

Поки що наче немає можливості передачи ключі через Kubertes Secret, тільки хардкодити у values.yaml, але вже є фіча-реквест.

Loading

AWS: знайомство з Karpenter для автоскейлінгу в EKS, та встановлення з Helm-чарту
0 (0)

18 Серпня 2023

На всіх попередніх проектах, де був Kubernetes я використовував AWS Elastic Kubernetes Service, а для скейлінгу його WorkerNodes – Cluster Autoscaler (CAS), бо в принципі інших варіантів раньше не було.

В цілому, CAS працював добре, проте в листопаді 2020 AWS випустив власне рішення для скейлінгу нод для EKS – Karpenter, і якщо спочатку відгуки були неоднозначні, то останні його версії дуже хвалять, а тому вирішив на новому проекті спробувати його.

Karpenter overview та Karpenter vs Cluster Autoscaler

Отже, що таке Karpenter? Це автоскейлер, який запускає нові WorkerNodes, коли Kubernetes має поди, які не може запустити через нестачу ресурсів на існуючих WorkerNodes.

На відміну від CAS, він вміє автоматично вибирати найбільш відповідний тип інстансу в залежності від потреб подів, які треба запустити.

Крім того, він може керувати подами на нодах, щоб оптимізувати їх розміщення по серверам для того, щоб виконати де-скейлінг WorkerNodes, які можна зупинити для оптимізації вартості кластеру.

Ще з приємних можливостей це те, що на відміну від CAS вам не потрібно створювати декілька WorkerNodes groups з різними типами інстансів – Karpenter сам може визначити необхідний для поду/ів тип ноди, і створити нову ноду – більше ніяк мук вибора “Managed чи Self-managed нод-групи” – ви просто описуєте конфигурацію того, які типи інстасів можна використовувати, і Karpenter сам створить ту ноду, яка потрібна для кожного нового поду.

Фактично, ви взагалі лишаєте осторонь потребу у взаємодії з AWS по менеджменту EC2 – це все бере на себе єдиний компонент, Karpenter.

Також, Karpenter вміє обробляти Terminating та Stopping Events на ЕС2, і переміщати поди з нод, які будуть зупинені – див. native interruption handling.

Karpenter Best Practices

Повний список є на сторінці Karpenter Best Practices, рекомендую його проглянути. Там же є й EKS Best Practices Guides – теж цікаво ознайомитись.

Тут тезісно основні корисні поради:

  • Керучий под Karpenter треба запускати або у Fargate, або на звичайній ноді з Autoscale NodeGroup (скоріш за все, я буду створювати одну звичайну ASG для всіх крітікал-сервісів с лейблою типу “critcal-addons” – Karpenter, aws-load-balancer-controller, coredns, ebs-csi-controller, external-dns, etc.)
  • налаштуйте Interruption Handling – тоді Karpeneter буде переносити існуючі поди з ноди, яку буде видалено або запинено Амазоном
  • якщо Kubernetes API не доступен ззовні (а так і має бути), то налаштуйте AWS STS VPC endpoint для VPC кластеру
  • створіть різні provisioners для різних команд, які користуються різними типами інстансів (наприклад, для Bottlerocket та Amazon Linux)
  • налаштуйте consolidation для ваших provisioners – тоді Karpeneter буде намагатись переміщати запущені поди на існучі ноди, або на меншу ноду, яка буде дешевше існуючої
  • використовуйте Time To Live для нод, створених Karpenter, щоб видаляти ноди, які не використовуються, див. How Karpenter nodes are deprovisioned
  • додавайте аннотацію karpenter.sh/do-not-evict для подів, які небажано зупиняти – тоді Karpenter не буде видялти ноду, на якій такі поди запущені навіть після закінчення TTL цієї ноди
  • використовуйте Limit Ranges для налаштування дефолтних обмежень на resources подів

Виглядає все досить цікаво – давайте пробувати запускати його.

Встановлення Krapenter

Будемо використовувати Krapenter Helm-чарт.

Пізніше зробимо нормально, через автоматизацію, поки що для знайомства – руками.

AWS IAM

KarpenterInstanceNodeRole Role

Переходимо в АІМ Roles, створюємо нову роль для менеджменту WorkerNodes:

Додаємо Amazon-managed полісі:

  • AmazonEKSWorkerNodePolicy
  • AmazonEKS_CNI_Policy
  • AmazonEC2ContainerRegistryReadOnly
  • AmazonSSMManagedInstanceCore

Зберігаємо як KarpenterInstanceNodeRole:

KarpenterControllerRole Role

Додаємо другу роль – для самого Karpenter, тут політику описуємо самі у JSON.

Переходимо у IAM > Policies, створюємо власну полісі:

{
    "Statement": [
        {
            "Action": [
                "ssm:GetParameter",
                "iam:PassRole",
                "ec2:DescribeImages",
                "ec2:RunInstances",
                "ec2:DescribeSubnets",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeLaunchTemplates",
                "ec2:DescribeInstances",
                "ec2:DescribeInstanceTypes",
                "ec2:DescribeInstanceTypeOfferings",
                "ec2:DescribeAvailabilityZones",
                "ec2:DeleteLaunchTemplate",
                "ec2:CreateTags",
                "ec2:CreateLaunchTemplate",
                "ec2:CreateFleet",
                "ec2:DescribeSpotPriceHistory",
                "pricing:GetProducts"
            ],
            "Effect": "Allow",
            "Resource": "*",
            "Sid": "Karpenter"
        },
        {
            "Action": "ec2:TerminateInstances",
            "Condition": {
                "StringLike": {
                    "ec2:ResourceTag/Name": "*karpenter*"
                }
            },
            "Effect": "Allow",
            "Resource": "*",
            "Sid": "ConditionalEC2Termination"
        }
    ],
    "Version": "2012-10-17"
}

Зберігаємо як KarpenterControllerPolicy:

Створюємо другу IAM Role з цією політикою.

IAM OIDC identity provider вже повинні мати, якщо нема – то йдемо у документацію Creating an IAM OIDC provider for your cluster.

На початку створення ролі у Select trusted entity вибираємо Web Identity, а в Identity provider – OpenID Connect provider URL нашого кластеру. В Audience вибираємо sts.amazonaws.com:

Далі, підключаємо політику, яку робили вище:

Зберігаємо як KarpenterControllerRole.

Trusted Policy має виглядати так:

IAM Service Account з ролью KarpenterControllerRole буде створено самим чартом.

Security Groups та Subnets tags для Karpenter

Далі треба додати тег Key=karpenter.sh/discovery,Value=${CLUSTER_NAME} до SecurityGroups та Subnets, які використовуються існуючими WorkerNodes, і в яких потім Karpenter буде створювати нові.

В How do I install Karpenter in my Amazon EKS cluster? є приклад, як це зробити двома командами, але я як завжди перший раз вважаю за краще зробити це руками.

Знаходимо SecurityGroups та Subnets нашої WorkerNode AutoScaling Group – вона у нас зараз одна, тож це буде просто:

Додаємо теги:

Повторюємо для Subnets.

aws-auth ConfigMap

Додаємо в aws-auth новую роль для майбутніх WorkerNodes, щоб вони могли приєднатися до кластеру.

Див. Enabling IAM principal access to your cluster.

Бекапимо ConfigMap:

[simterm]

$ kubectl -n kube-system get configmap aws-auth -o yaml > aws-auth-bkp.yaml

[/simterm]

Редагуємо її:

[simterm]

$ kubectl -n kube-system edit configmap aws-auth

[/simterm]

В блок mapRoles додаємо новий мапінг – нашої ролі для WorkerNodes до RBAC-груп system:bootstrappers та system:nodes, в rolearn вказуємо IAM роль KarpenterInstanceNodeRole, яку робили для майбутніх WorkerNodes:

...
- groups:
  - system:bootstrappers
  - system:nodes
  rolearn: arn:aws:iam::492***148:role/KarpenterInstanceNodeRole
  username: system:node:{{EC2PrivateDNSName}}
...

В мене чомусь додано однією строкою, можливо, це кривий CDK криво створив, бо з eksctl наскільки пам’ятаю створювалось нормально:

Перепишемо трохи, і додаємо новий мапінг.

Будьте тут уважні, бо можна розвалити кластер. В Production такого руками краще не робити – це все повинно бути в коді автоматизації Terraform/CDK/Pulumi/etc:

Перевіряємо, що не зламали доступи – глянемо ноди:

[simterm]

$ kk get node
NAME                         STATUS   ROLES    AGE   VERSION
ip-10-0-2-173.ec2.internal   Ready    <none>   28d   v1.26.4-eks-0a21954
ip-10-0-2-220.ec2.internal   Ready    <none>   38d   v1.26.4-eks-0a21954
...

[/simterm]

Працює? ОК.

Встановлення Karpenter Helm chart

В How do I install Karpenter in my Amazon EKS cluster? знов пропонується якесь збочення с helm template, хоча робоче.

Ми просто створимо власний values.yaml – це буде корисно для майбутньої автоматизації, де задамо nodeAffinity та інші параметри для чарту.

Дефолтний values самого чарту – тут>>>.

Перевіряємо labels нашої ноди:

[simterm]

$ kk get node ip-10-0-2-173.ec2.internal -o json | jq -r '.metadata.labels."eks.amazonaws.com/nodegroup"'
EKSClusterNodegroupNodegrou-zUKXsgSLIy6y

[/simterm]

В своєму файлі values.yaml описуємо affinity – першу частину не міняємо, в другій – в key=eks.amazonaws.com/nodegroup задаємо ім’я нод-групи, EKSClusterNodegroupNodegrou-zUKXsgSLIy6y:

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: karpenter.sh/provisioner-name
          operator: DoesNotExist
      - matchExpressions:
        - key: eks.amazonaws.com/nodegroup
          operator: In
          values:
          - EKSClusterNodegroupNodegrou-zUKXsgSLIy6y

В serviceAccount додаємо аннотацію з ARN нашої IAM-ролі KarpenterControllerRole:

...
serviceAccount:
  create: true
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::492***148:role/KarpenterControllerRole

Додаємо блок settings – тут в принципі все зрозуміло з назв параметрів.

Єдине, що в defaultInstanceProfile задаємо не повний ARN ролі, а тільки її ім’я:

...
settings:
  aws:
    clusterName: eks-dev-1-26-cluster
    clusterEndpoint: https://2DC***124.gr7.us-east-1.eks.amazonaws.com
    defaultInstanceProfile: KarpenterInstanceNodeRole

Тепер ми готові к деплою.

Знаходимо актуальну версію Karpenter на сторінці релізів.

Так як деплоїмо для тесту, то можна взяти останню на сьогодні – v0.30.0-rc.0.

Деплоїмо з Helm OCI registry:

[simterm]

$ helm upgrade --install --namespace dev-karpenter-system-ns --create-namespace -f values.yaml karpenter oci://public.ecr.aws/karpenter/karpenter --version v0.30.0-rc.0 --wait

[/simterm]

Перевіряємо поди:

[simterm]

$ kk -n dev-karpenter-system-ns get pod
NAME                         READY   STATUS    RESTARTS   AGE
karpenter-78f4869696-cnlbh   1/1     Running   0          44s
karpenter-78f4869696-vrmrg   1/1     Running   0          44s

[/simterm]

Ок, все є.

Створення Default Provisioner

Тепер ми можемо починати налаштовувати автоскейлінг.

Для цього першим додаємо Provisioner, див. Create Provisioner.

В ресурсі Provisioner описуємо які типи EC2-інстансів використовувати, у providerRef задаємо значення імені ресурсу AWSNodeTemplate, у consolidation – включаємо переміщення подів для оптимізації використання WorkerNodes.

Всі параметри є у Provisioners – дуже корисно їх подивитись.

Готові приклади є в репозиторії – examples/provisioner.

В ресурсі AWSNodeTemplate описується де саме створювати нові ноди – по тегу karpenter.sh/discovery=eks-dev-1-26-cluster, який ми завали раніше на SecurityGroups та Subnets.

Всі параметри для AWSNodeTemplate є у Node Templates.

Отже, що треба:

  • використовувати тільки T3 small, medium або large
  • тільки в AvailabilityZone us-east-1a та us-east-1b

Створюємо маніфест:

apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec: 
  requirements:
    - key: karpenter.k8s.aws/instance-family
      operator: In
      values: [t3]
    - key: karpenter.k8s.aws/instance-size
      operator: In
      values: [small, medium, large]
    - key: topology.kubernetes.io/zone
      operator: In
      values: [us-east-1a, us-east-1b]
  providerRef:
    name: default
  consolidation: 
    enabled: true
  ttlSecondsUntilExpired: 2592000
  ttlSecondsAfterEmpty: 30
---
apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: default
spec:
  subnetSelector:
    karpenter.sh/discovery: eks-dev-1-26-cluster
  securityGroupSelector:
    karpenter.sh/discovery: eks-dev-1-26-cluster

Створюємо ресурси:

[simterm]

$ kk -n dev-karpenter-system-ns apply -f provisioner.yaml 
provisioner.karpenter.sh/default created
awsnodetemplate.karpenter.k8s.aws/default created

[/simterm]

Перевірка роботи автоскейлінгу з Karpenter

Щоб перевірити що все працює – можна заскейлити існуючу NodeGroup, видаливши з неї вілька EC2-інстансів.

В цьому Kubenetes зараз працює nskmrb наш моніторинг – трохи поломаємо його ^-)

Міняємо параметриAutoScale Group:

Або створити Deployment, подам якого задати багато requests і кількість replicas:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 50
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-container
          image: nginx
          resources:
            requests:
              memory: "2048Mi"
              cpu: "1000m"
            limits:
              memory: "2048Mi"
              cpu: "1000m"
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: my-app

Дивимось логи Karpenter – створено новий інстанс:

[simterm]

2023-08-18T10:42:11.488Z        INFO    controller.provisioner  computed 4 unready node(s) will fit 21 pod(s)   {"commit": "f013f7b"}
2023-08-18T10:42:11.497Z        INFO    controller.provisioner  created machine {"commit": "f013f7b", "provisioner": "default", "machine": "default-p7mnx", "requests": {"cpu":"275m","memory":"360Mi","pods":"9"}, "instance-types": "t3.large, t3.medium, t3.small"}
2023-08-18T10:42:12.335Z        DEBUG   controller.machine.lifecycle    created launch template {"commit": "f013f7b", "machine": "default-p7mnx", "provisioner": "default", "launch-template-name": "karpenter.k8s.aws/15949964056112399691", "id": "lt-0288ed1deab8c37a7"}
2023-08-18T10:42:12.368Z        DEBUG   controller.machine.lifecycle    discovered launch template      {"commit": "f013f7b", "machine": "default-p7mnx", "provisioner": "default", "launch-template-name": "karpenter.k8s.aws/10536660432211978551"}
2023-08-18T10:42:12.402Z        DEBUG   controller.machine.lifecycle    discovered launch template      {"commit": "f013f7b", "machine": "default-p7mnx", "provisioner": "default", "launch-template-name": "karpenter.k8s.aws/15491520123601971661"}
2023-08-18T10:42:14.524Z        INFO    controller.machine.lifecycle    launched machine        {"commit": "f013f7b", "machine": "default-p7mnx", "provisioner": "default", "provider-id": "aws:///us-east-1b/i-060bca40394a24a62", "instance-type": "t3.small", "zone": "us-east-1b", "capacity-type": "on-demand", "allocatable": {"cpu":"1930m","ephemeral-storage":"17Gi","memory":"1418Mi","pods":"11"}}

[/simterm]

Та за хвилину перевіряємо ноди в кластері:

[simterm]

$ kk get node
NAME                         STATUS   ROLES    AGE     VERSION
ip-10-0-2-183.ec2.internal   Ready    <none>   6m34s   v1.26.6-eks-a5565ad
ip-10-0-2-194.ec2.internal   Ready    <none>   19m     v1.26.4-eks-0a21954
ip-10-0-2-212.ec2.internal   Ready    <none>   6m38s   v1.26.6-eks-a5565ad
ip-10-0-3-210.ec2.internal   Ready    <none>   6m38s   v1.26.6-eks-a5565ad
ip-10-0-3-84.ec2.internal    Ready    <none>   6m36s   v1.26.6-eks-a5565ad
ip-10-0-3-95.ec2.internal    Ready    <none>   6m35s   v1.26.6-eks-a5565ad

[/simterm]

Або в AWS Console по тегу karpenter.sh/managed-by:

Готово.

Що лишилось зробити:

  • для дефолтної Node Group, яка створюється з кластером з AWS CDK додати тег critical-addons=true та tains на NoExecute і NoSchedule – це буде саме окрема група для всякіх контролерів (див. Kubernetes: Pods та WorkerNodes – контроль розміщення подів на нодах)
  • в автоматизації створення кластеру для WorkerNodes SecurityGroups та Private Subnets додати теги Key=karpenter.sh/discovery,Value=${CLUSTER_NAME}
  • у values чартів для деплою AWS ALB Controller, ExternalDNS та власне Karpenter додати tolerations на тег critical-addons=true та taints  NoExecute і NoSchedule

На разі наче все.

Всі поди піднялись, все працює.

І пара корисних команд для перевірки статусу подів/нод.

Вивести кількість подів на кожній ноді:

[simterm]

$ kubectl get pods -A -o jsonpath='{range .items[?(@.spec.nodeName)]}{.spec.nodeName}{"\n"}{end}' | sort | uniq -c | sort -rn

[/simterm]

Вивести поди на окремій ноді:

[simterm]

$ kubectl get pods --all-namespaces -o wide --field-selector spec.nodeName=ip-10-0-2-212.ec2.internal

[/simterm]

Окремо ще можна додати плагінів для kubectl, які відображають зайняті ресурси на нодах – див. Kubernetes: менеджер плагинов Krew и полезные плагины для kubectl.

О, і ще треба погратись з Vertical Pod Autoscaler – як Karpenter буде робити з ним.

Корисні посилання

Loading