Terraform: EKS та Karpenter – upgrade версії модуля з 19.21 на 20.0

Автор |  10/07/2024
 

Наче звична задача – оновити версію модулю Terraform, але в terraform-aws-modules/eks версії 20.0 були досить великі зміни з breaking changes.

Зміни стосуються аутентифікації та авторизації в AWS IAM та AWS EKS, які розбирав в пості AWS: Kubernetes та Access Management API – нова схема авторизації в EKS.

Але там ми все робили руками, аби взагалі подивись на новий механізм – а тепер давайте це зробимо з Terraform.

Крім того, зміни відбулися в Karpenter відносно IRSA (IAM Roles for ServiceAccounts). Нову схему роботи з ServiceAccount описував в AWS: EKS Pod Identities – заміна IRSA? Спрощуємо менеджмент IAM доступів, а апдейт конкретно в  модулі Karpenter глянемо в цьому пості.

Хоча я буду робити апгрейд версії самого AWS EKS і модуля Terraform через створення нового кластеру, а тому в принципі можу не морочити голову з “live-update” кластера, але хочеться спробувати зробити так, аби це можна було застосувати на живому кластері та не втратити до нього доступу і не поламати умовний Production.

Втім, майте на увазі, що те, що описано в цьому пості робиться на тестовому кластері (бо в мене взагалі один кластер для Dev/Staging/Prod). Тож не варто відразу робити апгрейд на продакшені, а краще спочатку протестувати на якомусь Dev-оточенні.

Про сетап самого кластеру детальніше писав у Terraform: створення EKS, частина 2 – EKS кластер, WorkerNodes та IAM, і в цьому пості приклади коду будуть саме звідти, хоча він вже трохи відрізняється від того, що було описано там, бо створення EKS зробив окремим власним модулем, щоб простіше було менеджити різні кластери-оточення.

В цілому, змін наче і дуже небагато – але пост вийшов довгий, бо намагався показати все детально і з реальними прикладами.

Що змінилося?

Повний опис є в GitHub – див. v20.0.0 та Upgrade from v19.x to v20.x.

Що саме нас цікавить (знов-таки – конкретно в моєму випадку):

  • EKS:
    • aws-auth: винесено в окремий модуль, в параметрах самого модуля terraform-aws-modules/eks його вже нема
      • з terraform-aws-modules/eks видалено параметри manage_aws_auth_configmap, create_aws_auth_configmap, aws_auth_roles, aws_auth_users, aws_auth_accounts
    • authentication_mode: додано значення API_AND_CONFIG_MAP
    • bootstrap_cluster_creator_admin_permissions: hardcoded у false
      • але можна передати enable_cluster_creator_admin_permissions зі значенням true, хоча тут наче треба додавати Access Entry
    • create_instance_profile: дефолтне значення змінилось з true на false, аби відповідати змінам в Karpenter v0.32 (але в мене Karpenter і так вже 0.32 і все працює, тому тут змін не має бути)
  • Karpenter:
    • irsa: видалено імена змінних з “irsa” – пачка перейменувань і декілька імен видалено взагалі
    • create_instance_profile: дефолтне значення з true стало false
    • enable_karpenter_instance_profile_creation: видалена
    • iam_role_arn: стало node_iam_role_arn
    • irsa_use_name_prefix: стало iam_role_name_prefix

В документації по апдейту описано ще досить багато змін по Karpenter – але в мене зараз  сам Karpenter версії 0.32 (див. Karpenter: Beta version – обзор змін та upgrade v0.30.0 на v0.32.1), модуль Terraform terraform-aws-modules/eks/aws//modules/karpenter зараз 19.21.0, і процес апгрейду самого EKS з 19 на 20 ніяк не вплинув на роботу Karpenter, тож їх можна оновлювати окремо – спочатку EKS, потім вже Karpenter.

План апгрейду

Що будемо робити:

  1. EKS: оновлення версії модулю з 19.21 => 20.0 з API_AND_CONFIG_MAP
    1. додавання aws-auth окремим модулем
  2. Karpenter: оновлення версії модулю з 19.21 => 20.0

EKS: upgrade 19.21 на 20.0

Нам потрібно:

  • видалити все, пов’язане з aws_auth, в моєму випадку це:
    • manage_aws_auth_configmap
    • aws_auth_users
    • aws_auth_roles
  • додати authentication_mode зі значенням API_AND_CONFIG_MAP (пізніше, для 21, треба буде замінити на API)
  • додати новий модуль для aws_auth_roles і перенести aws_auth_users та aws_auth_roles туди

Щодо bootstrap_cluster_creator_admin_permissions та enable_cluster_creator_admin_permissions – так як цей кластер створювався з 19.21, то root-юзер там вже є, і він буде доданий в Access Entries разом з WorkerNodes IAM Role, тому тут нічого робити не треба.

А як перенести в access_entries наших юзерів і ролі – подивимось мабуть вже в наступному пості, бо зараз будемо робити тільки оновлення версії модуля зі збереженням aws-auth ConfigMap.

