На всіх попередніх проектах, де був 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таtaintsNoExecuteі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 буде робити з ним.












