AWS: Karpenter та SSH для Kubernetes WorkerNodes

Автор |  19/06/2024
 

Маємо AWS EKS кластер з WorkerNodes/EC2, які створюються за допомогою Karpenter.

Процес створення інфраструктури, кластеру та запуск Karpenter описаний у попередніх постах:

Чого прям ну дуже не вистачає в цій системі – це доступу до серверів по SSH, без якого почуваєшся… Ну, наче DevOps, а не Infrastructure Engineer. Короче – доступ по SSH іноді прям треба, але – сюрпрайз – Karpenter з коробки не дає можливості додати ключ на ноди, які він менеджить.

Хоча, здавалося б – в чому проблема в EC2NodeClass передати ключ, як це робиться в Terraform resource "aws_instance" з параметром key_name?

Але – ОК. Нема, то й нема. Можливо, додадуть пізніше.

Натомість в документації Can I add SSH keys to a NodePool? пропонується використати або AWS Systems Manager Session Manager, або AWS EC2 Instance Connect, або “the old school way” – додати публічну частину ключа через AWS EC2 User Data, і підключатись через bastion-хост або VPN.

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

Варіант 1: AWS Systems Manager Session Manager та SSH на EC2

AWS Systems Manager Session Manager використовується для менеджменту EC2-інстансів. Взагалі він вміє досить багато, наприклад – слідкувати за патчами і апдейтами для пакетів, які встановлені на інстансах.

Зараз він нас цікавить тільки як система, яка дозволить виконати SSH на Kubernetes WorkerNode.

Для роботи потребує SSM-агента, який по дефолту є на всіх інстансах з Amazon Linux AMI.

Знаходимо ноди, які створені Kaprneter (у нас є окрема лейбла для них):

$ kubectl get node -l created-by=karpenter
NAME                          STATUS   ROLES    AGE     VERSION
ip-10-0-34-239.ec2.internal   Ready    <none>   21h     v1.28.8-eks-ae9a62a
ip-10-0-35-100.ec2.internal   Ready    <none>   9m28s   v1.28.8-eks-ae9a62a
ip-10-0-39-0.ec2.internal     Ready    <none>   78m     v1.28.8-eks-ae9a62a
...

Отримуємо Instance ID:

$ kubectl get node ip-10-0-34-239.ec2.internal -o json | jq -r ".spec.providerID" | cut -d \/ -f5 
i-011b1c0b5857b0d92

AWS CLI: TargetNotConnected when calling the StartSession operation

Пробуємо підключитись – і отримуємо помилку “TargetNotConnected“:

$ aws --profile work ssm start-session --target i-011b1c0b5857b0d92

An error occurred (TargetNotConnected) when calling the StartSession operation: i-011b1c0b5857b0d92 is not connected.

Або через AWS Console:

Але і тут маємо помилку підключення – “SSM Agent is not online“:

Помилка виникає через те, що:

  • або в IAM-ролі, яка підключена до інстансу, нема дозволу на SSM
  • або EC2 запущено в приватній мережі, і агент не може підключитись до зовнішнього ендпоінту

SessionManager та політики для IAM

Перевіряємо – знаходимо IAM Role:

І підключені до неї політики – про SSM нема нічого:

Редагуємо політику – поки що руками, потім зробимо в коді Terraform:

Підключаємо AmazonSSMManagedInstanceCore:

І за хвилину-дві пробуємо ще раз:

SessionManager та VPC Endpoint

Інша можлива причина проблем підключення SSM-агенту до AWS – нема доступу з інстансу до ендпоінтів SSM:

  • ssm.region.amazonaws.com
  • ssmmessages.region.amazonaws.com
  • ec2messages.region.amazonaws.com

Якщо сабнет приватний, і має ліміти на зовнішні підключення – то можливо треба створити VPC Endpoint для SSM.

Див. SSM Agent is not online та Troubleshooting Session Manager.

AWS CLI: SessionManagerPlugin is not found

Але після фіксу IAM при підключенні з робочої машини з AWS CLI маємо помилку “SessionManagerPlugin is not found“:

$ aws --profile work ssm start-session --target i-011b1c0b5857b0d92

SessionManagerPlugin is not found. Please refer to SessionManager Documentation here: http://docs.aws.amazon.com/console/systems-manager/session-manager-plugin-not-found