Для тесту Karpenter – створив тестовий деплой з одним подом, який затригерить створення WorkerNode, і до нього Ingress/ALB, на який йде постійний ping, аби впевнитись, що все буде працювати без даунтаймів.

NodeClaims зараз:

$ kk get nodeclaim
NAME            TYPE       ZONE         NODE                          READY   AGE
default-7hjz7   t3.small   us-east-1a   ip-10-0-45-183.ec2.internal   True    53s

Окей, поїхали оновлювати EKS.

Поточний код Terraform та структура модулів

Аби далі краще розуміти момент з видаленням ресурс aws_auth з Terraform state – моя поточна структура файлів/модулів:

$ tree .
.
|-- Makefile
|-- backend.hcl
|-- backend.tf
|-- envs
|   `-- test-1-28
|       |-- VERSIONS.md
|       |-- backend.tf
|       |-- main.tf
|       |-- outputs.tf
|       |-- providers.tf
|       |-- test-1-28.tfvars
|       `-- variables.tf
|-- modules
|   `-- atlas-eks
|       |-- configs
|       |   `-- karpenter-nodepool.yaml.tmpl
|       |-- controllers.tf
|       |-- data.tf
|       |-- eks.tf
|       |-- iam.tf
|       |-- karpenter.tf
|       |-- outputs.tf
|       |-- providers.tf
|       `-- variables.tf
|-- outputs.tf
|-- providers.tf
|-- variables.tf
`-- versions.tf

Тут:

  • в envs/test-1-28/main.tf викликається модуль з modules/atlas-eks з необхідними параметрами – і виконувати terraform state rm ми будемо саме в envs/test-1-28
  • в modules/atlas-eks/eks.tf викликається модуль terraform-aws-modules/eks/aws потрібної версії – тут ми будемо робити зміни в коді

Виклик рутового модуля в файлі envs/test-1-28/main.tf виглядає так:

module "atlas_eks" {
  source = "../../modules/atlas-eks"

  # 'devops'
  component = var.component

  # 'ops'
  aws_environment = var.aws_environment

  # 'test'
  eks_environment = var.eks_environment

  env_name = local.env_name

  # '1.28'
  eks_version = var.eks_version

  # 'endpoint_public_access', 'enabled_log_types'
  eks_params = var.eks_params

  # 'coredns = v1.10.1-eksbuild.6', 'kube_proxy = v1.28.4-eksbuild.1', etc
  eks_addon_versions = var.eks_addon_versions

  # AWS IAM Roles to be added to EKS aws-auth as 'masters'
  eks_aws_auth_users = var.eks_aws_auth_users

  # GitHub IAM Roles used in Workflows
  eks_github_auth_roles = var.eks_github_auth_roles

  # 'vpc-0fbaffe234c0d81ea'
  vpc_id = var.vpc_id

  helm_release_versions = var.helm_release_versions

  # override default 'false' in the module's variables
  # will trigger a dedicated module like 'eks_blueprints_addons_external_dns_test'
  # with a domainFilters == variable.external_dns_zones.test == 'test.example.co'
  external_dns_zone_test_enabled = true

  # 'instance-family', 'instance-size', 'topology'
  karpenter_nodepool = var.karpenter_nodepool
}

