Karpenter: Beta version – обзор змін та upgrade v0.30.0 на v0.32.1

Автор |  04/11/2023
 

Отже, Karpenter зробив ще один великий шаг до релізу, і у версії 0.32 вийшов з етапу Alpha до Beta.

Давайте кратко подивимось на зміни – а вони досить суттєві, а потім виконаємо апгрейд на EKS з Karpneter Terraform module та Karpenter Helm chart.

Сам процес установки Karpenter описував у пості Terraform: створення EKS, частина 3 – установка Karpenter, і нижче буде трохи відсилок на нього, типу імені файла karpenter.tf та деяких variables.

Основна документація:

Що нового в v0.32.1?

Починаючи з 0.32, API-ресурси Provisioner, AWSNodeTemplate та Machine будуть deprecated, а з версії 0.33 їх приберуть взагалі.

Замість них додані:

  • Provisioner => NodePool
  • AWSNodeTemplate => EC2NodeClass
  • Machine => NodeClaim

v1alpha5/Provisioner => v1beta1/NodePool

NodePool являється наступником Provisioner, і має параметри для:

  • налаштування запуску Pods та WorkerNodes – requirements подів до нод, taints, labels
  • налаштування того, як Karpenter буде розміщувати поди на нодах, та як буде виконувати deprovisioning зайвих нод

“Під капотом” NodePool буде створювати ресурси v1beta1/NodeClaims (які прийшли за зміну v1alpha5/Machine), і взагалі тут ідея приблизно як з Deployments та Pods: в Deployment (NodePool) ми описуємо template того, як буде створюватись Pod (NodeClaims). А v1beta1/NodeClaims в свою чергу являється наступником ресурсу v1alpha5/Machine.

Також була додана новая секція disruption – сюди були перенесені всі параметри, які відносяться до видалення зайвих нод та управління подами, див. Disruption.

v1alpha5/AWSNodeTemplate => v1beta1/EC2NodeClass

EC2NodeClass являється наступником AWSNodeTemplate, і має параметри для:

  • налаштування AMI
  • SecurityGroups
  • subnets
  • EBS
  • IMDS
  • User Data

Було видалене поле spec.instanceProfile – тепер Karpenter буде створювати Instance Profile для ЕС2 на основі IAM Role, яку буде передано в spec.role.

Також остаточно було видалене поле spec.launchTemplateName.

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

Зміни в Labels

Лейбли karpenter.sh/do-not-evict та karpenter.sh/do-not-consolidate були об’єднані у нову лейблу karpenter.sh/do-not-disrupt, яку можна використовувати як для Pods, щоб заборонити Karpenetr виконувати Pod eviction, так і для WorkerNodes, щоб заборонити видалення цієї ноди.

Міграція 0.30.0 на v0.32.1

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

Що ми маємо зараз?

  • AWS EKS Cluster, створений з Terraform
  • за допомогою модулю terraform-aws-modules/eks/aws//modules/karpenter версії 0.30.0 в AWS створюються необхідні ресурси – IAM, SQS, etc
  • сам Karpenter встановлено з Helm-чарту karpenter
    • який приймає параметр version, який ми передаємо зі змінної var.helm_release_versions.karpenter
  • і маємо два ресурси kubectl_manifest – для karpenter_provisioner і karpenter_node_template

Процес міграції включає в себе:

  1. оновлення IAM Role, яка використовується подами контролера Karpenter для управління EC2 в AWS:
    1. замінити тег karpenter.sh/provisioner-name на karpenter.sh/nodepool (див. chore: Release v0.32.0) – стосується тільки ролі, яка була створена з Cloudformation, бо в Terraform модулі використовується інший Condition
    2. додати IAM Policy iam:CreateInstanceProfile, iam:AddRoleToInstanceProfile, iam:RemoveRoleFromInstanceProfile, iam:DeleteInstanceProfile та iam:GetInstanceProfile
  2. додавання нових CRD v1beta1, після чого Karpenter сам оновить ресурси Machine на NodeClaim
    1. для міграції AWSNodeTemplate => EC2NodeClass та Provisioner => NodePool можна використати утіліту karpenter-convert