Встановлюємо його локально – див. документацію Install the Session Manager plugin for the AWS CLI.

Для Arch Linux є пакет aws-session-manager-plugin в AUR:

$ yay -S aws-session-manager-plugin

І тепер можемо підключитись:

$ aws --profile work ssm start-session --target i-011b1c0b5857b0d92

Starting session with SessionId: arseny-33ahofrlx7bwlecul2mkvq46gy
sh-4.2$ 

Залишилось додати це в автоматизацію.

Terraform: EKS module та додавання IAM Policy

Для модулю EKS політику можемо додати через параметр iam_role_additional_policies – див. node_groups.tf і в прикладах AWS EKS Terraform module:

В модулі версії 20.0 ім’я параметру змінилось – iam_role_additional_policies => node_iam_role_additional_policies, але у нас поки що версія 19.21.0, і роль додається таким чином:

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

  cluster_name    = local.env_name
  cluster_version = var.eks_version
  ...
  vpc_id                   = local.vpc_out.vpc_id
  subnet_ids               = data.aws_subnets.private.ids
  control_plane_subnet_ids = data.aws_subnets.intra.ids

  manage_aws_auth_configmap = true

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

Відключаємо те, що робили руками, деплоїмо Terraform, перевіряємо – політика додана:

І підключення працює:

$ aws --profile work ssm start-session --target i-011b1c0b5857b0d92

Starting session with SessionId: arseny-pt7d44xp6ibvqcezj2oqjaxv5q
sh-4.2$ bash
[ssm-user@ip-10-0-34-239 bin]$ pwd
/usr/bin

Варіант 2: AWS EC2 Instance Connect та SSH на EC2

Інший варіант підключення – через EC2 Instance Connect. Документація – Connect to your Linux instance with EC2 Instance Connect.

Також потребує агента, який також по дефолту є на Amazon Linux.

Для інстансів в приватних мережах для підключення потребує EC2 Instance Connect VPC Endpoint.

SecurityGroup та SSH

Для Instance Connect через ендпоінт потрібен доступ до порта 22, SSH (на відміну від SSM, який відкриває підключення через самого агента).

Відкриваємо порт для всіх адрес у VPC:

EC2 Instance Connect VPC Endpoint

Переходимо в VPC > Endpoints, створюємо ендпоінт:

Вибираємо тип EC2 Instance Connect Endpoint, саму VPC, та SecurityGroup:

Вибираємо Subnet – у нас більшість ресурсів в us-east-1a, тому візьмемо її, аби не ганяти зайвий cross-AvailabilityZone трафік (див. AWS: Cost optimization – обзор расходов на сервисы и стоимость трафика в AWS):

Кілька хвилин чекаємо статус Active:

І підключаємось з AWS CLI вказуючи --connection-type eice, бо інстанси в приватній мережі:

$ aws --profile work ec2-instance-connect ssh --instance-id i-011b1c0b5857b0d92 --connection-type eice
...
[ec2-user@ip-10-0-34-239 ~]$ 

Terraform: EC2 Instance Connect, EKS та VPC

Для Terraform тут буде потрібно в модулі EKS додавати node_security_group_additional_rules для доступу по SSH, і для VPC створювати EC2 Instance Connect Endpoint, бо у нас VPC та EKS створюються окремо.

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

  cluster_name    = local.env_name
  cluster_version = var.eks_version

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

  node_security_group_name    = "${local.env_name}-node-sg"
  cluster_security_group_name = "${local.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      = [local.vpc_out.vpc_cidr]
      type        = "ingress"
    }
  }

  node_security_group_tags = {
    "karpenter.sh/discovery" = local.env_name
  }
  ...
}
...

Якщо створювали руками, як описано вище – то з SecurityGroup видаляємо правило з SSH, і деплоїмо з Terraform.

Для VPC EC2 Ednpoint я не знайшов, як це зробити через модуль Антона Бабенко terraform-aws-modules/vpc, але можна зробити окремим ресурсом через aws_ec2_instance_connect_endpoint:

resource "aws_ec2_instance_connect_endpoint" "example" {
  subnet_id = module.vpc.private_subnets[0]
  security_group_ids = ["sg-0b70cfd6019c635af"]
}

Втім, тут треба передавати SecurityGroup ID з кластеру, а кластер у нас створюється після VPC, тому виникає проблема “куриця-яйце”.