А код основного модуля – так:

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 19.21.0"

  # is set in `locals` per env
  # '${var.project_name}-${var.eks_environment}-${local.eks_version}-cluster'
  # 'atlas-eks-test-1-28-cluster'
  # passed from the root module
  cluster_name    = "${var.env_name}-cluster"

  # passed from the root module
  cluster_version = var.eks_version

  # 'eks_params' passed from the root module
  cluster_endpoint_public_access = var.eks_params.cluster_endpoint_public_access

  # 'eks_params' passed from the root module
  cluster_enabled_log_types = var.eks_params.cluster_enabled_log_types

  # 'eks_addons_version' passed from the root module
  cluster_addons = {
    coredns = {
      addon_version = var.eks_addon_versions.coredns
      configuration_values = jsonencode({
        replicaCount = 1
        resources = {
          requests = {
            cpu = "50m"
            memory = "50Mi"
          }
        }
      })
    }
    kube-proxy = {
      addon_version = var.eks_addon_versions.kube_proxy
      configuration_values = jsonencode({
        resources = {
          requests = {
            cpu = "20m"
            memory = "50Mi"
          }
        }
      })
    }    
    vpc-cni = {
      # old: eks_addons_version
      # new: eks_addon_versions
      addon_version = var.eks_addon_versions.vpc_cni
      configuration_values = jsonencode({
        env = {
          ENABLE_PREFIX_DELEGATION = "true"
          WARM_PREFIX_TARGET       = "1"
          AWS_VPC_K8S_CNI_EXTERNALSNAT = "true"
        }
      })      
    }
    aws-ebs-csi-driver = {
      addon_version            = var.eks_addon_versions.aws_ebs_csi_driver
      # iam.tf
      service_account_role_arn = module.ebs_csi_irsa_role.iam_role_arn
    }
  }

  # make as one complex var?
  # passed from the root module
  vpc_id                   = var.vpc_id
  # for WorkerNodes
  # passed from the root module
  subnet_ids               = data.aws_subnets.private.ids
  # for the Control Plane
  # passed from the root module
  control_plane_subnet_ids = data.aws_subnets.intra.ids

  manage_aws_auth_configmap = true

  # `env_name` make too long name causing issues with IAM Role (?) names
  # thus, use a dedicated `env_name_short` var
  eks_managed_node_groups = {
    # eks-default-dev-1-28
    "${local.env_name_short}-default" = {

      # `eks_managed_node_group_params` from defaults here

      # number, e.g. 2
      min_size = var.eks_managed_node_group_params.default_group.min_size
      # number, e.g. 6
      max_size = var.eks_managed_node_group_params.default_group.max_size
      # number, e.g. 2
      desired_size = var.eks_managed_node_group_params.default_group.desired_size
      # list, e.g. ["t3.medium"]
      instance_types = var.eks_managed_node_group_params.default_group.instance_types
      # string, e.g. "ON_DEMAND"
      capacity_type = var.eks_managed_node_group_params.default_group.capacity_type

      # allow SSM
      iam_role_additional_policies = {
        AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
      }

      taints = var.eks_managed_node_group_params.default_group.taints

      update_config = {
        max_unavailable_percentage = var.eks_managed_node_group_params.default_group.max_unavailable_percentage
      }
    }
  }

  # 'atlas-eks-test-1-28-node-sg'
  node_security_group_name    = "${var.env_name}-node-sg"
  # 'atlas-eks-test-1-28-cluster-sg'
  cluster_security_group_name = "${var.env_name}-cluster-sg"

  # to use with EC2 Instance Connect
  node_security_group_additional_rules = {
    ingress_ssh_vpc = {
      description = "SSH from VPC"
      protocol    = "tcp"
      from_port   = 22
      to_port     = 22
      cidr_blocks      = [data.aws_vpc.eks_vpc.cidr_block]
      type        = "ingress"
    }
  }

  # 'atlas-eks-test-1-28'
  node_security_group_tags = {
    "karpenter.sh/discovery" = var.env_name
  }

  cluster_identity_providers = {
    sts = {
      client_id = "sts.amazonaws.com"
    }
  }

  # passed from the root module
  aws_auth_users = var.eks_aws_auth_users
  
  # locals flatten() 'eks_masters_access_role' + 'eks_github_auth_roles'
  aws_auth_roles = local.aws_auth_roles
}

Трохи більше про модулі Terraform див. у Terraform: модулі, Outputs та Variables та Terraform: створення модулю для збору логів AWS ALB в Grafana Loki.

Ролі/юзери для aws-auth формуються в locals зі змінних у variables.tf та envs/test-1-28/test-1-28.tfvars, всі з system:masters – до RBAC ми ще не дійшли:

locals {
  # create a short name for node names in the 'eks_managed_node_groups'
  # 'test-1-28'
  env_name_short = "${var.eks_environment}-${replace(var.eks_version, ".", "-")}"

  # 'eks_github_auth_roles' passed from the root module
  github_roles = [for role in var.eks_github_auth_roles : {
    rolearn  = role
    username = role
    groups   = ["system:masters"]
  }]

  # 'eks_masters_access_role' + 'eks_github_auth_roles'
  # 'eks_github_auth_roles' from the root module
  # 'aws_iam_role.eks_masters_access' from the iam.tf here
  aws_auth_roles = flatten([
    {
      rolearn  = aws_iam_role.eks_masters_access_role.arn
      username = aws_iam_role.eks_masters_access_role.arn
      groups   = ["system:masters"]
    },
    local.github_roles
  ])
}

Антон в документації зробив класний Diff of Before (v19.21) vs After (v20.0) по змінам, то давайте спробуємо.

Зміни в authentication_mode та aws_auth

В модулі EKS найбільша зміна це переключення authentication_mode на API_AND_CONFIG_MAP.

Кластер вже є, створений з версією 19.21.0.

Вкладка Access зараз виглядає так:

Access configuration == ConfgiMap, і в IAM access entries пусто.

Тепер робимо зміни в коді файла modules/atlas-eks/eks.tf:

  1. міняємо версію на v20.0
  2. видаляємо все, що пов’язано з aws_auth
  3. додаємо authentication_mode зі значенням API_AND_CONFIG_MAP

Зміни поки виглядають так:

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  #version = "~> 19.21.0"
  version = "~> v20.0"
  ...
  # removing for API_AND_CONFIG_MAP
  #manage_aws_auth_configmap = true

  # adding for API_AND_CONFIG_MAP
  authentication_mode = "API_AND_CONFIG_MAP"

  ...
  # removing for API_AND_CONFIG_MAP
  # passed from the root module
  #aws_auth_users = var.eks_aws_auth_users
  
  # removing for API_AND_CONFIG_MAP
  # locals flatten() 'eks_masters_access_role' + 'eks_github_auth_roles'
  #aws_auth_roles = local.aws_auth_roles
}

