Отже, маємо кластер AWS Elastic Kubernetes Service з Authentication mode EKS API and ConfigMap, який ми включили під час апгрейду Terraform-модуля з версії 19.21 на 20.0.
Перед тим, як переключати EKS Authentication mode повністю на API – нам потрібно з aws-auth
ConfgiMap перенести всіх юзерів і ролі в Access Entries EKS-кластера.
І ідея зараз така, щоб створити окремий проект Terraform, назвемо його “atlas-iam“, в якому ми будемо менеджити всі IAM-доступи – і для EKS з Access Entries та Pod Identities, і для RDS з IAM database authentication, і, можливо, потім сюди ж перенесемо і юзер-менеджмент взагалі.
Всі три схеми розбирав детальніше:
- AWS: EKS Pod Identities – заміна IRSA? Спрощуємо менеджмент IAM доступів
- AWS: Kubernetes та Access Management API – нова схема авторизації в EKS
- AWS: RDS з IAM database authentication, EKS Pod Identities та Terraform
Не знаю, наскільки описана нижче схема зайде нам в майбутньому Production, але в цілому мені ідея поки що подобається – окрім проблеми з динамічними іменами для EKS Pod Identities.
Отже, поглянемо як ми з Terraform можемо реалізувати автоматизацію управління доступом для IAM Users та IAM Roles до EKS Cluster з EKS Access Entries, та як у Terraform можна створювати EKS Pod Identities для ServiceAccounts. Про додавання RDS сьогодні говорити не будемо – але його будемо мати на увазі при плануванні.
Все описане нижче – скоріш чернетка того, як воно буде, і скоріш за все якісь апдейти по ходу реалізації будуть робитись. Але загальна ідея може бути приблизно такою.
Зміст
EKS Authentification та IAM: the current state
Зараз в aws-auth
ConfgiMap зараз маємо:
- IAM Users: звичайні юзери, які ходять в Kubernetes
- IAM Roles: ролі для доступу в кластер з GitHub Actions
І всі з правами system:master
– заодно наведемо трохи порядок в цьому.
Окремо зараз в проектах (окремі репозиторії для Backend API, моніторинг і т.д.) для відповідних Kubernetes Pods створюються IAM Roles та Kubernetes ServiceAccounts, які теж хочеться звідти винести в цей новий проект, і управляти з одного місця з EKS Pod Identity associations.
Тож наразі у нас по EKS дві задачі:
- створити EKS Authentification API Access Entries для юзерів та GitHub ролей
- створити Pod Identity associations для ServiceAccounts
Планування проекту
Головне питання тут – на якому рівні будемо менеджити? На рівні AWS-акаунтів – чи на рівні EKS-кластерів/RDS? А від цього будуть залежати і структура коду, і змінні.
Multiple AWS accounts з одним EKS та RDS оточеннями
Варіант 1 – якщо маємо декілька AWS-акаунтів, то можемо управляти на рівні акаунтів: тоді для кожного акаунта можемо створити змінну зі списком EKS-кластерів, змінну зі списком юзерів/ролей, і потім в циклах створювати відповідні ресурси.
Тоді структура файлів Terraform може бути такою:
providers.tf
: описуємо AWS provider зassume_role
для AWS-акаунту Dev/Staging/Prod (або використовуємо якийсь Terragrunt)variables.tf
: наші змінні з дефолтними значеннямиenvs/
: каталог з іменами AWS-акаунтівaws-env-dev
: каталог конкретного акаунтуdev.tfvars
: значення для змінних з іменами EKS-кластерів і списком юзерів в цьому AWS-акаунті
eks_access_entires.tf
: ресурси для EKS- …
І потім в коді в циклі проходимось по всім кластерам та всім юзерам, які задані в envs/aws-env-dev/dev.tfvars
.
Але тут буде питання в тому, як створювати юзерів на кластерах:
- або мати однакових юзерів і пермішени на всіх кластерах в акаунті, що, в принципі, ОК, якщо маємо AWS-мультіакаунт, і в кожному умовному AWS-Dev у нас тільки EKS-Dev
- або створювати декілька ресурсів
access_entries
– під кожен кластер окремо, і вже вaccess_entries
в циклі проходитись по групам юзерів для конкретного кластеру – якщо в AWS-Dev у нас окрім EKS-Dev якісь додаткові кластери EKS, де треба мати окремий набір юзерів
В моєму випадку у нас поки що один AWS-акаунт з одним Kubernetes-кластером, але пізніше скоріш за все ми будемо їх розділяти та створювати окремі акаунти під Ops/Dev/Staging/Prod. Тоді просто додамо нове оточення в каталог envs/
, і там опишемо новий EKS-кластер(и).
Multiple EKS та RDS clusters в одному AWS-акаунті
Варіант 2 – якщо AWS account один на всі EKS-оточення, то можна всім управляти на рівні кластерів, і тоді структура може бути такою:
providers.tf
: описуємо AWS provider зassume_role
для AWS-акаунту Mainvariables.tf
: наші змінні з дефолтними значеннямиenvs/
: каталог з іменами EKS-кластерівdev/
: каталог для EKS Devdev-eks-cluster.tfvars
– зі списком юзерів EKS Devdev-rds-backend.tfvars
– зі списком юзерів RDS Dev
prod/
prod-eks-cluster.tfvars
– зі списком юзерів EKS Prodprod-rds-backend.tfvars
– зі списком юзерів RDS Prod
eks_access_entires.tf
: ресурси для EKS- …
Можна взагалі все “огорнути” в модулі, і потім в циклах викликати саме модулі.
User permissions
Крім того, ще подумаємо про те, які права яким юзерам можуть знадобитись? Це треба, аби далі продумати структуру змінних і цикли for_each
в ресурсах:
- група
devops
: будуть кластер-адмінами - група
backend
:- права
edit
в усіх Namespaces з іменами “backend“ - права
read-only
на якісь обрані неймспейси
- права
- група
qa
:- права
edit
в усіх Namespaces з іменами “qa“ - права
read-only
на якісь обрані неймспейси
- права
Тепер, маючи уявлення про те, що нам треба – можна починати створювати файли Terraform і формувати змінні.
Структура файлів Terraform
У нас вже склалась однакова схема на всіх проектах, і новий буде виглядати так – аби не ускладнювати поки що вирішив без модулів, далі побачимо:
$ tree terraform/ terraform/ |-- Makefile |-- backend.tf |-- envs | `-- ops | `-- atlas-iam.tfvars |-- iam_eks_access_entires.tf |-- iam_eks_pod_identities.tf |-- providers.tf |-- variables.tf `-- versions.tf
Тут:
- в
Makefile
: командиterraform init
/plan
/apply
– і для виклику локально, і для CI/CD backend.tf
: S3 для стейт-файлів, DynamoDB для state-lockenvs/ops
: файлиtfvars
зі списками кластерів та юзерів в цьому AWS-акаунтіproviders.tf
: тутprovider "aws"
з дефолтними тегами і необхідними параметрами- в моєму випадку
provider "aws"
бере значення змінної оточенняAWS_PROFILE
для визначення того, на який акаунт він має підключатись, а вAWS_PFOFILE
задається регіон та необхідна IAM Role
- в моєму випадку
variables.tf
: змінні з дефолтними значеннямиversions.tf
: версії самого Terraform та AWS Provider
І сюди ж потім можна буде додати файл типу iam_rds_auth.tf
.
Terraform та EKS Access Management API
Почнемо з основного – доступ юзерів.
Для цього нам потрібно:
- є юзер, який може належати до однії з груп –
devops
,backend
,qa
- йому потрібно задати тип прав доступу –
admin
,edit
,read-only
- і ці права видати або на весь кластер – для девопсів, або на конкретний неймспейс(и) – для
backend
таqa
Отже, будуть два типи ресурсів – aws_eks_access_entry
та eks_access_policy_association
:
- в
aws_eks_access_entry
: описуємо EKS Access Entity – IAM User, для якого налаштовуємо доступ, та ім’я кластера, до якого доступ буде додаватись; параметри тут будуть:cluster name
principal-arn
- в
eks_access_policy_association
– описуємо тип доступу – admin/edit/etc та scope – cluster-wide, або конкретний неймспейс; параметри тут будуть:cluster-name
principal-arn
policy-arn
access-scope
Створення variables
Див. Terraform: знайомство з типами даних – primitives та complex.
Тож які змінні нам будуть потрібні:
- список кластерів – тут можна просто
list()
:-
atlas-eks-ops-1-28-cluster
-
atlas-eks-test-1-28-cluster
-
- списки з юзерами, різні списки для різних груп – можна зробити однієї змінною типу
map()
:devops
:-
arn:aws:iam::111222333:user/user-1
- arn:aws:iam::111222333:user/user-2
-
backend
:- arn:aws:iam::111222333:user/user-3
- arn:aws:iam::111222333:user/user-4
- список з EKS Cluster Access Policies – хоча вони дефолтні, і змінюватись навряд чи будуть, але задля загального шаблону давайте теж зробимо окремою змінною
access-scope
дляaws_eks_access_policy_association
– перепробував різні варіанти, але в результаті зробив без овер-інжинірінгу – можемо зробити змінну типуmap(object)
:- група
devops
:aws_eks_access_policy_association
буде задаватись зaccess_scope = cluster
- група
backend
:- список неймпспейсів, де всі члени групи будуть мати права
edit
- список неймпспейсів, де всі члени групи будуть мати права
read-only
- список неймпспейсів, де всі члени групи будуть мати права
- група
qa
:- список неймпспейсів, де всі члени групи будуть мати права
edit
- список неймпспейсів, де всі члени групи будуть мати права
read-only
- список неймпспейсів, де всі члени групи будуть мати права
- група
І потім створимо aws_eks_access_policy_association
декількома ресурсами для кожної групи окремо. Далі побачимо як саме.
Поїхали – у файлі variables.tf
описуємо змінні. Поки що всі значення запишемо в defaults
, потім винесемо в tfvars
оточень.
Змінна eks_clusters
Спочатку змінну з кластерами – поки тут буде один, тестовий:
variable "eks_clusters" { description = "List of EKS clusters to create records" type = set(string) default = [ "atlas-eks-test-1-28-cluster" ] }
Змінна eks_users
Додаємо список юзерів в трьох групах:
variable "eks_users" { description = "IAM Users to be added to EKS with aws_eks_access_entry, one item in the set() per each IAM User" type = map(list(string)) default = { devops = [ "arn:aws:iam::492***148:user/arseny" ], backend = [ "arn:aws:iam::492***148:user/oleksii", "arn:aws:iam::492***148:user/test-eks-acess-TO-DEL" ], qa = [ "arn:aws:iam::492***148:user/yehor" ], } }
Змінна eks_access_scope
Список для aws_eks_access_policy_association
і access-scope
, як це може виглядати:
- ім’я команди
- список неймспейсів на які будуть права
admin
- список неймспейсів на які будуть права
edit
- список неймспейсів на які будуть права
read-only
- список неймспейсів на які будуть права
Тож можна зробити щось накшталт такого:
variable "eks_access_scope" { description = "EKS Namespaces for teams to grant access with aws_eks_access_policy_association" type = map(object({ namespaces_admin = optional(set(string)), namespaces_edit = optional(set(string)), namespaces_read_only = optional(set(string)) })) default = { backend = { namespaces_edit = ["*backend*", "*session-notes*"], namespaces_read_only = ["*ops*"] }, qa = { namespaces_edit = ["*qa*"], namespaces_read_only = ["*backend*"] } } }
Тут:
backend
мають праваedit
доступ на всі Namespaces і іменами “*backend*” та “*session-notes*“, і праваread-only
на неймспейси з “*ops*” – наприклад, доступ в Namespace “ops-monitoring-ns“, куди бекенд-тіма інколи заходить.qa
мають праваedit
на всі Namespaces і іменами “*qa*“, і праваread-only
на неймспейси Backend API
І тоді ми в принципі досить гнучко зможемо додавати першмішени для кожної групи юзерів.
Зверніть увагу, що в type
ми задаємо optional(set())
– бо група юзерів може не мати якоїсь групи неймспейсів.
Змінна eks_access_policies
Останнім додаємо список з дефолтними політиками:
variable "eks_access_policies" { description = "List of EKS clusters to create records" type = map(string) default = { cluster_admin = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy", namespace_admin = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSAdminPolicy", namespace_edit = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSEditPolicy", namespace_read_only = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy", } }
Створення aws_eks_access_entry
Додаємо перший ресурс – aws_eks_access_entry
.
Аби в одному циклі створювати aws_eks_access_entry
і для кожного кластера зі списку eks_clusters
, і для кожного юзерів з кожної групи – створимо три змінних в locals
:
locals { eks_access_entries_devops = flatten([ for cluster in var.eks_clusters : [ for user_arn in var.eks_users.devops : { cluster_name = cluster principal_arn = user_arn } ] ]) eks_access_entries_backend = flatten([ for cluster in var.eks_clusters : [ for user_arn in var.eks_users.backend : { cluster_name = cluster principal_arn = user_arn } ] ]) eks_access_entries_qa = flatten([ for cluster in var.eks_clusters : [ for user_arn in var.eks_users.qa : { cluster_name = cluster principal_arn = user_arn } ] ]) }
А потім їх використаємо в resource "aws_eks_access_entry"
з for_each
, яким сформуємо map()
з key=idx
та value=entry
:
resource "aws_eks_access_entry" "devops" { for_each = { for idx, entry in local.eks_access_entries_devops : idx => entry } cluster_name = each.value.cluster_name principal_arn = each.value.principal_arn }
Тут у for_each
буде формуватись список типу:
[ { cluster_name = "atlas-eks-test-1-28-cluster", principal_arn = "arn:aws:iam::492***148:user/arseny" }, { cluster_name = "atlas-eks-test-1-28-cluster", principal_arn = "arn:aws:iam::492***148:user/another.user" } ]
І аналогічно створюємо ресурси aws_eks_access_entry
для груп backend
з local.eks_access_entries_backend
і для qa
з local.eks_access_entries_qa
:
... resource "aws_eks_access_entry" "backend" { for_each = { for cluser, user in local.eks_access_entries_backend : cluser => user } cluster_name = each.value.cluster_name principal_arn = each.value.principal_arn } resource "aws_eks_access_entry" "qa" { for_each = { for cluser, user in local.eks_access_entries_qa : cluser => user } cluster_name = each.value.cluster_name principal_arn = each.value.principal_arn }
Створення eks_access_policy_association
Наступний крок – надати цим юзерам пермішени.
Для групи devops
це буде cluster-admin, а для бекенду – edit
в одних неймспейсах і read-only
в інших – по спискам неймспейсів в namespaces_edit
та namespaces_read_only
у змінній eks_access_scope
.
Як і з aws_eks_access_entry
– ресурси eks_access_policy_association
для кожної групи юзерів зробимо трьома окремими сутностями.
Я вирішив не ускладнювати код, і зробити його більш читабельним, хоча можна було додати ще якийсь locals
з flatten()
і потім все робити в одному-двох ресурсах aws_eks_access_policy_association
з циклами.
Тут знов використовуємо вже існуючі locals
– eks_access_entries_devops
, eks_access_entries_backend
та eks_access_entries_qa
з яких беремо кластери і юзерів, а потом для кожного задаємо права – аналогічно тому, як робили для aws_eks_access_entry
.
Додаємо перший eks_access_policy_association
– для девопсів, з Policy var.eks_access_policies.cluster_admin
та access_scope = cluster
:
# DEVOPS CLUSTER ADMIN resource "aws_eks_access_policy_association" "devops" { for_each = { for cluser, user in local.eks_access_entries_devops : cluser => user } cluster_name = each.value.cluster_name principal_arn = each.value.principal_arn policy_arn = var.eks_access_policies.cluster_admin access_scope { type = "cluster" } }
Група backend
і права edit
на неймспейси задані в списку namespaces_edit
змінної eks_access_scope
для групи “backend
“:
# BACKEND EDIT resource "aws_eks_access_policy_association" "backend_edit" { for_each = { for cluser, user in local.eks_access_entries_backend : cluser => user } cluster_name = each.value.cluster_name principal_arn = each.value.principal_arn policy_arn = var.eks_access_policies.namespace_edit access_scope { type = "namespace" namespaces = var.eks_access_scope["backend"].namespaces_edit } }
Аналогічно – бекенди, але вже права read-only
на групу неймспейсів зі списку namespaces_read_only
:
# BACKEND READ ONLY resource "aws_eks_access_policy_association" "backend_read_only" { for_each = { for cluser, user in local.eks_access_entries_backend : cluser => user } cluster_name = each.value.cluster_name principal_arn = each.value.principal_arn policy_arn = var.eks_access_policies.namespace_read_only access_scope { type = "namespace" namespaces = var.eks_access_scope["backend"].namespaces_read_only } }
І аналогічно для QA:
# QA EDIT resource "aws_eks_access_policy_association" "qa_edit" { for_each = { for cluser, user in local.eks_access_entries_qa : cluser => user } cluster_name = each.value.cluster_name principal_arn = each.value.principal_arn policy_arn = var.eks_access_policies.namespace_edit access_scope { type = "namespace" namespaces = var.eks_access_scope["qa"].namespaces_edit } } # QA READ ONLY resource "aws_eks_access_policy_association" "qa_read_only" { for_each = { for cluser, user in local.eks_access_entries_qa : cluser => user } cluster_name = each.value.cluster_name principal_arn = each.value.principal_arn policy_arn = var.eks_access_policies.namespace_read_only access_scope { type = "namespace" namespaces = var.eks_access_scope["qa"].namespaces_read_only } }
Робимо terraform plan
– і маємо юзерів:
Виконуємо terraform apply
, і перевіряємо EKS Access Entries:
І пермішени тестового юзера з групи backend
:
Перевіряємо як все працює.
Створюємо kubectl context
для тестового юзера (--profile test-eks
):
$ aws --profile test-eks eks update-kubeconfig --name atlas-eks-test-1-28-cluster --alias test-cluster-test-profile Updated context test-cluster-test-profile in /home/setevoy/.kube/config
Перевіряємо з kubectl auth can-i
.
В default Namespace – не можемо нічого:
$ kk -n default auth can-i list pods no
І неймспейс з іменем “*backend*” – тут можемо створювати поди:
$ kk -n dev-backend-api-ns auth can-i create pods yes
Неймспейс з “*ops*” – можемо виконати list
:
$ kk -n ops-monitoring-ns auth can-i list pods yes
Але не можемо нічого створити:
$ kk -n ops-monitoring-ns auth can-i create pods no
Окей – тут наче все готово.
AIM Roles для GitHub Actionsтут, аби ці ро нічим не відрізняються від звичайних юзерів – тому їх можна буде додати таким самим чином.
Terraform та EKS Pod Identities
Друге, що нам треба – це створити Pod Identity associations, аби замінити стару схему з EKS ServiceAccounts та OIDC.
Note: не забудьте додати eks-pod-identity-agent Add-On, приклад для terraform-aws-modules/eks – тут>>>.
Наприклад, на поточному кластері у нас є ServiceAccount для Yet Another Cloudwatch Exporter (YACE):
$ kk -n ops-monitoring-ns get sa yace-serviceaccount -o yaml apiVersion: v1 kind: ServiceAccount metadata: annotations: eks.amazonaws.com/role-arn: arn:aws:iam::492***148:role/atlas-monitoring-ops-1-28-yace-exporter-access-role ...
Що нам потрібно – це створити Pod Identity associations з такими параметрами:
cluster_name
namespace
service_account
role_arn
Зараз IAM Role для YACE створюється в коді Terraform в репозиторії з моніторингом, і тут є два варіанти:
- перенести створення всіх IAM Roles для всіх проектів в цей новий, і потім тут жеж створювати
aws_eks_pod_identity_association
- але для цього доведеться міняти досить багато коду – випилювати створення ролей в інших проектах, і додавати їх в цей новий, плюс девелопери (якщо говорити про Backend API) вже якось звикли робити це в своїх проектах – потрібно буде писати документацію, як це робити в іншому проекті
- або залишити створення ролей в кожному проекті окремо – а в новому просто мати змінну зі списками інших проектів та їхніх ролей (чи використати
terraform_remote_state
іterraform outputs
з кожного проекту)- але тут будемо мати трохи геморою із запуском нових проектів, особливо, якщо там ролі будуть створюватись з якимись динамічними іменами – бо доведеться спочатку виконати
terraform apply
в новому проекті, отримати там ARN ролей, потім додавати їх в змінні нашого проекту “atlas-iam“, робитиterraform apply
тут аби ці ролі підключити до EKS, і тільки тоді робити умовнийhelm install
зі створенням ServiceAccount та подів нового проекту
- але тут будемо мати трохи геморою із запуском нових проектів, особливо, якщо там ролі будуть створюватись з якимись динамічними іменами – бо доведеться спочатку виконати
Але в обох варіантах нам для Pod Identity associations потрібно буде задавати такі параметри:
cluster_name
: змінна вже є- імена ServiceAccount: вони будуть однакові на всіх кластерах
role_arn
: залежить від того, як будемо створювати IAM Rolesnamespace
: а от тут питаннячко:- ім’я неймспейсу для моніторингу на всіх кластерах у нас однакове – “ops-monitoring-ns“, де Ops – це AWS або EKS оточення
- а от для бекенду у нас на одному кластері є і Dev, і Staging, і Production – кожен у власному неймспейсі
А в неймспейсах для Pod Identity association ми вже не можемо використати “*
“, як робили з Access Entries для юзерів, тобто маємо створювати окрему Pod Identity association на кожен конкретний неймспейс.
Давайте спочатку подивимось як ми взагалі можемо створювати необхідні ресурси – а потім подумаємо над змінними, і вирішимо яким чином це все реалізувати.
З використанням resource "aws_eks_pod_identity_association"
Варіант перший – використати “ванільний” aws_eks_pod_identity_association
з Terraform AWS Provider.
Аби створити Pod Identity association для нашого умовного YACE-екпортеру, нам потрібно:
-
assume_role_policy
: хто зможе виконувати IAM Role Assume aws_iam_role
: IAM Role з необхідними доступами до AWS APIaws_eks_pod_identity_association
: підключити цю роль до EKS-кластеру і ServiceAccount в ньому
Давайте зробимо окремий файл для ролей – eks_pod_iam_roles.tf
.
Описуємо aws_iam_policy_document
– тепер ніяких OIDC, просто pods.eks.amazonaws.com
:
# Trust Policy to be used by all IAM Roles data "aws_iam_policy_document" "assume_role" { statement { effect = "Allow" principals { type = "Service" identifiers = ["pods.eks.amazonaws.com"] } actions = [ "sts:AssumeRole", "sts:TagSession" ] } }
Далі – сама роль.
Ролі треба буде створювати для кожного кластеру – тому знов візьмемо нашу змінну variable "eks_clusters"
, і потім в циклі створимо ролі з іменами під кожен кластер:
# Create an IAM Role to be assumed by Yet Another CloudWatch Exporter resource "aws_iam_role" "yace_exporter_access" { for_each = var.eks_clusters name = "${each.key}-yace-exporter-role" assume_role_policy = data.aws_iam_policy_document.assume_role.json inline_policy { name = "${each.key}-yace-exporter-policy" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = [ "cloudwatch:ListMetrics", "cloudwatch:GetMetricStatistics", "cloudwatch:GetMetricData", "tag:GetResources", "apigateway:GET" ] Effect = "Allow" Resource = ["*"] }, ] }) } tags = { Name = "${each.key}-yace-exporter-role" } }
І тепер в eks_pod_identities
.tf можемо додати асоціацію:
resource "aws_eks_pod_identity_association" "yace" { for_each = var.eks_clusters cluster_name = each.key namespace = "example" service_account = "example-sa" role_arn = aws_iam_role.yace_exporter_access["${each.key}"].arn }
Де в "aws_iam_role.yace_exporter_access["${each.key}"].arn"
посилаємось на IAM Role з конкретним іменем кожного кластеру:
... # aws_eks_pod_identity_association.yace["atlas-eks-test-1-28-cluster"] will be created + resource "aws_eks_pod_identity_association" "yace" { + association_arn = (known after apply) + association_id = (known after apply) + cluster_name = "atlas-eks-test-1-28-cluster" + id = (known after apply) + namespace = "example" + role_arn = (known after apply) + service_account = "example-sa" + tags_all = { + "component" = "devops" + "created-by" = "terraform" + "environment" = "ops" } } ...
З використанням модуля terraform-aws-eks-pod-identity
Інший варіант – робити через terraform-aws-eks-pod-identity
. Тоді нам не потрібно окремо описувати роль – ми можемо задати IAM Policy прямо в модулі, і він все створить за нас. Крім того, він дозволяє в одному модулі створити кілька associations для різних кластерів.
Наприклад:
module "yace_pod_identity" { source = "terraform-aws-modules/eks-pod-identity/aws" version = "~> 1.2" for_each = var.eks_clusters name = "${each.key}-yace" attach_custom_policy = true source_policy_documents = [jsonencode({ Version = "2012-10-17" Statement = [ { Action = [ "cloudwatch:ListMetrics", "cloudwatch:GetMetricStatistics", "cloudwatch:GetMetricData", "tag:GetResources", "apigateway:GET" ] Effect = "Allow" Resource = ["*"] }, ] })] associations = { ex-one = { cluster_name = each.key namespace = "custom" service_account = "custom" } } }
І для кожного кластера буде створено і роль, і асоціація:
Крім того, terraform-aws-eks-pod-identity
вміє створювати всякі дефолтні IAM Roles – для ALB Ingress Controller, External DNS тощо.
Але в моєму випадку ці сервіси в EKS створюються з aws-ia/eks-blueprints-addons/aws
, який сам створює ролі, і якихось змін відносно Pod Identity association я там не бачу (хоча GitHub Issue з обговоренням відкрита ще в листопаді 2023 – див. Switch to IRSAv2/pod identity).
Окей.
То як ми можемо зробити всю цю схему?
- у нас будуть IAM Roles – створюються в інших проектах, або в нашому “atlas-iam“
- ми будемо знати в які неймспейси які ServiceAccounts нам треба підключати
Яку ми можемо придумати змінну для всього цього діла?
Можемо задати ім’я проекту – “backend-api“, “monitoring“, etc, і в кожному проекті мати списки з неймспейсами, ServiceAccount та IAM Roles.
А потім для кожного проекту мати окремий ресурс aws_eks_pod_identity_association
, який в циклі буде проходитись по всім EKS-кластерам в AWS-акаунті.
Проблема: Pod Identity association та динамічні Namespaces
Але! Для Backend API в GitHub Actions у нас створюються динамічні оточення, і для них – Kubernetes Namespaces з іменами типу “pr-1212-backend-api-ns“. Тобто, є статичні неймспейси – “dev-backend-api-ns“, “staging-backend-api-ns” та “prod-backend-api-ns“, які ми знаємо – але будуть імена, які ми ніяк заздалегідь дізнатись не можемо.
На схожу тему є відкрита ще в грудні 2023 GitHub Issue – [EKS]: allow EKS Pod Identity association to accept a glob for the service account name (my-sa-*), але вона поки що без змін.
Тому як рішення поки що бачу тільки залишити старий IRSA для бекенду, а Pod Identity використовувати тільки для тих проектів, які мають статичні неймспейси.
Pod Identity association для Monitoring project
Ну й давайте зробимо одну асоціацію, і потім вже в процесі роботи будемо дивитись як можна покращити процес.
В проекті з моніторингом в мене є 5 кастомних IAM Roles – для Grafana Loki з доступами до S3, для звичайного CloudWatch Exporter, для Yet Another CloudWatch Exporter, для нашого власного Redshift Exporter, і для X-Ray Daemon.
Створення ролей все ж, мабуть, краще винести в цей проект, “atlas-iam“. Тоді при створенні нового проекту ми спочатку в цьому проекті описуємо його роль і асоціацію в потрібних неймспейсах з потрібним ServiceAccount, а потім в самому проекті в Helm-чарті вказуємо ім’я ServiceAccount.
Щодо terraform-aws-modules/eks-pod-identity/aws
– як на мене, то він більш заточений під всякі дефолтні ролі, хоча можливість створення власних там є.
Але зараз простішим буде зробити так:
- створювати IAM Roles для кожного сервісу зі звичайним
resource "aws_iam_role"
- і з
resource "aws_eks_pod_identity_association"
підключати ці ролі до кластерів
Отже, створимо нову змінну, де будуть проекти та їхні неймспейси і ServiceAccounts:
variable "eks_pod_identities" { description = "EKS ServiceAccounts for Pods to grant access with eks-pod-identity" type = map(object({ namespace = string, projects = map(object({ serviceaccount_name = string })) })) default = { monitoring = { namespace = "ops-monitoring-ns" projects = { loki = { serviceaccount_name = "loki-sa" }, yace = { serviceaccount_name = "yace-sa" } } } } }
Далі, у файлі eks_pod_iam_roles.tf
зробимо роль – як робили вище, але без циклів, бо у нас одна роль на весь AWS-акаунт, яка буде підключатись до різних Kubernetes-кластерів:
# Trust Policy to be used by all IAM Roles data "aws_iam_policy_document" "assume_role" { statement { effect = "Allow" principals { type = "Service" identifiers = ["pods.eks.amazonaws.com"] } actions = [ "sts:AssumeRole", "sts:TagSession" ] } } # Create an IAM Role to be assumed by Yet Another CloudWatch Exporter resource "aws_iam_role" "yace_exporter_access" { name = "monitoring-yace-exporter-role" assume_role_policy = data.aws_iam_policy_document.assume_role.json inline_policy { name = "monitoring-yace-exporter-policy" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = [ "cloudwatch:ListMetrics", "cloudwatch:GetMetricStatistics", "cloudwatch:GetMetricData", "tag:GetResources", "apigateway:GET" ] Effect = "Allow" Resource = ["*"] }, ] }) } tags = { Name = "monitoring-yace-exporter-role" } }
Об’єкт data "aws_iam_policy_document" "assume_role"
у нас тут буде один – і потім його використаємо в усіх IAM Roles.
Далі в файлі eks_pod_identities.tf
описуємо власне aws_eks_pod_identity_association
з використанням namespace
та service_account
зі змінної, яку описали вище, role_arn
отримуємо з resource "aws_iam_role" "yace_exporter_access"
, а імена кластерів беремо в циклі зі змінної var.eks_clusters
:
resource "aws_eks_pod_identity_association" "yace" { for_each = var.eks_clusters cluster_name = each.key namespace = var.eks_pod_identities.monitoring.namespace service_account = var.eks_pod_identities.monitoring.projects["yace"].serviceaccount_name role_arn = aws_iam_role.yace_exporter_access.arn }
Виконуємо terraform plan
:
Робимо terraform apply
, та перевіряємо Pod Identity associations в EKS:
Перевірка EKS Pod Identities
Ну і перевіримо, чи це працює.
Описуємо Kubernetes Pod з AWS CLI та ServiecAccount з іменем “yace-sa
“:
--- apiVersion: v1 kind: ServiceAccount metadata: name: yace-sa namespace: ops-monitoring-ns --- apiVersion: v1 kind: Pod metadata: name: pod-identity-test namespace: ops-monitoring-ns spec: containers: - name: aws-cli image: amazon/aws-cli:latest command: ['sleep', '36000'] restartPolicy: Never serviceAccountName: yace-sa
Створюємо їх:
$ kk apply -f irsa-sa.yaml serviceaccount/yace-sa created pod/pod-identity-test created
Підключаємось в Pod:
$ kk -n ops-monitoring-ns exec -ti pod-identity-test -- bash
І пробуємо виконати запит до CloudWatch – наприклад, “cloudwatch:ListMetrics
” на який ми давали права в IAM Role:
bash-4.2# aws cloudwatch list-metrics --namespace "AWS/SNS" | head { "Metrics": [ { "Namespace": "AWS/SNS", "MetricName": "NumberOfMessagesPublished", "Dimensions": [ { "Name": "TopicName", "Value": "dev-stable-diffusion-running-opsgenie"
А на S3 у нас прав нема:
bash-4.2# aws s3 ls An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied
Висновки
Отже, що ми маємо в результаті.
З EKS Access Entries все доволі ясно, і рішення має право на життя: можемо з одного Terraform-коду управляти юзерами для різних EKS кластерів і навіть в різних AWS-акаунтах.
Код, описаний в eks_access_entires.tf
дозволяє нам досить гнучко створювати нові EKS Authentification API Access Entries для різних юзерів з різними правами, хоча я не торкався питання Kubernetes RBAC – створення окремих груп з власними RoleBindings. Але в моєму випадку це поки трохи overhead.
А от з EKS Pod Identities автоматизація “дала збій” – бо на цей час нема можливості використовувати “*
” в іменах неймспейсів та/або ServiceAccounts – а тому ми маємо досить жорстко прив’язуватись до якихось постійних імен. Тому описане рішення може застосувати, коли у вас заздалегідь відомі імена об’єктів в Kubernetes – але для якихось динамічних рішень все ще доведеться використовувати стару схему з IRSA та OIDC.
Втім, сподіваюсь, цей момент пофіксять, і тоді можна буде всі наші проекти менеджити вже з одного коду.