Після чого виконати заміну WorkerNodes:

  • з використання фічі drift:
    • додати taint karpenter.sh/legacy=true:NoSchedule до існуючого Provisioner
    • Karpneter помітить всі ноди цього Provisioner як drifted
    • Karpenter запустить нові ЕС2, використовуючи новий NodePool
  • видалення нод:
    • створити NodePool аналогічний існуючому Provisioner
    • видалити існуючий Provisioner командою kubectl delete provisioner <provisioner-name> --cascade=foreground, в результаті чого Karpenter видалить всі його ноди виконавши node drain для всіх відразу, і поди, які перейдуть в стан Pending, запустить на нодах, які були створені з NodePool
  • ручна заміна:
    • створити NodePool аналогічний існуючому Provisioner
    • додати taint karpenter.sh/legacy=true:NoSchedule до старого Provisioner
    • по черзі вручну видалити всі його WorkerNopes з kubectl delete node

Що це значить для нас?

Всі зміни начебто backward compatible (перевірив – відкатував версії), тобто можемо спокійно оновлювати існуючі ресурси один за одним – поламатись нічого не повинно.

Тож, що зробимо:

  • оновимо модуль Terraform
  • додамо CRD
  • оновимо Helm-чарт з Karpenter
  • задеплоїмо, перевіримо – старі ноди від старого Provisioner продовжать працювати, поки ми їх не вб’ємо
  • додамо нові NodePool та EC2NodeClass
  • перестворимо WorkerNodes

Поїхали.

Step 1: оновлення Terraform Karpenter module

Виконуємо terraform apply до всіх змін, щоб мати задеплоєну останню версію нашого коду.

У файлі karpenter.tf маємо виклик модуля і його версію:

module "karpenter" {
  source  = "terraform-aws-modules/eks/aws//modules/karpenter"
  version = "19.16.0"
...

Karpenter Instance Profile

В самому модулі була додана нова змінна enable_karpenter_instance_profile_creation, яка визначає хто буде менеджити IAM Roles для WorkerNodes – Terraform, як було раніше, чи використати нову фічу від Karpenter. Якщо enable_karpenter_instance_profile_creation задати в true, то модуль просто додає ще один блок прав в IAM, див. main.tf.

Але тут “є нюанс” (с) в залежностях модулю Karpneter, і Helm-чарту: якщо enable_karpenter_instance_profile_creation включити в true (дефолтне значення false)  – то модуль не створить resource "aws_iam_instance_profile", який  далі використовується в чарті для Karpenter – параметр settings.aws.defaultInstanceProfile.

Тож тут два варіанти:

  1. спочатку оновити тільки версії – модуля і чарта, але використати старі параметри і Provisioner
    1. після апдейту – створити NodePool and EC2NodeClass, замінити параметри, і перестворити WorkerNodes
  2. або обновити відразу все – і модуль/чарт, і параметри, і Provisioner замінити на NodePool, і задеплоїти все разом

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

Почнемо, звісно, з Dev.

Міняємо версію на v19.18.0 – див. Releases, і додаємо enable_karpenter_instance_profile_creation = true – але поки закоментимо:

module "karpenter" {
  source  = "terraform-aws-modules/eks/aws//modules/karpenter"
  #version = "19.16.0"
  version = "19.18.0"

  cluster_name = module.eks.cluster_name

  irsa_oidc_provider_arn          = module.eks.oidc_provider_arn
  irsa_namespace_service_accounts = ["karpenter:karpenter"]

  create_iam_role      = false
  iam_role_arn         = module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_arn
  irsa_use_name_prefix = false