Виконуємо terraform init, аби оновити модуль EKS:

...
Initializing modules...
Downloading registry.terraform.io/terraform-aws-modules/eks/aws 20.0.1 for atlas_eks.eks...
- atlas_eks.eks in .terraform/modules/atlas_eks.eks
- atlas_eks.eks.eks_managed_node_group in .terraform/modules/atlas_eks.eks/modules/eks-managed-node-group
- atlas_eks.eks.eks_managed_node_group.user_data in .terraform/modules/atlas_eks.eks/modules/_user_data
- atlas_eks.eks.fargate_profile in .terraform/modules/atlas_eks.eks/modules/fargate-profile
...

Глянемо aws-auth зараз – заодно будемо мати його “бекап” в YAML:

$ kk -n kube-system get cm aws-auth -o yaml
apiVersion: v1
data:
  mapAccounts: |
    []
  mapRoles: |
    - "groups":
      - "system:bootstrappers"
      - "system:nodes"
      "rolearn": "arn:aws:iam::492***148:role/test-1-28-default-eks-node-group-20240705095955197900000003"
      "username": "system:node:{{EC2PrivateDNSName}}"
    - "groups":
      - "system:masters"
      "rolearn": "arn:aws:iam::492***148:role/atlas-eks-test-1-28-masters-access-role"
      "username": "arn:aws:iam::492***148:role/atlas-eks-test-1-28-masters-access-role"
    - "groups":
      - "system:masters"
      "rolearn": "arn:aws:iam::492***148:role/atlas-test-ops-1-28-github-access-role"
      "username": "arn:aws:iam::492***148:role/atlas-test-ops-1-28-github-access-role"
...
  mapUsers: |
    - "groups":
      - "system:masters"
      "userarn": "arn:aws:iam::492***148:user/arseny"
      "username": "arseny"
...

Виконуємо terraform plan, і бачимо, що дійсно – aws-auth буде видалено:

Отже, якщо ми хочемо його залишити – то треба додати новий модуль з terraform-aws-modules/eks/aws//modules/aws-auth.

Приклад в документації виглядає так:

module "eks" {
  source  = "terraform-aws-modules/eks/aws//modules/aws-auth"
  version = "~> 20.0"

  manage_aws_auth_configmap = true

  aws_auth_roles = [
    {
      rolearn  = "arn:aws:iam::66666666666:role/role1"
      username = "role1"
      groups   = ["custom-role-group"]
    },
  ]

  aws_auth_users = [
    {
      userarn  = "arn:aws:iam::66666666666:user/user1"
      username = "user1"
      groups   = ["custom-users-group"]
    },
  ]
}

В моєму випадку – для aws_auth_roles та aws_auth_users використовуємо значення з locals:

module "aws_auth" {
  source  = "terraform-aws-modules/eks/aws//modules/aws-auth"
  version = "~> 20.0"

  manage_aws_auth_configmap = true

  aws_auth_roles = local.aws_auth_roles

  aws_auth_users = var.eks_aws_auth_users
}

Виконуємо ще раз terraform init, ще раз робимо terraform plan, і тепер маємо новий ресурс для aws-auth:

А старий все ще буде видалятись – тобто, спочатку Terraform його видалить, а потім створить заново, просто з новим іменем та ID в стейті:

Аби Terraform не видалив ресурс aws-auth з кластеру – нам потрібно видалити його зі state-файла: тоді Terraform не буде нічого знати про цей ConfgiMap, а при створенні з нашого module "aws_auth" – просто створить записи у своєму state file, але не буде нічого виконувати в Kubernetes.

Important: про всяк випадок – зробіть бекап відповідного state-file, бо будемо робити state rm.

Видалення aws_auth з Terraform State

В моєму випадку треба перейти в каталог з оточенням, envs/test-1-28, і вже звідти виконувати операції зі state.

Note: уточнюю, бо випадково все ж таки видалив aws-auth ConfigMap з production-кластеру. Але просто заново виконав terraform apply на ньому – і все без проблем відновилось.

Знаходимо ім’я модуля, як він записаний в стейті:

$ cd envs/test-1-28/
$ terraform state list | grep aws_auth
module.atlas_eks.module.eks.kubernetes_config_map_v1_data.aws_auth[0]

Можна перевірити з terraform plan output, який робили вище, аби впевнитись, що видаляємо саме його:

...
module.atlas_eks.module.eks.kubernetes_config_map_v1_data.aws_auth[0]: Refreshing state... [id=kube-system/aws-auth]
...

Тут ім’я module.atlas_eks – саме так, як задано в моєму модулі EKS у файлі envs/test-1-28/main.tf:

module "atlas_eks" {
  source = "../../modules/atlas-eks"
...

З terraform state rm видаляємо запис про ресурс:

$ terraform state rm 'module.atlas_eks.module.eks.kubernetes_config_map_v1_data.aws_auth[0]'
Acquiring state lock. This may take a few moments...
Removed module.atlas_eks.module.eks.kubernetes_config_map_v1_data.aws_auth[0]
Successfully removed 1 resource instance(s).
Releasing state lock. This may take a few moments...

Виконуємо terraform plan ще раз, і тепер нема ніяких “destroy” – тільки створення module.atlas_eks.module.aws_auth.kubernetes_config_map_v1_data.aws_auth[0]:

 # module.atlas_eks.module.aws_auth.kubernetes_config_map_v1_data.aws_auth[0] will be created
+ resource "kubernetes_config_map_v1_data" "aws_auth" {
...
Plan: 1 to add, 10 to change, 0 to destroy.

Виконуємо terraform apply:

...
module.atlas_eks.module.eks.aws_eks_cluster.this[0]: Still modifying... [id=atlas-eks-test-1-28-cluster, 50s elapsed]
module.atlas_eks.module.eks.aws_eks_cluster.this[0]: Modifications complete after 52s [id=atlas-eks-test-1-28-cluster]
...
module.atlas_eks.module.aws_auth.kubernetes_config_map_v1_data.aws_auth[0]: Creation complete after 6s [id=kube-system/aws-auth]
...

Перевіряємо сам ConfigMap – ніяк змін:

$ kk -n kube-system get cm aws-auth -o yaml
apiVersion: v1
data:
  mapAccounts: |
    []
  mapRoles: |
    - "groups":
      - "system:masters"
      "rolearn": "arn:aws:iam::492***148:role/atlas-eks-test-1-28-masters-access-role"
      "username": "arn:aws:iam::492***148:role/atlas-eks-test-1-28-masters-access-role"
...

І глянемо, що змінилось в AWS Console > EKS > ClusterName > Access:

Тепер у нас тут значення EKS API and ConfigMap та дві Access Entries – для WorkerNodes, та для рутового юзера.

Перевіряємо NodeClaims – Karpenter працює?

Можна поскейлити ворклоади, аби впевнитись:

$ kk -n test-fastapi-app-ns scale deploy fastapi-app --replicas=20
deployment.apps/fastapi-app scaled

Логи Karpenter – все добре:

...
karpenter-8444499996-9njx6:controller {"level":"INFO","time":"2024-07-05T12:27:41.527Z","logger":"controller.provisioner","message":"created nodeclaim","commit":"a70b39e","nodepool":"default","nodeclaim":"default-59ms4","requests":{"cpu":"1170m","memory":"942Mi","pods":"8"},"instance-types":"c5.large, r5.large, t3.large, t3.medium, t3.small"}
...

І нові NodeClaims створились:

$ kk get nodeclaim
NAME            TYPE       ZONE         NODE                          READY   AGE
default-59ms4   t3.small   us-east-1a   ip-10-0-46-72.ec2.internal    True    59s
default-5fc2p   t3.small   us-east-1a   ip-10-0-39-114.ec2.internal   True    7m6s

Тут в принципі все – можемо переходити до апгрейду модуля з Karpenter.

Karpenter: upgrade 19.21 на 20.0

Також є Karpenter Diff of Before (v19.21) vs After (v20.0), але дещо довелося міняти вручну.

Тут я пішов “методом тика” – робимо terraform plan, дивимось, що не так в результатах – фіксимо – ше раз plan. Пройшло без проблем, хоча з деякими помилками – подивимось на них.

Поточний код Terraform для Karpenter

Поточний код в modules/atlas-eks/karpenter.tf:

module "karpenter" {
  source = "terraform-aws-modules/eks/aws//modules/karpenter"
  version = "19.21.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
}

Деякі outputs з модулю Karpenter використовуються в helm_releasemodule.karpenter.irsa_arn:

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
    replicas: 1
    settings:
      clusterName: ${module.eks.cluster_name}
      clusterEndpoint: ${module.eks.cluster_endpoint}
      interruptionQueueName: ${module.karpenter.queue_name}
      featureGates: 
        drift: true      
    serviceAccount:
      annotations:
        eks.amazonaws.com/role-arn: ${module.karpenter.irsa_arn} 
    EOT
  ]

  depends_on = [
    helm_release.karpenter_crd
  ]
}

Апгрейд модулю Karpenter

Для початку міняємо версію на 20.0:

module "karpenter" {
  source = "terraform-aws-modules/eks/aws//modules/karpenter"
  #version = "19.21.0"
  version = "20.0"
...

Робимо terraform init та terraform plan, і дивимось на помилки:

...
 An argument named "iam_role_arn" is not expected here.
...
│ An argument named "irsa_use_name_prefix" is not expected here.
...
│ An argument named "enable_karpenter_instance_profile_creation" is not expected here.

Міняємо імена параметрів, і з  Karpenter Diff of Before (v19.21) vs After (v20.0) додаємо створення ресурсів для IRSA (IAM Role for ServiceAccounts – роль для Karpenter), бо вона у нас є в поточному сетапі:

  • iam_role_arn => node_iam_role_arn
  • irsa_use_name_prefix – тут трохи не зрозумів, бо по документації вона стала iam_role_name_prefix, але iam_role_name_prefix в inputs нема взагалі, тому просто закоментив
  • iam_role_name_prefix – теж не поняв – по документації вона стала node_iam_role_name_prefix, але знов-таки – такої нема, тому теж просто закоментив
  • enable_karpenter_instance_profile_creation: видалена

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

module "karpenter" {
  source = "terraform-aws-modules/eks/aws//modules/karpenter"
  #version = "19.21.0"
  version = "20.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

  # 19 > 20
  #iam_role_arn         = module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_arn
  node_iam_role_arn         = module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_arn

  # 19 > 20
  #irsa_use_name_prefix = false
  #iam_role_name_prefix = false
  #node_iam_role_name_prefix = false

  # 19 > 20
  #enable_karpenter_instance_profile_creation = true

  # 19 > 20
  enable_irsa             = true
  create_instance_profile = true

  # 19 > 20
  # To avoid any resource re-creation
  iam_role_name          = "KarpenterIRSA-${module.eks.cluster_name}"
  iam_role_description   = "Karpenter IAM role for service account"
  iam_policy_name        = "KarpenterIRSA-${module.eks.cluster_name}"
  iam_policy_description = "Karpenter IAM role for service account"
}

Ще раз робимо terraform plan, і тепер маємо іншу помилку, тепер вже в helm_release:

...
This object does not have an attribute named "irsa_arn".
...

Бо irsa_arn тепер стала iam_role_arn, міняємо теж:

...
    serviceAccount:
      annotations:
        eks.amazonaws.com/role-arn: ${module.karpenter.iam_role_arn} 
    EOT
...

Ще раз робимо terraform plan – тепер маємо іншу проблему, з довжиною імені ролі:

...
Plan: 1 to add, 3 to change, 0 to destroy.
╷
│ Error: expected length of name_prefix to be in the range (1 - 38), got KarpenterIRSA-atlas-eks-test-1-28-cluster-
│ 
│   with module.atlas_eks.module.karpenter.aws_iam_role.controller[0],
│   on .terraform/modules/atlas_eks.karpenter/modules/karpenter/main.tf line 69, in resource "aws_iam_role" "controller":
│   69:   name_prefix = var.iam_role_use_name_prefix ? "${var.iam_role_name}-" : null
...

Тому задав iam_role_use_name_prefix = false, і тепер весь оновлений код виглядає так:

module "karpenter" {
  source = "terraform-aws-modules/eks/aws//modules/karpenter"
  #version = "19.21.0"
  version = "20.0"

  cluster_name = module.eks.cluster_name

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

  # 19 > 20
  #create_iam_role      = false
  create_node_iam_role = false

  # 19 > 20
  #iam_role_arn         = module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_arn
  node_iam_role_arn         = module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_arn

  # 19 > 20
  #irsa_use_name_prefix = false
  #iam_role_name_prefix = false
  #node_iam_role_name_prefix = false

  # 19 > 20
  #enable_karpenter_instance_profile_creation = true

  # 19 > 20
  enable_irsa             = true
  create_instance_profile = true

  # To avoid any resource re-creation
  iam_role_name          = "KarpenterIRSA-${module.eks.cluster_name}"
  iam_role_description   = "Karpenter IAM role for service account"
  iam_policy_name        = "KarpenterIRSA-${module.eks.cluster_name}"
  iam_policy_description = "Karpenter IAM role for service account"

  # expected length of name_prefix to be in the range (1 - 38), got KarpenterIRSA-atlas-eks-test-1-28-cluster-
  iam_role_use_name_prefix = false
}

...
resource "helm_release" "karpenter" {
  ...
    serviceAccount:
      annotations:
        eks.amazonaws.com/role-arn: ${module.karpenter.iam_role_arn} 
    EOT
  ]
  ...
}

Виконуємо terraform plan – нічого не має видалитись:

...
Plan: 1 to add, 5 to change, 0 to destroy.

В плані у нас є:

  • буде додано module.atlas_eks.module.karpenter.aws_eks_access_entry.node: – трохи забігаючи наперед – це треба буде відключити, зараз побачимо, чому
  • в module.atlas_eks.module.karpenter.aws_iam_policy.controller: будуть оновлені політики – тут наче все ОК.
  • в module.atlas_eks.module.karpenter.aws_iam_role.controller: буде додано правило Allow з pods.eks.amazonaws.com – для роботи з EKS Pod Identities

Наче виглядає ОК – давайте деплоїти і тестити.

