Отже, маємо кластер 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 nameprincipal-arn
- в
eks_access_policy_association– описуємо тип доступу – admin/edit/etc та scope – cluster-wide, або конкретний неймспейс; параметри тут будуть:cluster-nameprincipal-arnpolicy-arnaccess-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_namenamespaceservice_accountrole_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.
Втім, сподіваюсь, цей момент пофіксять, і тоді можна буде всі наші проекти менеджити вже з одного коду.











