Karpenter виконує схожу роботу із Cluster Autoscaler проте реагує значно швидше на потреби скейлінгу, не має обмежень по типам інстансів в межах одного пулу, працює і логікою вартості вузлів тощо. Karpenter не потребує Node та Autoscaling груп і працює напряму із API без традиційних абстракцій AWS необхідних для цього. Окрім цього він може виконувати безпекові функції: забезпечує автоматичне перестворення нод після проходження певного часу чи після виходу нової AMI.
Уперше Karpenter було представлено в 2021 році у якості продукту із відкритим кодом. У 2024 році була вже представлена версія 1.0, котра була оголошена Амазоном як готова до використання в prod-середовищах на AWS. Karpenter було cпроектовано для використання на різних хмарних середовищах, тому є сторонні імплементації Karpenter для Azure, Alibaba Cloud і можливо інші. На практиці ж він перш за все розвивається для AWS, інші його версії можуть бути не достатньо стабільними, а офіційна докуменація для розробників відсутня.
Ця стаття буде умовно поділена на 3 частини. Перша буде про установку Karpenter на вже діючий EKS кластер, для чого вже традиційно скористаємось Тераформом. А в другій та третій поговоримо про його роботу та особливості.
Перед інсталяцією Karpenter варто звернути увагу на функціонал EKS Auto Mode, що з'явився відносно нещодавно. Він перекладає відповідальність на установку та підтримку базових AWS-аддонів, серед яких AWS LB Contoller, Karpenter, EBS CSI та інші на хмарний сервіс Amazon. Не бачу причин не спробувати спершу саме його.
1. BASIC KARPENTER INSTALLATION WITH TERRAFORM
Увесь код доступний за цією адресою, там же є про установку EKS кластера v1.32 та мереж, котрі для нього необхідні. Ось основна стаття як цей код застосовується, проте цього разу скористуємось останньою версією Тераформа v1.12:
$ git clone git@github.com:ipeacocks/terraform-aws-example.git
$ cd terraform-aws-example/eks-infra/addons/karpenter
Основний функціонал знаходиться в main.tf
$ cat main.tf
data "terraform_remote_state" "eks" {
backend = "s3"
config = {
bucket = "my-tf-state-2023-06-01"
key = "my-eks.tfstate"
region = "us-east-1"
}
}
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "my-tf-state-2023-06-01"
key = "my-vpc.tfstate"
region = "us-east-1"
}
}
data "aws_caller_identity" "current" {}
locals {
name = "${var.name_prefix}-${data.terraform_remote_state.eks.outputs.cluster_name}-${var.region}"
}
module "this" {
source = "terraform-aws-modules/eks/aws//modules/karpenter"
version = "20.36.0"
cluster_name = data.terraform_remote_state.eks.outputs.cluster_name
iam_policy_name = local.name
iam_policy_use_name_prefix = false
enable_v1_permissions = true
iam_role_name = local.name
iam_role_use_name_prefix = false
queue_name = local.name
create_node_iam_role = false
node_iam_role_arn = format("arn:aws:iam::%s:role/%s", data.aws_caller_identity.current.account_id, data.terraform_remote_state.eks.outputs.eks_managed_node_groups["one"].iam_role_name)
# Since the nodegroup role will already have an access entry
create_access_entry = false
}
resource "aws_eks_pod_identity_association" "this" {
cluster_name = data.terraform_remote_state.eks.outputs.cluster_name
namespace = var.namespace
service_account = "karpenter"
role_arn = module.this.iam_role_arn
}
resource "helm_release" "crd" {
name = "karpenter-crd"
repository = "oci://public.ecr.aws/karpenter"
chart = "karpenter-crd"
version = var.helm_package_version
namespace = var.namespace
create_namespace = true
}
resource "helm_release" "this" {
name = "karpenter"
repository = "oci://public.ecr.aws/karpenter"
chart = "karpenter"
version = var.helm_package_version
namespace = var.namespace
create_namespace = true
values = [
templatefile("${path.module}/templates/helm/values.yaml.tpl", {
clusterName = data.terraform_remote_state.eks.outputs.cluster_name
interruptionQueueName = module.this.queue_name
})
]
depends_on = [helm_release.crd]
}
resource "kubectl_manifest" "generic_ec2_node_class" {
yaml_body = templatefile("${path.module}/templates/manifests/generic-ec2nodeclass.yaml.tpl", {
private_subnets = join(",", data.terraform_remote_state.vpc.outputs.worker_subnet_ids)
cluster_node_security_group_id = data.terraform_remote_state.eks.outputs.node_security_group_id
cluster_name = data.terraform_remote_state.eks.outputs.cluster_name
role = data.terraform_remote_state.eks.outputs.eks_managed_node_groups["one"].iam_role_name
tags = join(",", [for key, value in var.tags : "${key}: '${value}'"])
})
depends_on = [helm_release.this]
}
resource "kubectl_manifest" "generic_node_pool" {
yaml_body = templatefile("${path.module}/templates/manifests/generic-nodepool.yaml.tpl", {
disruption = {
consolidation_policy = var.generic_node_pool.disruption.consolidation_policy
consolidate_after = var.generic_node_pool.disruption.consolidate_after
}
expire_after = var.generic_node_pool.expire_after
instance = {
# Because Karpenter has only greater operator.
min_cpu = var.generic_node_pool.instance.min_cpu - 1
min_memory = var.generic_node_pool.instance.min_memory_mb - 1
}
})
depends_on = [helm_release.this]
}
Цей код залежить від параметрів EKS кластеру та мережі тож він і вичитує відповідні Terraform стейти. Далі ми користуємось готовим модулем для установки всіх речей, що необхідні для роботи Karpenter: IAM ролі (котру потребує і контролер, і майбутні підняті нові вузли, яким необхідно буде підключення до кластеру), SQS черги тощо.
Доступ до власної ролі аддону буде забезпечуватись через pod identity. Установка та оновлення Karpenter CRDs та самого контролера відбувається окремо, адже кожне наступне оновлення потребуватиме явного оновлення CRDs.
Зазначу, що у разі, коли Karpenter піднімається у неймспейсі відмінному від системного kube-system, потрібно також застосувати FlowSchemas, інакше запити до kubeapi можуть тротлитись.
За допомогою ресурсу kubectl_manifest ми застосовуємо загальні nodepool/nodeclass, що відповідатимуть за логіку підняття вузлів. Вони формуватимуть фінальний nodeclaim, в результаті чого буде піднятий новий вузол для розміщення подів в статусі "Pending".
Переглянемо тепер вміст темплейтів. Значення values.yaml, котрі відправляються на вхід helm чарту:
$ cat templates/helm/values.yaml.tpl
controller:
resources:
limits:
cpu: 1
memory: 1Gi
requests:
cpu: 1
memory: 1Gi
metrics:
port: 8000
settings:
clusterName: ${clusterName}
interruptionQueue: ${interruptionQueueName}
Тут, як вже було згадано, необхідні ім'я кластеру та SQS-черги для отримання повідомлень про потенційні заміщення вузлів.
Generic nodepool описує умови, котрим має задовольняти под, якому не вистачає місця для роботи. Тому він має інформацію про необхідну архітектуру, параметри консолідації вузла (ми їх якраз і параметризуємо) тощо:
$ cat templates/manifests/generic-nodepool.yaml.tpl
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: generic
annotations:
kubernetes.io/description: "General purpose NodePool for amd64/arm64 workloads"
spec:
disruption:
consolidationPolicy: ${disruption.consolidation_policy}
consolidateAfter: ${disruption.consolidate_after}
limits:
cpu: 1k
template:
metadata:
labels:
node-type: dynamic
spec:
expireAfter: ${expire_after}
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: generic
taints:
- key: your.company.io/workloads
effect: NoSchedule
- key: your.company.io/workloads
effect: NoExecute
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
- key: kubernetes.io/os
operator: In
values: ["linux"]
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["t", "c", "m"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["3"]
- key: "karpenter.k8s.aws/instance-cpu"
operator: Gt
values: ["${instance.min_cpu}"]
- key: "karpenter.k8s.aws/instance-memory"
operator: Gt
values: ["${instance.min_memory}"]
Nodeclass же описує який саме вузол піднімати та із якими параметрами щодо диску, мереж та інше:
$ cat templates/manifests/generic-ec2nodeclass.yaml.tpl
# how to find latest AMI https://karpenter.sh/docs/concepts/nodeclasses/#specamiselectorterms
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: generic
annotations:
kubernetes.io/description: "General purpose EC2NodeClass for running AL 2023 nodes"
spec:
amiSelectorTerms:
- alias: al2023@v20250403
metadataOptions:
httpEndpoint: enabled
httpPutResponseHopLimit: 2
httpTokens: required
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
deleteOnTermination: true
encrypted: true
throughput: 250
volumeSize: 30Gi
volumeType: gp3
role: ${role}
securityGroupSelectorTerms:
- id: ${cluster_node_security_group_id}
subnetSelectorTerms:
%{ for subnet in split(",", private_subnets) }
- id: ${subnet}
%{ endfor }
tags:
Name: ${cluster_name}-eks-dynamic
%{ for tag in split(",", tags) }
${tag}
%{ endfor }
Лишились файли змінних variables.tf та підключених провайдерів terraform.tf, тут не так щоб багато особливого, тому не буду зупинятись на цьому.
Скачуємо залежності та застосовуємо код:
$ terraform init
$ terraform apply
Перевіряємо чи Karpenter піднявся:
$ kubectl get pods -n kube-system | grep karpenter
karpenter-dd675d75b-mktcg 1/1 Running 0 11h
karpenter-dd675d75b-vzhfc 1/1 Running 0 11h
$ kubectl logs -n kube-system -l app.kubernetes.io/name=karpenter
{"level":"INFO","time":"2025-05-16T14:05:19.806Z","logger":"controller.controller-runtime.metrics","message":"Starting metrics server","commit":"0871602"}
{"level":"INFO","time":"2025-05-16T14:05:19.806Z","logger":"controller","message":"starting server","commit":"0871602","name":"health probe","addr":"[::]:8081"}
{"level":"INFO","time":"2025-05-16T14:05:19.807Z","logger":"controller.controller-runtime.metrics","message":"Serving metrics server","commit":"0871602","bindAddress":":8000","secure":false}
{"level":"INFO","time":"2025-05-16T14:05:19.907Z","logger":"controller","message":"attempting to acquire leader lease kube-system/karpenter-leader-election...","commit":"0871602"}
Та перевіримо його роботу. Спершу ми створимо deployment із 0 под і потім збільшимо кількість реплік:
$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: inflate
spec:
replicas: 0
selector:
matchLabels:
app: inflate
template:
metadata:
labels:
app: inflate
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values:
- arm64
terminationGracePeriodSeconds: 0
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- name: inflate
image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
resources:
requests:
cpu: 1
securityContext:
allowPrivilegeEscalation: false
nodeSelector:
node-type: dynamic
tolerations:
- effect: NoSchedule
key: your.company.io/workloads
operator: Exists
- effect: NoExecute
key: your.company.io/workloads
operator: Exists
topologySpreadConstraints:
- labelSelector:
matchLabels:
app: inflate
maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
- labelSelector:
matchLabels:
app: inflate
maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway # DoNotSchedule
EOF
deployment.apps/inflate created
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
inflate 0/0 0 0 19s
Перевіримо нинішній стан нод, а саме їхню кількість:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-10-0-25-246.ec2.internal Ready <none> 12h v1.32.3-eks-473151a
ip-10-0-9-219.ec2.internal Ready <none> 12h v1.32.3-eks-473151a
$ kubectl get nodeclaims
No resources found
Збільшимо кількість inflate реплік до 5:
$ kubectl scale deployment inflate --replicas 5
deployment.apps/inflate scaled
Поди перейдуть в стан Pending, через відсутність необхідних потужностей:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
inflate-694646c7b9-8hgz6 0/1 Pending 0 6s
inflate-694646c7b9-dhfjm 0/1 Pending 0 6s
inflate-694646c7b9-dr2ht 0/1 Pending 0 6s
inflate-694646c7b9-m9vmh 0/1 Pending 0 6s
inflate-694646c7b9-rfpcm 0/1 Pending 0 6s
І в межах 30 секунд Karpenter забезпечить ці потужності:
$ kubectl get nodeclaims
NAME TYPE CAPACITY ZONE NODE READY AGE
generic-279cw c6g.xlarge on-demand us-east-1c ip-10-0-31-247.ec2.internal True 41s
generic-4lg8t c6g.xlarge on-demand us-east-1b ip-10-0-21-173.ec2.internal True 41s
generic-7pj6r c6g.xlarge on-demand us-east-1a ip-10-0-15-2.ec2.internal True 41s
generic-977zx c6g.xlarge on-demand us-east-1b ip-10-0-17-210.ec2.internal True 41s
generic-vg9zn c6g.xlarge on-demand us-east-1c ip-10-0-27-128.ec2.internal True 41s
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-10-0-13-111.ec2.internal Ready <none> 27m v1.32.3-eks-473151a
ip-10-0-19-213.ec2.internal Ready <none> 27m v1.32.3-eks-473151a
ip-10-0-22-84.ec2.internal Ready <none> 14s v1.32.3-eks-473151a
ip-10-0-23-25.ec2.internal Ready <none> 13s v1.32.3-eks-473151a
ip-10-0-26-246.ec2.internal Ready <none> 12s v1.32.3-eks-473151a
ip-10-0-31-145.ec2.internal Ready <none> 11s v1.32.3-eks-473151a
Видалимо inflate та nodeclaim, експерименти не мають тривати вічність:
$ kubectl delete deploy inflate
$ kubectl delete nodeclaim generic-f6bq4
Або ж Карпентер сам видалить зайві вузли після вказаного consolidateAfter часу.
2. KARPENTER BASICS AND HOW IT WORKS
У цій частині, як я і обіцяв, поговоримо про деталі роботи Карпентера, його налаштування та можливі опції. Наявна робоча версія, котру ми проінсталювали вище, має у цьому допомогти і надати тестовий полігон для перевірки надалі написаного.
Основними об'єктами, що надають CRDs Карпентера, є Nodepool, Nodeclass та Nodeclaim. Nodepool забезпечує виконання умов, котрі запитують поди (необхідну архітектуру, AZ, лейбли кожного нового вузла тощо). Nodeclass описує параметри (security groups, розмір диска, мереж) вузла (EC2 ноди), котрий буде піднятий для розташування на ньому под. Конфіг Nodepool-а має посилання на Nodeclass із яким він працює, а останній може бути доданий до декількох Нодпулів.
Після оцінки вимог, що накладають Nodepool та відповідний йому Nodeclass, Karpenter сворює Nodeclaim. Це вже інструкції Карпентеру щодо того, який саме та де піднімати вузол. Також він відображає життєвий цикл вузла: можна переглядати його стан, а видалення Nodeclaim призводить до видалення відповідного вузла із EKS кластера.
2.1. Nodepools
Зупинимось окремо на кожному об'єкті Карпентера. Отже, що робить Nodepool? Він задовольняє наступні типи запитів:
- Ресурсні запити. Підбір вузла, де є доступні ресурси CPU/пам'яті
- Вимоги вибору вузлів. Запуск на вузлі де є необхідний лейбл (nodeSelector) чи спорідненість (node affinity) по якимось атрибутам
- Розподіл по топології. Розподіляє поди топології мереж/вузлів/capacity хмарного середовища задля забезпечення вищого рівня доступності
- Споріденість/не спорідненість по подам (pod affinity/anti-affinity). Розташування подів в зележності від розміщення інших подів
Тепер розглянемо імплементацію цього на практиці. У якості основного прикладу Nodepool візьмемо generic, котрий згенерував Terraform у першій часині, та поглянемо за що відповідають окремі директиви. Тож він робить наступне:
• Описує та проставляє taint-и щоб задовольнити tolaration-и, котрі вже проставлені в подах.
Якщо подивитись на деплоймент та нодпул, котрими ми оперували у першій частині статті, то деплоймент має наступні толерейшени:
...
tolerations:
- effect: NoSchedule
key: your.company.io/workloads
operator: Exists
- effect: NoExecute
key: your.company.io/workloads
operator: Exists
...
Тобто, якщо на вузлі будуть присутні такі тейнти, то вони не будуть перешкодою для створення подів цього деплойменту. Cаме такі тейнти Карпентер (тобто його generic нодпул) і проставить на вузлах, щоб забезпечити старт цих подів:
...
taints:
- key: your.company.io/workloads
effect: NoSchedule
- key: your.company.io/workloads
effect: NoExecute
...
Навіщо це робиться? Для того, щоб на такі вузли могли зайти лише потрібні поди. Наприклад навантаження, що потребує відеокарт, але очевидно такої конфігурації не потребують всі наявні поди.
Також Karpenter може проставляти початкові стартові тейнти (startup taints) задля того, щоб отримати певний час на додаткову конфігурацію вузлів перед розміщенням корисного навантаження.
• Забезпечує створення необхідних лейблів на новостворених вузлах, щоб таким чином забезпечити старт под на них. Наприклад, якщо нодпул має наступний блок конфігурації:
...
spec:
...
template:
metadata:
labels:
node-type: dynamic
...
То Карпентер підніматиме вузли із лейблом 'node-type: dynamic'. А под, котрий має відповідний nodeselector, або affinity розміститься на ній:
...
containers:
- name: inflate
...
nodeSelector:
node-type: dynamic
...
Разом із taint (приклад, що наведено вище) може бути забезпечена умова, що певна група подів і лише вона зможе розміститись на цих вузлах. Таким чином можна відділити робоче навантаження і навантаження, що генерують контролери та інші сервісні аддони (Karpenter обов'язково має працювати на статичних вузлах).
• В залежності від необхідної архітектури CPU, що потребує пода, може піднімати відповідні вузли. Якщо деплоймент матиме наступну nodeAffinity:
...
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values:
- arm64
...
То наступний nodepool:
...
spec:
...
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
...
Вплине на те, що Карпентер підніме arm64 вузол. І відповідно values: ["amd64"] не зробить нічого і буде обслуговувати nodeAffinity лише із amd архітектурою. Та ж сама логіка стосується і інших лейблів, котрі за замовчуванням проставляє Karpenter, зокрема ОС, spot чи on-demand, EC2 покоління тощо. Наприклад
...
nodeSelector:
topology.kubernetes.io/zone: us-west-2a
karpenter.sh/capacity-type: spot
...
Такий под підніметься на spot-інстансі в us-west-2a при умові що Karpenter Nodepool дозволяє це.
• Може піднімати вузли в залежності від бажаної дистрибуції подів а межах AZ, самих хостів. Для деалойменту, що має наступні опції в своєму описі
...
topologySpreadConstraints:
- labelSelector:
matchLabels:
app: inflate
maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
- labelSelector:
matchLabels:
app: inflate
maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
...
Карпентер відповідно стартуватиме вузли, щоб запезпечити ці topologySpread правила. Тобто буде намагатись одночасно розподіляти їх по зонах доступності та хостнеймах, причому цього разу на етапі нодпула ніяких додаткових правил не потрібно, адже Карпентер буде використовувати лейбли, котрі він сам же і проставляє на всіх вузлах.
Ба більше, Karpenter може гарантувати у відсотковому відношенні розподіл навантаження по spot та on-demand вузлах! Для цього ж використовується той же topologySpreadConstraints проте по іншому topologyKey ключу.
• Може піднімати вузли та EBS томи в однакових AZ. У AWS диск до віртуальної машини EC2 можна підключити лише в тому випадку, якщо він лежить в тій самій AZ. Тож Karpenter відслідковує це і у разі появи нового вузла підніме EBS в тій же зоні доступності. Якщо ж диск вже створений, то EC2 вузол буде піднято в тій же зоні доступності.
Окрім всього цього Нодпули визначають політику заміщення старих вузлів новими, консолідації вузлів задля здешевлення вартості; зупинки вузлів, котрі не мають корисного навантаження тощо. Детальніше про це поговоримо трохи далі, а зараз перейдемо до розмови про Nodeclass-и.
2.2. Nodeclasses
Нодкласи визначають специфічні параметри вузлів кожного хмарного середовиша на якому працює Karpenter. Так як на разі по-факту підтримується лише AWS, то у yaml-описах вони зазначаються як EC2NodeClass. Nodepool має пряме посилання на NodeClass, який використовує:
...
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: generic
...
Але сам NodeClass може одночасно використовуватись різними Нодпулами. Він також потребує додаткову роль, щоб кожен вузол, котрий підняв Karpenter, мав ті ж можливості, що і статичні вузли кластеру: підключення до API Control Plane тощо.
На прикладі Нодкласу, котрий ми застосували вище за допомогою Terraform, розглянемо його можливості та особливості:
• Він відповідальний за вибір AMI для майбутніх вузлів:
...
spec:
amiSelectorTerms:
- alias: al2023@v20250505
...
Ми обрали останній на цей момент Amazon Linux 2023, проте amiSelectorTerms має значно ширші можливості. Наприклад, тут можуть бути застосовані логічні операції AND чи OR по тегам імеджів, іменам, id та інше.
Karpeter наполегливо рекомендує відмовитись від @latest варіанта для production середовищ, адже у разі помилки збирання AMI на стороні Амазону це може призвести до інциденту в обслуговуванні.
Останню версію AMI для Нодекласу можна знайти ось так:
$ export K8S_VERSION="1.32"
$ aws ssm get-parameters-by-path --path "/aws/service/eks/optimized-ami/$K8S_VERSION/amazon-linux-2023/" --recursive | jq -cr '.Parameters[].Name' | grep -v "recommended" | awk -F '/' '{print $10}' | sed -r 's/.*(v[[:digit:]]+)$/\1/' | sort | uniq
...
v20250501
v20250505
v20250514
Тут є і варіанти Bottlerocket та AL2. Останній не рекомендовано до використання, адже в версії EKS 1.33 він пересане бути доступний для створення воркерів.
• Описує опції доступності метаданих всередині EC2 інстансу:
...
spec
metadataOptions:
httpEndpoint: enabled
httpPutResponseHopLimit: 2
httpTokens: required
...
Вони будуть корисні для агентів моніторингу типу Datadog, щоб отримати теги вузла EC2 для своїх метрик. Тут можна прочитати про це більше.
• Може додавати теги до всіх новостворених EC2 вузлів. У Тераформ коді ми описали їх динамічно, що врешті решт перетворюєть в такі теги:
...
spec:
tags:
Name: my-eks-eks-dynamic
cluster-name: my-eks
node-group: dynamic
team: devops
...
• Може управляти розміром диску, його типом, продуктивністю тощо, що потім буде підключений до EC2:
...
spec:
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
deleteOnTermination: true
encrypted: true
throughput: 250
volumeSize: 30Gi
volumeType: gp3
...
Для Amazon Linux достатньо 1 диску, проте Bottlerocket AMI потребує 2 диски для роботи: один для control volume (в режимі тільки для читання), а інший для самих імеджів та логів.
• Має перелік сабнетів для розміщення вузлів. У нашому випадку це ті ж сабнети, що і статичні воркери:
...
spec:
subnetSelectorTerms:
- id: subnet-05722f9c81bf38622
- id: subnet-02bdad92b264d27f6
- id: subnet-08d3e419f1bd24f83
...
• Вказує Security Group-и для EC2 інстансів:
...
spec
securityGroupSelectorTerms:
- id: sg-046434eb75957a578
...
Це лише основний список опцій, що ми використали в нашому прикладі, проте можливості значно ширші за ці і про них можна прочитати в офіційній документації.
2.3. Nodeclaims
На відміну від двох попередніх, Нодклейми є незмінними об'єктами і призначені більше для управління життєвим циклом створених вузлів та відстеження їх стану. Це результат роботи Нодпулу та Нодкласу.
Коли щось пішло не так, то можна викликати describe на потрібний Nodeclaim:
$ kubectl get nodeclaims
$ kubectl describe nodeclaim <nodeclaim>
Видалення Нодклейма призведе до видалення вузла, котрий був ним створений.
3. DISRUPTION PROCESS
У останній частині поговоримо як Karpenter проводить заміну вузлів, які процеси за цим стоять та що він робить для того, щоб якомога зменшити час можливих простоїв у обслуговування.
Отже, Karpenter постійно слідкує за тим, щоб кластер був максимально ефективним (звісно власні додані правила накладають особливості) в контексті вартості, безпеки та відповідності заданій конфігурації. Для виконання цих вимог постійно працюють два контролери: Disruption та Termination. Disruption контролер слідкує за тим, які вузли можна вивести із обслуговування і за потреби замінити на інший (виконавши попереднє підняття наступника), а Termination займається безпечною зупинкою попередніх вузлів.
Процес заміни вузлів чи їх видалення доволі витончений. Disruption контролер замінює вузли із причин Дріфта (зміна в конфігурації вузлів/нодпулів) чи Консолідації (виведення вузлів задля ефективності пакування робочих навантажень). Причому по першій причині Karpenter відпрацьовує першим та не працює по всім причинам одночасно. Якщо коротко, то Disruption контролер працює наступним чином:
- Збирає масив вузлів, котрий необхідно вивести із обслуговування. Якщо таких вузлів немає чи на них працюють поди, котрі не можна виселити, контролер почне робити все з початку пізніше.
- У разі знаходження такої ноди чи нод, Disruption контролер перевіряє чи не порушує їх видалення бюджети виведення/заміни вузлів (disruption budgets) та запускає симуляції планування розміщення для перевірки потреби в заміні чи піднятті нових вузлів.
- Додає тейнт karpenter.sh/disrupted:NoSchedule до вузлів, котрі контролер прагне вивести із кластеру чи замінити, щоб нові поди тим часом не змогли стартувати на такому вузлі
- Стартує всі вузли, котрі були прораховані у попередній симуляції і чекає допоки вони отримають статус Ready. Якщо ж цей процес не закінчиться успішно, то Disruption контролер прибере попередньо проставлений тейнт і увесь процес почнеться із самого початку.
- Видаляє всі ноди, заміну яким він вже знайшов. Проте завдяки фіналайзеру, котрий проставляє сам Karpenter при старту вузлів, нода буде в статусі Terminating зі всіма робочими подами, допоки Termination контролер не виконає свою роботу із безпечного видалення вузла.
- Після безпечного видалення одного чи декілька вузлів, Disruption контролер починає все спочатку.
Отже тепер м'яч на стороні Termination контролера і він береться за свою роботу:
- Додає taint karpenter.sh/disrupted:NoSchedule до всіх вузлів, котрі планує видалити, що зупиняє kube-scheduler від старту под на таких потужностях. Він повторює цю дію за Disruption контролером, адже в деяких моментах Termination контролер стартує одночасно із ним.
- Починає виселення под із цих вузлів, для чого використовує Kubernetes Eviction API, зокрема притримуючись виставлених PDB подів, ігноруючи роботу статичних подів (різноманітні поди в межах деймонсетів на кшталт fluent-bit, aws-node, csi-контролерів тощо); подів, котрі ігнорують тейнт, що був виставлений у попередньому пункті та подів, що вже відпрацювали і знаходяться в статусі succeeded/failed.
- Термінейтить нодклейм, що був відповідальний за створення та життєвий цикл цього вузла, через API хмарного провайдера.
- Видаляє finalizer із ноди, після чого її повне видалення дозволене для kube-api
Завдяки фіналайзеру навіть ручне видалення вузла чи нодклейма, котрий відповідальний за цей вузол, відбудеться безпечне видалення вузла Termination контролером.
Карпентер категоризує 2 автоматизовані методи для процесу заміщення/видалення (disruption) вузлів:
• Безпечні (Graceful). У цьому випадку попередньо стартують нові вузли на заміну, а швидкість таких заміщень може бути регульована додатковими бюджетами, про котрі поговоримо пізніше. До цих методів входять:
- Консолідація (Consolidation). Основна ціль цього процесу - зменшити вартість вузлів кластеру. Щоб зробити це Карпентер може видалити вузли при наступних умовах: якщо вони порожні (тобто не обслуговують корисне навантаження), якщо їхні поди можна розмістити на інших вузлах зі збереженням всіх попередньо вказаних умов (наприклад topologySpread чи (anti-)affinity), та у разі якщо їх можна замінити дешевшими (наприклад, раніше була потреба у більш потужнішому вузлі, бо на ньому тимчасово працювали інші поди). Для управління консолідацією є два параметри: consolidationPolicy (визначає чи можна консолідувати вузли, котрі, на думку Карпентара, слабо утилізовані) та consolidateAfter (через яку кількість часу після останнього старту чи зупинки поду можна запускати консолідацію). Для спот-інстансів за-замовчуванням не дозволені консолідації із заміною дорожчого інстансу на дешевший, адже це вносить додаткові ризики, якщо в процесі AWS вирішить відібрати вузол. Опціонально це можна дозволити.
- Дрифт (Drift). Карпентер запустить перестворення вузлів у разі зміни певних параметрів Нодкласу/Нодпула (тегів, базової AMI, зміни типів інстансів тощо).
• Примусові (Forceful). Цього разу термінейт вузлів починається одразу, до готовності нових у якості заміни. Рейтлімітити ці методи не можна, окрім PDB подів у деяких випадках.
- Закінчення терміну дії (Expiration). Вузол буде змушений піти на заміну після закінчення терміну spec.template.spec.expireAfter вказаному на кожному із вузлів (значення цього параметру за-замовчуванням рівний 720 годин). Без задання параметра terminationGracePeriod, поди із анотацією karpenter.sh/do-not-disrupt завжди блокуватимуть видалення нод, на котрих працюють фізично. Саме тому ці два параметри рекомендують вказувати в парі.
- Переривання (Interruption). Це примусові методи, що виникли по причині вилучення spot-інстансів, подій технічного обслуговування чи термінету/зупинки вузлів. Коли Карпентер виявляє ці події, він (точніше його Termination контролер) одразу починає виселяти поди із таких вузлів задля максимізації часу на підняття нових та переселення подів. У такі моменти terminationGracePeriodSeconds для подів може не бути витриманий повністю за браком доступного часу, особливо якщо його значення доволі високе. Наприклад, AWS перед вилученням spot-вузлів дає лише 2 хвилини, що іноді може бути замало. Ці переривання працюють через SQS-чергу, котру було додано до конфігурації на етапі інсталяції Карпентера в першій частині статті. В цю чергу пересилаються події AWS сервісів через EventBridge.
- Автоматичне відновлення вузлів (Node Auto Repair). Функціонал, що наразі перебуває в статусі альфа, вимкнений за замовчуванням та потребує додаткового агента. У разі збоїв заліза, файлових систем тощо, Karpenter терміново виведе такі вузли із кластеру, пропускаючи механізми, що обслуговують Termination та Disruption контролери.
Як вже було згадано, у випадку безпечних методів заміщення вузлів (Automated Graceful Methods) можна впливати на швидкість таких заміщень і опція budgets допомагає і цим. Ці методи поділяються на Косолідацію та Дрифт. Консолідація може відбуватись якщо вузол перестав бути корисним (тобто на ньому вже відсутнє корисне навантаження) чи якщо його можна замінити на менший та дешевший. Тому budget і оперує 3 опціями: Empty, Underutilized та Drifted.
Уявімо ситуацію, що ми хочемо заборонити будь-які заміщення вузлів в робочий час (по UTC) окрім по причині Empty і дозволити всі заміщення (Empty, Drifted та Underutilized) і всі інші години. Для цього правимо блок budgets в Нодпулі:
...
disruption:
budgets:
- nodes: "1"
reasons:
- Empty
- duration: 9h
nodes: "0"
reasons:
- Drifted
- Underutilized
schedule: 0 8 * * mon-fri
- duration: 15h
nodes: "1"
reasons:
- Drifted
- Underutilized
schedule: 0 17 * * mon-fri
- duration: 24h
nodes: "1"
reasons:
- Drifted
- Underutilized
schedule: 0 0 * * sat,sun
...
Доволі очевидний/явний графік. Ці ж бюджети можна описати більш лаконічно, проте менш очевидно:
...
disruption:
budgets:
- nodes: "1"
- duration: 9h
nodes: "0"
reasons:
- Drifted
- Underutilized
schedule: 0 8 * * mon-fri
...
Перше правило дозволяє заміщувати по 1 ноді за раз по всім 3 причинам, друге правило обмежує це робити по причинам Drifted та Underutilized. Отже вузли будуть завжди мати можливість видалятись, якщо вони будуть Empty.
Бюджети, як на мене, мають дещо неочевидний вигляд. Тому перед їх використанням я б рекомендував їх детально потестити в non-production середовищах.
Посилання:
https://karpenter.sh/v1.4/
https://community.aws/content/2dhlDEUfwElQ9mhtOP6D8YJbULA/run-kubernetes-clusters-for-less-with-amazon-ec2-spot-and-karpenter#step-6-optional-simulate-spot-interruption
https://www.cncf.io/blog/2024/11/06/karpenter-v1-0-0-beta/
https://github.com/kubernetes-sigs/karpenter/tree/main/designs
https://rtfm.co.ua/karpenter-vikoristannya-disruption-budgets/
https://www.slideshare.net/slideshow/adopting-karpenter-for-cost-and-simplicity-at-grafana-labspdf/264376667
https://github.com/aws/karpenter-provider-aws/tree/main/examples/v1
https://grafana.com/blog/2023/11/09/how-grafana-labs-switched-to-karpenter-to-reduce-costs-and-complexities-in-amazon-eks/
Немає коментарів:
Дописати коментар