Логи Karpenter запущені, NodeClaim зараз вже є, пінги на тестовий Ingress/ALB йдуть.

EKS: CreateAccessEntry – access entry resource is already in use on this cluster

Робимо terraform apply, і – як неочікувано! – маємо помилку:

...
│ Error: creating EKS Access Entry (atlas-eks-test-1-28-cluster:arn:aws:iam::492***148:role/test-1-28-default-eks-node-group-20240710092944387500000003): operation error EKS: CreateAccessEntry, https response error StatusCode: 409, RequestID: 004e014d-ebbb-4c60-919b-fb79629bf1ff, ResourceInUseException: The specified access entry resource is already in use on this cluster.

Тому що “EKS automatically adds access entries for the roles used by EKS managed node groups and Fargate profiles“, див. authentication_mode = “API_AND_CONFIG_MAP”.

І ми вже бачили Access Entry для WorkerNodes, коли виконували апгрейд версії модулю EKS:

Якщо використовуються self-managed NodeGroups – то для них в terraform-aws-eks/modules/self-managed-node-group/variables.tf була додана змінна create_access_entry з дефолтним значенням true.

Для Kaprneter теж є нова змінна create_access_entry, і вона теж з дефолтним значенням true.

Задаємо її в false, бо в моєму випадку ноди створенні з Karpenter використовують ту саму IAM Role, що ноди з module.eks.eks_managed_node_groups:

...
  # To avoid any resource re-creation
  iam_role_name          = "KarpenterIRSA-${module.eks.cluster_name}"
  iam_role_description   = "Karpenter IAM role for service account"
  iam_policy_name        = "KarpenterIRSA-${module.eks.cluster_name}"
  iam_policy_description = "Karpenter IAM role for service account"

  # expected length of name_prefix to be in the range (1 - 38), got KarpenterIRSA-atlas-eks-test-1-28-cluster-
  iam_role_use_name_prefix = false

  # Error: creating EKS Access Entry ResourceInUseException: The specified access entry resource is already in use on this cluster.
  create_access_entry = false
}
...

Ще раз виконуємо terraform apply – і тепер все пройшло без помилок.

В логах Karpenter взагалі нічого, тобто Kubernetes Pod не перестворювався, пінги на тестову апку продовжують йти.

Можна її поскейлити, аби тригернути створення нових Karpenter NodeClaims:

$ kk -n test-fastapi-app-ns scale deploy fastapi-app --replicas=10
deployment.apps/fastapi-app scaled

І вони створились без проблем:

...
karpenter-649945c6c5-lj2xh:controller {"level":"INFO","time":"2024-07-10T11:33:34.507Z","logger":"controller.nodeclaim.lifecycle","message":"launched nodeclaim","commit":"a70b39e","nodeclaim":"default-pn64t","provider-id":"aws:///us-east-1a/i-0316a99cd2d6e172c","instance-type":"t3.small","zone":"us-east-1a","capacity-type":"spot","allocatable":{"cpu":"1930m","ephemeral-storage":"17Gi","memory":"1418Mi","pods":"110"}}

Наче виглядає як все працює.

Фінальний Terraform код для EKS та Karpenter

В результаті всіх змін код буде виглядати так.

