На всіх попередніх проектах, де був 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 буде робити з ним.
Корисні посилання
- Getting Started with Karpenter
- Karpenter Best Practices
- Control Pod Density
- Deprovisioning Controller
- How do I install Karpenter in my Amazon EKS cluster?