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

Автор |  18/08/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 буде робити з ним.

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