Взагалі з Instance Connect виглядає якось трохи більш складно, чим з SSM, бо більше змін в коді, ще й в різних модулях.

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

Варіант 3: дідовський спосіб з SSH Public Key через EC2 User Data

Ну і самий старий і, можливо, простий варіант – це самому створити SSH-ключ, і додавати його публічну частину на EC2 при створені інстансу.

З недоліків тут те, що додавати таким чином багато ключів буде складно, та й взагалі EC2 User Data іноді може вилізти боком, але якщо потрібно додати тільки один ключ, якийсь “супер-адмін” на крайній випадок – то цілком валідний варіант.

Тим більш, якщо у вас є VPN до VPC (див.Pritunl: запуск VPN в AWS на EC2 з Terraform) – то підключення буде ще простішим.

Створюємо ключ:

$ ssh-keygen 
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/setevoy/.ssh/id_ed25519): /home/setevoy/.ssh/atlas-eks-ec2
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/setevoy/.ssh/atlas-eks-ec2
Your public key has been saved in /home/setevoy/.ssh/atlas-eks-ec2.pub
...

Публічну частину можемо зберігати в репозиторії – копіюємо її:

$ cat ~/.ssh/atlas-eks-ec2.pub 
ssh-ed25519 AAA***VMO setevoy@setevoy-wrk-laptop

Далі трохи костилів: EC2NodeClass у нас створюється з Terraform через ресурс kubectl_manifest. Найпростіший варіант, який поки що прийшов в голову – це додати публічний ключ в variables, і потім використати в kubectl_manifest.

Пізніше мабуть перенесу такі ресурси в окремий Helm-чарт, і зроблю більш красиво.

Поки що створюємо нову змінну:

variable "karpenter_nodeclass_ssh" {
  type        = string
  default = "ssh-ed25519 AAA***VMO setevoy@setevoy-wrk-laptop"
  description = "SSH Public key for EC2 created by Karpenter"
}

В конфіг EC2NodeClass додаємо spec.userData:

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}
      userData: |
        #!/bin/bash
        mkdir -p ~ec2-user/.ssh/
        touch ~ec2-user/.ssh/authorized_keys
        echo "${var.karpenter_nodeclass_ssh}" >> ~ec2-user/.ssh/authorized_keys
        chmod -R go-w ~ec2-user/.ssh/authorized_keys
        chown -R ec2-user ~ec2-user/.ssh       
  YAML

  depends_on = [
    helm_release.karpenter
  ]
}

Якщо використовується не Amazon Linux – то міняємо ec2-user на потрібного.

Майте на увазі, що зміни в EC2NodeClass призведуть до перестворення всіх інстансів, і що ваші сервіси сконфігуровані для стабільної роботи, див. Kubernetes: забезпечення High Availability для Pods.

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

$ kk get ec2nodeclass -o yaml
...
    userData: #!/bin/bash\nmkdir -p ~ec2-user/.ssh/\ntouch ~ec2-user/.ssh/authorized_keys\necho
      \"ssh-ed25519 AAA***VMO setevoy@setevoy-wrk-laptop\" >> ~ec2-user/.ssh/authorized_keys\nchmod -R go-w
      ~ec2-user/.ssh/authorized_keys\nchown -R ec2-user ~ec2-user/.ssh       \n
...

Чекаємо, коли Karpenter заскейлить якусь нову WorkerNode, і пробуємо SSH:

$ ssh -i ~/.ssh/hOS/atlas-eks-ec2 [email protected]
...
[ec2-user@ip-10-0-39-73 ~]$ 

Готово.

Висновки

  • AWS SessionManager: виглядає як найпростіший варіант з точку зору автоматизації, рекомендований самим AWS, але треба подумати, як робити той же scp через нього (хоча це начебто можливо через додаткові костилі – див. .SSH and SCP with AWS SSM)
  • AWS EC2 Instance Connect: прикольна фіча від Амазону, але якось більш геморно в автоматизації, тому не наш варіант
  • “дідовський” SSH: ну, старе – перевірене 🙂 але я не дуже люблю User Data, бо іноді може призвести до проблем з запуском інстансів; втім – теж простий з точки зору автоматиазції, і дає звичний SSH без додаткових тєлодвіженій