Для EKS:

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> v20.0"

  # is set in `locals` per env
  # '${var.project_name}-${var.eks_environment}-${local.eks_version}-cluster'
  # 'atlas-eks-test-1-28-cluster'
  # passed from a root module
  cluster_name    = "${var.env_name}-cluster"

  # passed from a root module
  cluster_version = var.eks_version

  # 'eks_params' passed from a root module
  cluster_endpoint_public_access = var.eks_params.cluster_endpoint_public_access

  # 'eks_params' passed from a root module
  cluster_enabled_log_types = var.eks_params.cluster_enabled_log_types

  # 'eks_addons_version' passed from a root module
  cluster_addons = {
    coredns = {
      addon_version = var.eks_addon_versions.coredns
      configuration_values = jsonencode({
        replicaCount = 1
        resources = {
          requests = {
            cpu = "50m"
            memory = "50Mi"
          }
        }
      })
    }
    kube-proxy = {
      addon_version = var.eks_addon_versions.kube_proxy
      configuration_values = jsonencode({
        resources = {
          requests = {
            cpu = "20m"
            memory = "50Mi"
          }
        }
      })
    }    
    vpc-cni = {
      # old: eks_addons_version
      # new: eks_addon_versions
      addon_version = var.eks_addon_versions.vpc_cni
      configuration_values = jsonencode({
        env = {
          ENABLE_PREFIX_DELEGATION = "true"
          WARM_PREFIX_TARGET       = "1"
          AWS_VPC_K8S_CNI_EXTERNALSNAT = "true"
        }
      })      
    }
    aws-ebs-csi-driver = {
      addon_version            = var.eks_addon_versions.aws_ebs_csi_driver
      # iam.tf
      service_account_role_arn = module.ebs_csi_irsa_role.iam_role_arn
    }
  }

  # make as one complex var?
  # passed from a root module
  vpc_id                   = var.vpc_id
  # for WorkerNodes
  # passed from a root module
  subnet_ids               = data.aws_subnets.private.ids
  # for the ControlPlane
  # passed from a root module
  control_plane_subnet_ids = data.aws_subnets.intra.ids

  # adding for API_AND_CONFIG_MAP
  # TODO: change to the "API" only after adding aws_eks_access_entry && aws_eks_access_policy_association
  authentication_mode = "API_AND_CONFIG_MAP"

  # `env_name` make too long name causing issues with IAM Role (?) names
  # thus, use a dedicated `env_name_short` var
  eks_managed_node_groups = {
    # eks-default-dev-1-28
    "${local.env_name_short}-default" = {

      # `eks_managed_node_group_params` from defaults here

      # number, e.g. 2
      min_size = var.eks_managed_node_group_params.default_group.min_size
      # number, e.g. 6
      max_size = var.eks_managed_node_group_params.default_group.max_size
      # number, e.g. 2
      desired_size = var.eks_managed_node_group_params.default_group.desired_size
      # list, e.g. ["t3.medium"]
      instance_types = var.eks_managed_node_group_params.default_group.instance_types
      # string, e.g. "ON_DEMAND"
      capacity_type = var.eks_managed_node_group_params.default_group.capacity_type

      # allow SSM
      iam_role_additional_policies = {
        AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
      }

      taints = var.eks_managed_node_group_params.default_group.taints

      update_config = {
        max_unavailable_percentage = var.eks_managed_node_group_params.default_group.max_unavailable_percentage
      }
    }
  }

  # 'atlas-eks-test-1-28-node-sg'
  node_security_group_name    = "${var.env_name}-node-sg"
  # 'atlas-eks-test-1-28-cluster-sg'
  cluster_security_group_name = "${var.env_name}-cluster-sg"

  # to use with EC2 Instance Connect
  node_security_group_additional_rules = {
    ingress_ssh_vpc = {
      description = "SSH from VPC"
      protocol    = "tcp"
      from_port   = 22
      to_port     = 22
      cidr_blocks      = [data.aws_vpc.eks_vpc.cidr_block]
      type        = "ingress"
    }
  }

  # 'atlas-eks-test-1-28'
  node_security_group_tags = {
    "karpenter.sh/discovery" = var.env_name
  }

  cluster_identity_providers = {
    sts = {
      client_id = "sts.amazonaws.com"
    }
  }
}

Для Karpenter:

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

  cluster_name = module.eks.cluster_name

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

  create_node_iam_role = false

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

  enable_irsa             = true
  create_instance_profile = true

  # backward compatibility with 19.21.0
  # see https://github.com/terraform-aws-modules/terraform-aws-eks/blob/master/docs/UPGRADE-20.0.md#karpenter-diff-of-before-v1921-vs-after-v200
  iam_role_name          = "KarpenterIRSA-${module.eks.cluster_name}"
  iam_role_description   = "Karpenter IAM role for service account"
  iam_policy_name        = "KarpenterIRSA-${module.eks.cluster_name}"
  iam_policy_description = "Karpenter IAM role for service account"

  iam_role_use_name_prefix = false

  # already created during EKS 19 > 20 upgrade with 'authentication_mode = "API_AND_CONFIG_MAP"'
  create_access_entry = false
}

...

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
    replicas: 1
    settings:
      clusterName: ${module.eks.cluster_name}
      clusterEndpoint: ${module.eks.cluster_endpoint}
      interruptionQueueName: ${module.karpenter.queue_name}
      featureGates: 
        drift: true
    serviceAccount:
      annotations:
        eks.amazonaws.com/role-arn: ${module.karpenter.iam_role_arn} 
    EOT
  ]

  depends_on = [
    helm_release.karpenter_crd
  ]
}

Ще треба буде оновити сам Karpenter з 0.32 на 0.37.

Підготовка до апгрейду з v20.0 на v21.0

Див. Upcoming Changes Planned in v21.0.

Що зміниться?

Основне – це зміни з aws-auth: модуль буде видалено, тому варто відразу вже переходити на authentication_mode = API, а для цього нам треба перенести всіх наших юзерів та ролі з aws-auth ConfigMap в EKS Access Entries.

Крім того, варто вже переходити на нову систему роботи з ServiceAccounts – EKS Pod Identities.

І виходить, що зміни буде дві:

  • в aws-auth є записи для IAM Roles та IAM Users: їх треба створити як EKS Access Entries
  • окремо маємо кілька IAM Roles, які використовуються в ServiceAccounts – їх треба перенести в Pod Identity associations
    • в Terraform для цього маємо новий ресурс aws_eks_pod_identity_association
    • а OIDC схоже не потрібен буде зовсім

Але все це я вже буду робити новим проектом в окремому репозиторії, який буде займатись IAM та доступом до EKS і RDS. Можливо, опишу в наступному пості.