Продовжуємо тему розгортання кластеру AWS Elastic Kubernetes Service за допомогою Terraform.
У першій частині підготували AWS VPC – див. Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints.
В цій частині розгорнемо сам кластер і налаштуємо AIM для нього, а в наступній – встановимо Karpenter та решту контроллерів.
Зміст
Планування
В цілому, список TODO наразі виглядає так:
- створити default NodeGroup з Taints
CrticalAddonsOnly=true
(див. Kubernetes: Pods та WorkerNodes – контроль розміщення подів на нодах) - створити StorageClass з
ReclaimPolicy=Retain
– для PVC, диски котрих треба зберігати при видаленні Deployment/StatefulSet - створити IAM “masters_access_role” з політикою
eks:DescribeCluster
для використанняaws eks update-kubeconfig
, щоб потім додавати юзерів - до
aws-auth
ConfigMap додати цю masters_access_role та мого IAM User як адмінів – поки не будемо ускладнювати з RBAC, бо “все тільки починається” (с) - створити OIDC Provider для кластеру
- додати subscription filter до EKS Cloudwatch Log Group, щоб збирати логи в Grafana Loki (див. Loki: збір логів з CloudWatch Logs з використанням Lambda Promtail)
- у самому кластері:
- встановити Karpenter
- встановити EKS EBS CSI Addon
- встановити ExternalDNS контролер
- встановити AWS Load Balancer Controller
- додати SecretStore CSI Driver та ASCP
- встановити Metrics Server
- і додати Vertical Pod Autoscaler та Horizontal Pod Autoscaler
Для кластеру також використаємо модуль, знову від @Anton Babenko – Terraform EKS module. Проте й інші модулі, наприклад – terraform-aws-eks
від Cookpad – я ним теж трохи користвувався, працював добре, але порівнювати не візьмусь.
Як і для модуля VPC, у Terraform EKS module теж маємо приклад кластеру та пов’язаних ресурсів – examples/complete/main.tf
.
Terraform Kubernetes provider
Для роботи модулю з aws-auth
ConfigMap потрібно буде додати ще один провайдер – kubernetes
.
У файлі providers.tf
додаємо його:
... provider "kubernetes" { host = module.eks.cluster_endpoint cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) exec { api_version = "client.authentication.k8s.io/v1beta1" command = "aws" args = ["--profile", "tf-admin", "eks", "get-token", "--cluster-name", module.eks.cluster_name] } }
Тут зверніть увагу, що в args передається AWS-профайл, бо сам кластер створюється Terraform від імені IAM Role:
... provider "aws" { region = "us-east-1" assume_role { role_arn = "arn:aws:iam::492***148:role/tf-admin" } ...
І AWS CLI Profile tf-admin
як раз теж виконує IAM Role Assume:
... [profile work] region = us-east-1 output = json [profile tf-admin] role_arn = arn:aws:iam::492***148:role/tf-admin source_profile = work ...
Error: The configmap “aws-auth” does not exist
Досить часта помилка, принаймні я неодноразово з нею стикався – коли під час виконання terraform apply
в кінці отримуємо цю помилку, а сама aws-auth
в кластері не створена.
Це призводить по-перше до того, що до кластеру не підключаються дефолтні WokrerNodes, по-друге – ми не можемо отримати доступ до кластеру з kubectl
, бо хоча aws eks update-kubeconfig
створює новий контекст в локальному ~/.kube/config
, сам kubectl
повертає помилку авторизації в кластері.
Продебажити це допомогло включення дебаг-логу Terraform через змінну TF_LOG=INFO
, де була сама помилка аутентифиікації провайдеру:
... [DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5: "kind": "Status", [DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5: "apiVersion": "v1", [DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5: "metadata": {}, [DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5: "status": "Failure", [DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5: "message": "Unauthorized", [DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5: "reason": "Unauthorized", [DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5: "code": 401 [DEBUG] provider.terraform-provider-kubernetes_v2.23.0_x5: } ...
Помилка виникала саме через те, що в args
провайдеру не було задано правильний локальний профайл.
Є інший варіант аутентифікації – через token
, див. цей коментар в GitHub Issues.
Але з ним були проблеми при створенні кластеру з нуля, бо Терраформ не міг виконанти data "aws_eks_cluster_auth"
. Треба ще якось спробувати, бо в цілому ідея з токеном мені подобається більше, ніж через AWS CLI. З іншого боку – у нас ще будуть провайдери kubectl
та helm
, і не факт, що їх можна аутентифікувати через токен (хоча, скоріш за все можно, але треба покопатись).
Terraform Kubernetes module
Окей, з провайдером розібрались – давайте додавати сам модуль.
Спочатку запустимо сам кластер с однією NodeGroup, а потім вже будемо додавати всякі контроллери.
Типи EKS NodeGroups
AWS має два типи NodeGroups – Self-Managed, та Amazon Managed, див. Amazon EKS nodes.
Головна, як на мене, перевага Amazon Managed це те, що ви не маєте перейматись оновленнями – все, що стосується операційної системи і компонентів самого Kubernetes, бере на себе Амазон:
Хоча якщо робити Self-managed Nodes використовуючи AMI від самого AWS з Amazon Linux – то там все вже буде налаштовано, і навіть для апдейтів достатньо ребутнути чи перестворити ЕС2 – тоді він запуститься з AMI з останніми патчами.
Окремо варто загадти Fargate – див. AWS: Fargate – можливості, порівняння з Lambda/EC2 та використання з AWS EKS, але я не бачу в них якогось великого сенсу, тим більш на них не зможемо створювати DaemonSets з, наприклад, Promtail для логів.
Також, Managed NodeGroups не потребують окремих налаштувать у aws-auth
ConfigMap – EKS сам додасть необхідні записи.
Anayway, щоб полегшити собі життя – будемо використовувати Amazon Managed Nodes. На цих нодах будуть жити тільки контроллери – “Critical Addons”, а ноди для ворклоадів будуть менеджитись Karpenter-ом.
Terraform EKS variables
Спершу нам потрібні будуть змінні.
Взагалі добре пройтись по всім inputs, і подивитись що можна налаштувати під себе.
Для мінімального конфігу нам знадобляться:
cluster_endpoint_public_access
– boolcluster_enabled_log_types
– listeks_managed_node_groups
:min_size
,max_size
таdesired_size
– number-
instance_types
– list capacity_type
– stringmax_unavailable_percentage
– number
aws_auth_roles
– mapaws_auth_users
– map
Поділимо змінні на три групи – одна для самого EKS, друга – з параметрами для NodeGroups, і третя – для IAM Users.
Описуємо першу змінну – с параметрами для самого EKS:
... variable "eks_params" { description = "EKS cluster itslef parameters" type = object({ cluster_endpoint_public_access = bool cluster_enabled_log_types = list(string) }) }
Та terraform.tfvars
зі значеннями – поки включимо всі логи, потім залишимо тільки реально потрібні:
... eks_params = { cluster_endpoint_public_access = true cluster_enabled_log_types = ["audit", "api", "authenticator", "controllerManager", "scheduler"] }
Далі, параметри для NodeGroups. Створимо об’єкт типу map
, в якому зможемо додавати конфігурції для декількох груп, які будемо тримати в елементах з типом object
, бо параметри будуть різних типів:
... variable "eks_managed_node_group_params" { description = "EKS Managed NodeGroups setting, one item in the map() per each dedicated NodeGroup" type = map(object({ min_size = number max_size = number desired_size = number instance_types = list(string) capacity_type = string taints = set(map(string)) max_unavailable_percentage = number })) }
Приклад додавання Taints є тут>>>, тож описуємо їх та інші параметри у tfvars
:
... eks_managed_node_group_params = { default_group = { min_size = 2 max_size = 6 desired_size = 2 instance_types = ["t3.medium"] capacity_type = "ON_DEMAND" taints = [ { key = "CriticalAddonsOnly" value = "true" effect = "NO_SCHEDULE" }, { key = "CriticalAddonsOnly" value = "true" effect = "NO_EXECUTE" } ] max_unavailable_percentage = 50 } }
І третя группа – список IAM юзерів, котрі будуть додані до aws-auth
ConfgiMap для доступу до кластеру. Тут використовуємо тип set
з ще одним object
, бо для юзера потрібно буде передавати list
зі список RBAC-груп:
... variable "eks_aws_auth_users" { description = "IAM Users to be added to the aws-auth ConfigMap, one item in the set() per each IAM User" type = set(object({ userarn = string username = string groups = list(string) })) }
Значення в tfvars
:
... eks_aws_auth_users = [ { userarn = "arn:aws:iam::492***148:user/arseny" username = "arseny" groups = ["system:masters"] } ]
Як і з NodeGroups, тут ми зможемо задати кілька юзерів, і всі вони потім будуть передані до aws_auth_users
модулю EKS.
Створення кластеру
Створюємо файл eks.tf
, додаємо модуль:
module "eks" { source = "terraform-aws-modules/eks/aws" version = "~> 19.0" cluster_name = "${local.env_name}-cluster" cluster_version = var.eks_version cluster_endpoint_public_access = var.eks_params.cluster_endpoint_public_access cluster_enabled_log_types = var.eks_params.cluster_enabled_log_types cluster_addons = { coredns = { most_recent = true } kube-proxy = { most_recent = true } vpc-cni = { most_recent = true } } vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnets control_plane_subnet_ids = module.vpc.intra_subnets manage_aws_auth_configmap = true eks_managed_node_groups = { default = { min_size = var.eks_managed_node_group_params.default_group.min_size max_size = var.eks_managed_node_group_params.default_group.max_size desired_size = var.eks_managed_node_group_params.default_group.desired_size instance_types = var.eks_managed_node_group_params.default_group.instance_types capacity_type = var.eks_managed_node_group_params.default_group.capacity_type 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 } } } cluster_identity_providers = { sts = { client_id = "sts.amazonaws.com" } } aws_auth_users = var.eks_aws_auth_users #aws_auth_roles = TODO }
Якщо для Addons треба додати параметри – можна зробити з configuration_values
, див. приклад тут>>>.
Додамо трохи outputs:
... output "eks_cloudwatch_log_group_arn" { value = module.eks.cloudwatch_log_group_arn } output "eks_cluster_arn" { value = module.eks.cluster_arn } output "eks_cluster_endpoint" { value = module.eks.cluster_endpoint } output "eks_cluster_iam_role_arn" { value = module.eks.cluster_iam_role_arn } output "eks_cluster_oidc_issuer_url" { value = module.eks.cluster_oidc_issuer_url } output "eks_oidc_provider" { value = module.eks.oidc_provider } output "eks_oidc_provider_arn" { value = module.eks.oidc_provider_arn }
Перевіряємо з terraform plan
, деплоїмо, та перевіряємо сам кластер:
Створюємо ~/.kube/config
:
$ aws --profile work --region us-east-1 eks update-kubeconfig --name atlas-eks-dev-1-27-cluster --alias atlas-eks-dev-1-27-work-profile Updated context atlas-eks-dev-1-27-work-profile in /home/setevoy/.kube/config
Та перевіряємо доступ з can-i
:
$ kubectl auth can-i get pod yes
Додаткова IAM Role
Окремо створимо IAM Role з політикою eks:DescribeCluster
, і підключимо її до кластеру в групу system:masters
– використовуючи цю роль, інші юзери зможуть проходити авторизацію в кластері.
В роль нам потрібно буде передати AWS Account ID, щоб в Principal
обмежити можливість виконання AssumeRole тільки юзерами цього акаунту.
Щоб не виносити це окремою змінною в variables.tf
– в eks.tf
додамо ресурс data "aws_caller_identity"
:
... data "aws_caller_identity" "current" {}
І далі описуємо саму роль з assume_role_policy
– кому буде дозволено assume цієї ролі, та inline_policy
з дозволом на виконання eks:DescribeCluster
:
... resource "aws_iam_role" "eks_masters_access_role" { name = "${local.env_name}-masters-access-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Sid = "" Principal = { AWS: "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" } } ] }) inline_policy { name = "${local.env_name}-masters-access-policy" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = ["eks:DescribeCluster*"] Effect = "Allow" Resource = "*" }, ] }) } tags = { Name = "${local.env_name}-access-role" } }
Повертаємось до module "eks"
і в aws_auth_roles
додаємо маппінг цієї ролі:
... aws_auth_users = var.eks_aws_auth_users aws_auth_roles = [ { rolearn = aws_iam_role.eks_masters_access_role.arn username = aws_iam_role.eks_masters_access_role.arn groups = ["system:masters"] } ] ...
Додамо output
:
... output "eks_masters_access_role" { value = aws_iam_role.eks_masters_access_role.arn }
Деплоїмо зміни:
$ terraform apply ... Outputs: ... eks_masters_access_role = "arn:aws:iam::492***148:role/atlas-eks-dev-1-27-masters-access-role" ...
Перевіряємо саму aws-auth
ConfigMap:
$ kk -n kube-system get cm aws-auth -o yaml apiVersion: v1 data: ... mapRoles: | - "groups": - "system:bootstrappers" - "system:nodes" "rolearn": "arn:aws:iam::492***148:role/default-eks-node-group-20230907145056376500000001" "username": "system:node:{{EC2PrivateDNSName}}" - "groups": - "system:masters" "rolearn": "arn:aws:iam::492***148:role/atlas-eks-dev-1-27-masters-access-role" "username": "arn:aws:iam::492***148:role/atlas-eks-dev-1-27-masters-access-role" mapUsers: | - "groups": - "system:masters" "userarn": "arn:aws:iam::492***148:user/arseny" "username": "arseny" ...
Додаємо новий профайл до ~/.aws/confing
:
... [profile work] region = us-east-1 output = json [profile eks-1-27-masters-role] role_arn = arn:aws:iam::492***148:role/atlas-eks-dev-1-27-masters-access-role source_profile = work
Додаємо новий контекст для kubectl
:
$ aws --profile eks-1-27-masters-role --region us-east-1 eks update-kubeconfig --name atlas-eks-dev-1-27-cluster --alias eks-1-27-masters-role Updated context eks-1-27-masters-role in /home/setevoy/.kube/config
І перевіряємо доступ:
$ kubectl auth can-i get pod yes $ kubectl get pod -A NAMESPACE NAME READY STATUS RESTARTS AGE kube-system aws-node-99gg6 2/2 Running 0 41h kube-system aws-node-bllg2 2/2 Running 0 41h ...
В наступній частині вже встановимо решту – Karpenter та різні Controllers.
Помилка Get “http://localhost/api/v1/namespaces/kube-system/configmaps/aws-auth”: dial tcp: lookup localhost on 10.0.0.1:53: no such host
Під час тестів перестворював кластер, щоб впевнитись, що весь код, описаний тут, працює.
І при видаленні кластеру Terraform видавав помилку:
... Plan: 0 to add, 0 to change, 34 to destroy. ... ╷ │ Error: Get "http://localhost/api/v1/namespaces/kube-system/configmaps/aws-auth": dial tcp: lookup localhost on 10.0.0.1:53: no such host │ ...
Рішення – видалити aws-auth
зі стейт-файлу:
$ terraform state rm module.eks.kubernetes_config_map_v1_data.aws_auth[0]
Ясна річ, що робити це треба тільки для тестового кластеру, а не Production.