Маємо AWS EKS кластер з WorkerNodes/EC2, які створюються за допомогою Karpenter.
Процес створення інфраструктури, кластеру та запуск Karpenter описаний у попередніх постах:
- Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints
- Terraform: створення EKS, частина 2 – EKS кластер, WorkerNodes та IAM
- Terraform: створення EKS, частина 3 – установка 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 без додаткових тєлодвіженій