  # In v0.32.0/v1beta1, Karpenter now creates the IAM instance profile
  # so we disable the Terraform creation and add the necessary permissions for Karpenter IRSA
  #enable_karpenter_instance_profile_creation = true
}

...

Поки не деплоїмо, йдемо до чарту.

Step 2: оновлення Karpenter Helm chart

Версії чартів в мене зібрані в одній змінній – оновлюємо тут karpneter v0.30.0 на v0.32.1:

...
helm_release_versions = {
  #karpenter                             = "v0.30.0"
  karpenter                             = "v0.32.1"
  external_dns                          = "1.13.1"
  aws_load_balancer_controller          = "1.6.1"
  secrets_store_csi_driver              = "1.3.4"
  secrets_store_csi_driver_provider_aws = "0.3.4"
  vpa                                   = "2.5.1"
}
...

Далі у нас два основних апдейти – це CRD та values чарту.

Karpenter CRD Upgrade

CRD є в основному чарті Karpneter, але:

Helm does not manage the lifecycle of CRDs using this method, the tool will only install the CRD during the first installation of the helm chart. Subsequent chart upgrades will not add or remove CRDs, even if the CRDs have changed. When CRDs are changed, we will make a note in the version’s upgrade guide.

Тобто наступні запуски helm install не оновлять CRD, які були встановлені при першій інсталяції.

Тож варіантів тут (знову!) два – або просто вручну їх додати з kubectl, або встановити з додаткового чарту karpenter-crd, див. CRD Upgrades.

При чому чарт встановить і старі CRD v1alpha5, і нові v1beta1, тобто ми будемо мати такий собі “backward compatible mode” – зможемо використовувати і старий Provisioner, і одночасно додати новий NodePool.

З helm template можна перевірити що саме чарт karpenter-crd буде робити:

$ helm template karpenter-crd oci://public.ecr.aws/karpenter/karpenter-crd --version v0.32.1
...

Але тут є нюанс: для встановлення нових CRD з чарту потрібно буде видалити вже існуючі CRD, а це призведе до того, що і існуючі Provisioner та Machine і відповідні WorkdeNodes будуть видалені.

Тож якщо це вже давно існуючий кластер, і ви хочете все зробити без downtime – то CRD встановлюємо руками.

Якщо кілька хвилин простою вам ОК – то краще вже робити з додатковим Helm-чартом, як й надалі буде все автоматично менеджити.

Ще можна спробувати заімпортити існуючі CRD в реліз нового чарту, див. Import Existing k8s Resources in Helm 3 – особисто я не пробував, але має працювати.

Отже в моєму випадку робимо апдейт CRD з чартом – додаємо його в наш Terraform:

...
resource "helm_release" "karpenter_crd" {
  namespace        = "karpenter"
  create_namespace = true

  name                = "karpenter-crd"
  repository          = "oci://public.ecr.aws/karpenter"
  repository_username = data.aws_ecrpublic_authorization_token.token.user_name
  repository_password = data.aws_ecrpublic_authorization_token.token.password
  chart               = "karpenter-crd"
  version             = var.helm_release_versions.karpenter
}
...

Переходимо до основного чарту.

Karpenter Chart values

Далі в resource "helm_release" "karpenter" прописуємо нові вальюси та додамо depends_on на чарт з CRD.

Додаємо параметр settings.aws.defaultInstanceProfile – потім його приберемо:

...
resource "helm_release" "karpenter" {
  namespace        = "karpenter"
  create_namespace = true

  name                = "karpenter"
  repository          = "oci://public.ecr.aws/karpenter"
  repository_username = data.aws_ecrpublic_authorization_token.token.user_name
  repository_password = data.aws_ecrpublic_authorization_token.token.password
  chart               = "karpenter"
  version             = var.helm_release_versions.karpenter

  values = [
    <<-EOT
    settings:
      clusterName: ${module.eks.cluster_name}
      clusterEndpoint: ${module.eks.cluster_endpoint}
      interruptionQueueName: ${module.karpenter.queue_name}
      aws:
        defaultInstanceProfile: ${module.karpenter.instance_profile_name}
    serviceAccount:
      annotations:
        eks.amazonaws.com/role-arn: ${module.karpenter.irsa_arn} 
    EOT
  ]

  depends_on = [
    helm_release.karpenter_crd
  ]

  /*
  set {
    name  = "settings.aws.clusterName"
    value = local.env_name
  }

  set {
    name  = "settings.aws.clusterEndpoint"
    value = module.eks.cluster_endpoint
  }

  set {
    name  = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
    value = module.karpenter.irsa_arn
  }

  set {
    name  = "serviceAccount.annotations.eks\\.amazonaws\\.com/sts-regional-endpoints"
    value = "true"
    type  = "string"
  }

  set {
    name  = "settings.aws.defaultInstanceProfile"
    value = module.karpenter.instance_profile_name
  }

  set {
    name  = "settings.aws.interruptionQueueName"
    value = module.karpenter.queue_name
  }
  */
}
...

Виконуємо terraform init щоб завантажити нову версію модулю.

Зараз у нас вже є CRD, які були створені при першій інсталяції Karnepnter:

$ kk get crd | grep karpenter
awsnodetemplates.karpenter.k8s.aws                          2023-10-03T08:30:58Z
machines.karpenter.sh                                       2023-10-03T08:30:59Z
provisioners.karpenter.sh                                   2023-10-03T08:30:59Z

Видаляємо їх:

$ kk -n karpenter delete crd awsnodetemplates.karpenter.k8s.aws machines.karpenter.sh provisioners.karpenter.sh
customresourcedefinition.apiextensions.k8s.io "awsnodetemplates.karpenter.k8s.aws" deleted
customresourcedefinition.apiextensions.k8s.io "machines.karpenter.sh" deleted
customresourcedefinition.apiextensions.k8s.io "provisioners.karpenter.sh" deleted

І запускаємо terraform apply – зараз у нас має оновитись тільки сам чарт – в IAM поки змін не буде, бо маємо enable_karpenter_instance_profile_creation == false.

Після деплою перевіряємо CRD:

$ kk get crd | grep karpenter
awsnodetemplates.karpenter.k8s.aws                          2023-11-02T15:33:26Z
ec2nodeclasses.karpenter.k8s.aws                            2023-11-03T11:20:07Z
machines.karpenter.sh                                       2023-11-02T15:33:26Z
nodeclaims.karpenter.sh                                     2023-11-03T11:20:08Z
nodepools.karpenter.sh                                      2023-11-03T11:20:08Z
provisioners.karpenter.sh                                   2023-11-02T15:33:26Z

Перевіряємо поди і ноди – все має залишитись, як було – той самий ресурс Machine, щоб був створений зі старого Provisiner, і та сама WokrderNode:

$ kk get machine
NAME            TYPE       ZONE         NODE                         READY   AGE
default-b6hdr   t3.large   us-east-1a   ip-10-1-35-97.ec2.internal   True    30d

Якщо все ОК – то переходимо до створення NodePool та EC2NodeClass.

Step 3: створення NodePool та EC2NodeClass

Спочатку давайте розберемося з IAM ролями 🙂 Але це стосується конкретного мого сетапу, бо якщо ви всі ноди створюєте з Karpenter, то цю частину можна скіпнути.

В Terraform модулі EKS у нас створюється Managed Node Group, в якій створюється IAM Role, яка потім використовується в InstanceProfile для всіх нод кластера.

Далі ця роль передається в модуль karpenter, і тому create_iam_role в модулі Карпентер стоїть в false – бо роль вже є:

...
module "karpenter" {
  ...
  # disable create as doing in EKS NodeGroup resource
  create_iam_role      = false
  iam_role_arn         = module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_arn
  irsa_use_name_prefix = false
  ...
}
...

Потім, коли Karpenter запускав нові EC2-інстанси, їм підключалась ця роль.

Але з  новою версією Karpenter він сам створює instanceProfile з spec.role.

Щоб в новому маніфесті з EC2NodeClass передати в поле spec.role ім’я замість iam_role_arn – шукаємо його в outputs.tf:

...
output "iam_role_name" {
  description = "The name of the IAM role"
  value       = try(aws_iam_role.this[0].name, null)
}

output "iam_role_arn" {
  description = "The Amazon Resource Name (ARN) specifying the IAM role"
  value       = try(aws_iam_role.this[0].arn, var.iam_role_arn)
}
...

Тепер можна додавати решту ресурсів.

Додавання EC2NodeClass

Див. доку в NodeClasses.

Тут робимо прямо в коді файлу karpenter.tf, як було і для AWSNodeTemplate.

Лишаємо і старий маніфест, і поруч додаємо новий:

...
resource "kubectl_manifest" "karpenter_node_template" {
  yaml_body = <<-YAML
    apiVersion: karpenter.k8s.aws/v1alpha1
    kind: AWSNodeTemplate
    metadata:
      name: default
    spec:
      subnetSelector:
        karpenter.sh/discovery: "atlas-vpc-${var.environment}-private"
      securityGroupSelector:
        karpenter.sh/discovery: ${local.env_name}
      tags:
        Name: ${local.env_name_short}-karpenter
        environment: ${var.environment}
        created-by: "karpneter"
        karpenter.sh/discovery: ${local.env_name}
  YAML

  depends_on = [
    helm_release.karpenter
  ]
}

resource "kubectl_manifest" "karpenter_node_class" {
  yaml_body = <<-YAML
    apiVersion: karpenter.k8s.aws/v1beta1
    kind: EC2NodeClass
    metadata:
      name: default
    spec:
      amiFamily: AL2
      role: ${module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_name}
      subnetSelectorTerms:
        - tags:
            karpenter.sh/discovery: "atlas-vpc-${var.environment}-private"
      securityGroupSelectorTerms:
        - tags:
            karpenter.sh/discovery: ${local.env_name}
      tags:
        Name: ${local.env_name_short}-karpenter
        environment: ${var.environment}
        created-by: "karpneter"
        karpenter.sh/discovery: ${local.env_name}
  YAML

  depends_on = [
    helm_release.karpenter
  ]
}
...

В spec.amiFamily передаємо AmazonLinux v2, в spec.role – IAM Role для InstanceProfile – вона ж додана і до aws-auth ConfigMap нашого кластера (в модулі eks).

Додавання NodePool

Так як Provisioner/NodePools планувалось мати не один, то їхні параметри задані в variables – копіюємо саму змінну:

...
variable "karpenter_provisioner" {
  type = map(object({
    instance-family = list(string)
    instance-size   = list(string)
    topology        = list(string)
    labels          = optional(map(string))
    taints = optional(object({
      key    = string
      value  = string
      effect = string
    }))
  }))
}

variable "karpenter_nodepool" {
  type = map(object({
    instance-family = list(string)
    instance-size   = list(string)
    topology        = list(string)
    labels          = optional(map(string))
    taints = optional(object({
      key    = string
      value  = string
      effect = string
    }))
  }))
}
...

І значення:

...
karpenter_provisioner = {
  default = {
    instance-family = ["t3"]
    instance-size   = ["small", "medium", "large"]
    topology        = ["us-east-1a", "us-east-1b"]
    labels = {
      created-by = "karpenter"
    }
  }
}

karpenter_nodepool = {
  default = {
    instance-family = ["t3"]
    instance-size   = ["small", "medium", "large"]
    topology        = ["us-east-1a", "us-east-1b"]
    labels = {
      created-by = "karpenter"
    }
  }
}
...

Як і з Provisioner – додаємо файл шаблону configs/karpenter-nodepool.yaml.tmpl – тут формат теж трохи змінився, наприклад labels тепер в блоці spec.template.metadata.labels а не spec.labels, як було в Provisioner, див. NodePools.

Тож тепер шаблон виглядає так:

apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: ${name}

spec:
  template:

    metadata:

    %{ if labels != null ~} 
      labels:
      %{ for k, v in labels ~}
        ${k}: ${v}
      %{ endfor ~}    
    %{ endif ~}

    spec:

    %{ if taints != null ~}
      taints:
        - key: ${taints.key}
          value: ${taints.value}
          effect: ${taints.effect}
    %{ endif ~}

      nodeClassRef:
        name: default
      requirements:
        - key: karpenter.k8s.aws/instance-family
          operator: In
          values: ${jsonencode(instance-family)}
        - key: karpenter.k8s.aws/instance-size
          operator: In
          values: ${jsonencode(instance-size)}
        - key: topology.kubernetes.io/zone
          operator: In
          values: ${jsonencode(topology)}
  # total cluster limits 
  limits:
    cpu: 1000
    memory: 1000Gi
  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 30s

Важливо: якщо ви в AWS не використовуєте Spot Instances, то додайте karpenter.sh/capacity-type == "on-demand", див. причину нижче у Помилка: The provided credentials do not have permission to create the service-linked role for EC2 Spot Instances.

І додаємо новий ресурс kubectl_manifest в karpenter.tf, поруч зі старим Provisioner:

...
resource "kubectl_manifest" "karpenter_provisioner" {
  for_each = var.karpenter_provisioner

  yaml_body = templatefile("${path.module}/configs/karpenter-provisioner.yaml.tmpl", {
    name            = each.key
    instance-family = each.value.instance-family
    instance-size   = each.value.instance-size
    topology        = each.value.topology
    taints          = each.value.taints
    labels = merge(
      each.value.labels,
      {
        component   = var.component
        environment = var.environment
      }
    )
  })

  depends_on = [
    helm_release.karpenter
  ]
}

resource "kubectl_manifest" "karpenter_nodepool" {
  for_each = var.karpenter_nodepool

  yaml_body = templatefile("${path.module}/configs/karpenter-nodepool.yaml.tmpl", {
    name            = each.key
    instance-family = each.value.instance-family
    instance-size   = each.value.instance-size
    topology        = each.value.topology
    taints          = each.value.taints
    labels = merge(
      each.value.labels,
      {
        component   = var.component
        environment = var.environment
      }
    )
  })

  depends_on = [
    helm_release.karpenter
  ]
}
...

Далі:

  • в resource "helm_release" "karpenter" видаляємо з values aws.defaultInstanceProfile
  • в module "karpenter" включаємо enable_karpenter_instance_profile_creation в true

Тепер Terraform має:

  • додати права до ролі KarpenterIRSA
  • якщо для модулю karpenter не передавався параметр create_instance_profile == false – то видалиться module.karpenter.aws_iam_instance_profile, але в моєму випадку він все одно не використовувався
  • і додати kubectl_manifest.karpenter_nodepool["default"] та kubectl_manifest.karpenter_node_class

Деплоїмо, перевіряємо:

$ kk get nodepool
NAME      NODECLASS
default   default

$ kk get ec2nodeclass
NAME      AGE
default   40s

І все ще маємо нашу стару Machine:

$ kk get machine
NAME            TYPE       ZONE         NODE                         READY   AGE
default-b6hdr   t3.large   us-east-1a   ip-10-1-35-97.ec2.internal   True    30d

Все – нам лишилось перестворити WorkderNodes, перемістити Поди, і після деплою на Staging та Production прибратись в коді – видалити все, що лишилось від версії 0.30.0.

The provided credentials do not have permission to create the service-linked role for EC2 Spot Instances

В якийсь момент в логах пішла помилка такого плана:

karpenter-5dcf76df9-l58zq:controller {“level”:”ERROR”,”time”:”2023-11-03T14:59:41.072Z”,”logger”:”controller”,”message”:”Reconciler error”,”commit”:”1072d3b”,”controller”:”nodeclaim.lifecycle”,”controllerGroup”:”karpenter.sh”,”controllerKind”:”NodeClaim”,”NodeClaim”:{“name”:”default-ttx86″},”namespace”:””,”name”:”default-ttx86″,”reconcileID”:”6d17cadf-a6ca-47e3-9789-2c3491bf419f”,”error”:”launching nodeclaim, creating instance, with fleet error(s), AuthFailure.ServiceLinkedRoleCreationNotPermitted: The provided credentials do not have permission to create the service-linked role for EC2 Spot Instances.”}

Але, по-перше – чому Spot? Звідки це?

Якщо глянути NodeClaim, який був створений для ціїє ноди, то там бачимо "karpenter.sh/capacity-type == spot":

$ kk get nodeclaim -o yaml
...
  spec:
    ...
    - key: karpenter.sh/nodepool
      operator: In
      values:
      - default
    - key: karpenter.sh/capacity-type
      operator: In
      values:
      - spot
...

Хоча в документації сказано, що по-дефолту capacity-type має бути on-demand:

...
        - key: "karpenter.sh/capacity-type" # If not included, the webhook for the AWS cloud provider will default to on-demand
          operator: In
          values: ["spot", "on-demand"]
...

А ми в NodePool його не вказували.

Якщо ж в NodePool вказати karpenter.sh/capacity-type явно:

...
      requirements:
        - key: karpenter.k8s.aws/instance-family
          operator: In
          values: ${jsonencode(instance-family)}
        - key: karpenter.k8s.aws/instance-size
          operator: In
          values: ${jsonencode(instance-size)}
        - key: topology.kubernetes.io/zone
          operator: In
          values: ${jsonencode(topology)}
        - key: karpenter.sh/capacity-type
          operator: In 
          values: ["on-demand"]
...

То все працює, як треба.

І по-друге – яких саме пермішенів йому не вистачає? Що за помилка “ServiceLinkedRoleCreationNotPermitted“?

Я спотами в AWS не користувався, тому довелось трохи погуглити, і відповідь знайшлась в документації Work with Spot Instances та гайді Using AWS Spot instances, де мова йде про IAM Role AWSServiceRoleForEC2Spot, яка має бути створена в AWS Account, щоб мати змогу створювати Spot-інстанси.

Трохи дивне рішення по-дефолту створювати Spot, тим більш в документації говориться навпаки. Крім того – в 0.30 все працювало і без явного налаштування karpenter.sh/capacity-type.

Окей, будемо мати на увазі – якщо користуємось виключно On Demand – то треба додавати в конфіг NodePool.

Step 3: оновлення WorkerNodes

Що нам лишилося – це переселити наші поди на нові ноди.

Насправді всі поди переїхали на нові ноди ще під час апдейту, але давайте зробимо, бо нам ще апдейтити інші кластери.

Тут маємо три варіанти, про які говорили на початку. Давайте пробувати робити це без даунтайму – з використанням drift (але без даунтайму – це якщо маєте мінімум по 2 поди на сервіс, і на додачу PodDisruptionBudget).

Що нам треба зробити – це додати taint до існуючого Provisioner, задеплоїти зміни, щоб taint додався до Nodes, і тоді Karpenter виконає Node Drain та створить нові ноди, щоб перемістити наші workloads.

Додаємо в наш шаблон configs/karpenter-provisioner.yaml.tmpl:

apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: ${name}
spec:

  taints:
    - key: karpenter.sh/legacy
      value: "true"
      effect: NoSchedule
...

В чарті версії 0.32.1 параметр drift досі в false, тому включаємо в values нашого resource "helm_release" "karpenter":

...
  values = [
    <<-EOT
    settings:
      clusterName: ${module.eks.cluster_name}
      clusterEndpoint: ${module.eks.cluster_endpoint}
      interruptionQueueName: ${module.karpenter.queue_name}
      featureGates:
        drift: true
...

І вже всі ці зміни разом можна викатувати на інші Kubernetes кластери – тільки не забудьте оновити tfvars для цих кластерів (якщо маєте щось типу окремих envs/dev/dev-1-28.tfvars, envs/staging/staging-1-28.tfvars, envs/prod/prod-1-28.tfvars).

Rolling back the upgrade

Навряд чи це знадобиться, бо в принці особливих проблема не має бути, але я робив під час ре-тесту апгрейду, тож запишу:

  • міняємо версію module "karpenter" з нової 19.18.0 на стару 19.16.0
  • в module "karpenter" коментуємо опцію enable_karpenter_instance_profile_creation
  • в tfvars для helm_release_versions міняємо версію чарту Karpenter з нової v0.32.1 на стару v0.30.0
  • лишаємо resource "helm_release" "karpenter_crd"
  • в resource "helm_release" "karpenter" коментуюємо новий блок values, розкоментуємо старі values через set
  • коментуємо ресурси resource "kubectl_manifest" "karpenter_nodepool" та resource "kubectl_manifest" "karpenter_node_class"
  • у файлі configs/karpenter-provisioner.yaml.tmpl прибираємо Taint