Доволі частий кейс, коли на новому проекті, який тільки створює свою інфраструктуру і CI/CD, робиться це як MVP/PoC, і на початку на тюнінг AWS IAM Roles та IAM Policies час не витрачається, а просто підключається AdministratorAccess.
Власне, саме так відбувалось і в моєму проекті, але ми ростемо, і прийшов час навести лад в IAM.
Проблема і задача
Отже, маємо GitHub Actions джоби, які деплоять інфрастуктуру з Terraform.
Для доступу до AWS з GitHub використовується Identity Provider з IAM Role: GitHub Actions Worker при старті джоби виконує аутентифікацію та авторизацію в AWS з заданою IAM Role, і потім запускає власне деплой з Terraform.
Для IAM Role зараз підключена політика AdministratorAccess, і наша задача – написати нову fine grained політику, де б не було зайвих доступів.
Варіант перший – це створити пусту політику, підключити її до ролі замість AdministratorAccess, і раз за разом запускати джобу дивлячись на помилки в логах:
А потім по черзі додавати дозволи, наприклад lambda:ListVersionsByFunction.
Він використає CloudTrail events для конкретної ролі та створить IAM Policy в якій будуть тільки ті API-виклики, які дійсно робились цією роллю.
Окрім IAM Access Analyzer є цікава тулза iann0036/iamlive, але в нашому випадку вона не дуже підходить, бо IAM Role використовується в GitHub Actions з AWS Indetity Provider.
Давайте глянемо, як налаштувати IAM Access Analyzer policy generation – створимо CloudTrail, IAM Role, напишемо Terraform-код який буде створювати ресурси, а потім перевіримо які політики нам запропонує Access Analyzer.
Створення CloudTrail Trail
Перше, що нам буде потрібно – це створити CloudTrail Trail, який буде логувати дії. Детальніше про CloudTrail писав в AWS: CloudTrail – обзор и интеграция с CloudWatch и Opsgenie, але зараз нам цікаві тільки типи івентів, які він вміє записувати:
Management events: все, що стосується змін в ресурсах – створення EC2, VPC, зміни в SecurtyGroups тощо
Data events: все, що стосується даних – створення об’єктів в S3-бакетах, зміни в таблицях DynamoDB, виклики Lambda-функцій
Отже, якщо наш Terraform-код займається тільки створенням ресурсів в AWS – то має вистачити Management events, якщо ж він додатково виконує якісь дії з даними/об’єктами – то потрібні обидва. Можна включити всі, але майте на увазі, що CloudTrail trails не безкоштовний – див. AWS CloudTrail pricing.
Переходимо в CloudTrail > Trails, створюємо новий Trail:
Включаємо логування обох типів – просто для перевірки, в цьому випадку точно вистачило б тільки Management events:
Для Data events вибираємо які саме сервіси будемо логувати:
Переходимо до IAM.
Створення IAM Role
Додаємо нову роль з Trusted entity type == AWS Account, бо зараз тестувати будемо локально з AWS CLI від свого IAM-юзера, а не через GitHub OIDC Identity Provider:
Підключаємо AdministratorAccess:
Зберігаємо цю роль:
Налаштування AWS CLI
Тестити будемо локально, але імітуємо роботу GitHub Actions.
Що нам треба – це створити AWS CLI Profile, який буде виконувати AssumeRole, яку ми створили, а потім з цим профайлом Terraform буде створювати ресурси в AWS.
В файлі ~/.aws/config додаємо новий профайл:
[profile iam-test]
region = us-east-1
role_arn = arn:aws:iam::492***148:role/iam-generator-test-TO-DEL
source_profile = work
source_profile = work тут – це мій робочий профайл, в якому задані Access та Secrets keys.
Напишемо простий код, який буде створювати S3 бакет використовуючи створений вище IAM CLI Profile iam-test (пам’ятаємо, що ім’я бакету має бути унікальним для заданого AWS Region, інакше AWS спробує створити корзину в іншому регіоні):
Використання IAM Access Analyzer policy generation
Краще зачекати хвилин 5 після запуску Terraform, аби CloudTrail встиг записати всі події, а потім можемо згенерувати IAM Policy для цієї ролі:
Вибираємо період, регіон та створений раніше Trail:
Чекаємо 5-10 хвилин, поки проаналізуються логи CloudTrail (можна перезавантажувати сторінку з F5, бо іноді Status сам не оновлюється):
І дивимось які політики нам пропонуються:
Ціла купа, і основна для нашого тесту – s3:CreateBucket.
Клікаємо Next, і маємо саму політику в JSON:
Зверніть увагу, що Access Analyzer створив окремі правила на API-виклики, які стосуються всіх бакетів – s3:ListAllMyBuckets, і окремі правила для викликів, які стосуються конкретного бакету/бакетів – s3:CreateBucket.
При цьому в Resource використовується ${BucketName}, який ми можемо замінити на своє значення:
Зберігаємо та підключаємо цю політику:
І тепер можемо відключити AdministratorAccess.
Але маємо на увазі, що ми виконували тільки створення ресурсів і, відповідно, виконувались API-виклики пов’язані тільки зі створенням корзини.
Тобто, якщо ми зараз приберемо AdministratorAccess і залишимо тільки цю нову політику – то виконати terraform destroy не зможемо, бо, по-перше – у нас нема права на s3:DeleteBucket, по-друге – при видаленні корзини AWS має перевірити чи нема в ній об’єктів, а для цього виконується операція s3:ListBucket – тому отримаємо помилку operation error S3: HeadBucket:
Тож треба виконати всі дії з Terraform, а вже після цього генерувати політику:
І потім відключати AdministratorAccess. Але навіть в такому випадку s3:ListBucket (для S3: HeadBucket) треба додавати вручну.
Хоча це вже проблема більш специфічна саме до S3, але може бути подібна і з іншими ресурсами.
Перед тим, як переключати EKS Authentication mode повністю на API – нам потрібно з aws-auth ConfgiMap перенести всіх юзерів і ролі в Access Entries EKS-кластера.
І ідея зараз така, щоб створити окремий проект Terraform, назвемо його “atlas-iam“, в якому ми будемо менеджити всі IAM-доступи – і для EKS з Access Entries та Pod Identities, і для RDS з IAM database authentication, і, можливо, потім сюди ж перенесемо і юзер-менеджмент взагалі.
Не знаю, наскільки описана нижче схема зайде нам в майбутньому 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-акаунту Main
variables.tf: наші змінні з дефолтними значеннями
envs/: каталог з іменами EKS-кластерів
dev/: каталог для EKS Dev
dev-eks-cluster.tfvars – зі списком юзерів EKS Dev
dev-rds-backend.tfvars – зі списком юзерів RDS Dev
prod/
prod-eks-cluster.tfvars – зі списком юзерів EKS Prod
prod-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
У нас вже склалась однакова схема на всіх проектах, і новий буде виглядати так – аби не ускладнювати поки що вирішив без модулів, далі побачимо:
в Makefile: команди terraform init/plan/apply – і для виклику локально, і для CI/CD
backend.tf: S3 для стейт-файлів, DynamoDB для state-lock
envs/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 Entity – IAM User, для якого налаштовуємо доступ, та ім’я кластера, до якого доступ буде додаватись; параметри тут будуть:
cluster name
principal-arn
в eks_access_policy_association – описуємо тип доступу – admin/edit/etc та scope – cluster-wide, або конкретний неймспейс; параметри тут будуть:
списки з юзерами, різні списки для різних груп – можна зробити однієї змінною типу 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()) – бо група юзерів може не мати якоїсь групи неймспейсів.
Аби в одному циклі створювати 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:
І аналогічно створюємо ресурси 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):
$ 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 Roles
namespace: а от тут питаннячко:
ім’я неймспейсу для моніторингу на всіх кластерах у нас однакове – “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 API
aws_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 можемо додати асоціацію:
Де в "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 для різних кластерів.
І для кожного кластера буде створено і роль, і асоціація:
Крім того, 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“, які ми знаємо – але будуть імена, які ми ніяк заздалегідь дізнатись не можемо.
Тому як рішення поки що бачу тільки залишити старий 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:
Далі, у файлі 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:
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.
Втім, сподіваюсь, цей момент пофіксять, і тоді можна буде всі наші проекти менеджити вже з одного коду.
Хоча я буду робити апгрейд версії самого AWS EKS і модуля Terraform через створення нового кластеру, а тому в принципі можу не морочити голову з “live-update” кластера, але хочеться спробувати зробити так, аби це можна було застосувати на живому кластері та не втратити до нього доступу і не поламати умовний Production.
Втім, майте на увазі, що те, що описано в цьому пості робиться на тестовому кластері (бо в мене взагалі один кластер для Dev/Staging/Prod). Тож не варто відразу робити апгрейд на продакшені, а краще спочатку протестувати на якомусь Dev-оточенні.
Про сетап самого кластеру детальніше писав у Terraform: створення EKS, частина 2 – EKS кластер, WorkerNodes та IAM, і в цьому пості приклади коду будуть саме звідти, хоча він вже трохи відрізняється від того, що було описано там, бо створення EKS зробив окремим власним модулем, щоб простіше було менеджити різні кластери-оточення.
В цілому, змін наче і дуже небагато – але пост вийшов довгий, бо намагався показати все детально і з реальними прикладами.
Що саме нас цікавить (знов-таки – конкретно в моєму випадку):
EKS:
aws-auth: винесено в окремий модуль, в параметрах самого модуля terraform-aws-modules/eks його вже нема
з terraform-aws-modules/eks видалено параметри manage_aws_auth_configmap, create_aws_auth_configmap, aws_auth_roles, aws_auth_users, aws_auth_accounts
authentication_mode: додано значення API_AND_CONFIG_MAP
bootstrap_cluster_creator_admin_permissions: hardcoded у false
але можна передати enable_cluster_creator_admin_permissions зі значенням true, хоча тут наче треба додавати Access Entry
create_instance_profile: дефолтне значення змінилось з true на false, аби відповідати змінам в Karpenter v0.32 (але в мене Karpenter і так вже 0.32 і все працює, тому тут змін не має бути)
Karpenter:
irsa: видалено імена змінних з “irsa” – пачка перейменувань і декілька імен видалено взагалі
create_instance_profile: дефолтне значення з true стало false
EKS: оновлення версії модулю з 19.21 => 20.0 з API_AND_CONFIG_MAP
додавання aws-auth окремим модулем
Karpenter: оновлення версії модулю з 19.21 => 20.0
EKS: upgrade 19.21 на 20.0
Нам потрібно:
видалити все, пов’язане з aws_auth, в моєму випадку це:
manage_aws_auth_configmap
aws_auth_users
aws_auth_roles
додати authentication_mode зі значенням API_AND_CONFIG_MAP (пізніше, для 21, треба буде замінити на API)
додати новий модуль для aws_auth_roles і перенести aws_auth_users та aws_auth_roles туди
Щодо bootstrap_cluster_creator_admin_permissions та enable_cluster_creator_admin_permissions – так як цей кластер створювався з 19.21, то root-юзер там вже є, і він буде доданий в Access Entries разом з WorkerNodes IAM Role, тому тут нічого робити не треба.
А як перенести в access_entries наших юзерів і ролі – подивимось мабуть вже в наступному пості, бо зараз будемо робити тільки оновлення версії модуля зі збереженням aws-auth ConfigMap.
Для тесту Karpenter – створив тестовий деплой з одним подом, який затригерить створення WorkerNode, і до нього Ingress/ALB, на який йде постійний ping, аби впевнитись, що все буде працювати без даунтаймів.
NodeClaims зараз:
$ kk get nodeclaim
NAME TYPE ZONE NODE READY AGE
default-7hjz7 t3.small us-east-1a ip-10-0-45-183.ec2.internal True 53s
Окей, поїхали оновлювати EKS.
Поточний код Terraform та структура модулів
Аби далі краще розуміти момент з видаленням ресурс aws_auth з Terraform state – моя поточна структура файлів/модулів:
в envs/test-1-28/main.tf викликається модуль з modules/atlas-eks з необхідними параметрами – і виконувати terraform state rm ми будемо саме в envs/test-1-28
в modules/atlas-eks/eks.tf викликається модуль terraform-aws-modules/eks/aws потрібної версії – тут ми будемо робити зміни в коді
Виклик рутового модуля в файлі envs/test-1-28/main.tf виглядає так:
module "atlas_eks" {
source = "../../modules/atlas-eks"
# 'devops'
component = var.component
# 'ops'
aws_environment = var.aws_environment
# 'test'
eks_environment = var.eks_environment
env_name = local.env_name
# '1.28'
eks_version = var.eks_version
# 'endpoint_public_access', 'enabled_log_types'
eks_params = var.eks_params
# 'coredns = v1.10.1-eksbuild.6', 'kube_proxy = v1.28.4-eksbuild.1', etc
eks_addon_versions = var.eks_addon_versions
# AWS IAM Roles to be added to EKS aws-auth as 'masters'
eks_aws_auth_users = var.eks_aws_auth_users
# GitHub IAM Roles used in Workflows
eks_github_auth_roles = var.eks_github_auth_roles
# 'vpc-0fbaffe234c0d81ea'
vpc_id = var.vpc_id
helm_release_versions = var.helm_release_versions
# override default 'false' in the module's variables
# will trigger a dedicated module like 'eks_blueprints_addons_external_dns_test'
# with a domainFilters == variable.external_dns_zones.test == 'test.example.co'
external_dns_zone_test_enabled = true
# 'instance-family', 'instance-size', 'topology'
karpenter_nodepool = var.karpenter_nodepool
}
А код основного модуля – так:
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 19.21.0"
# is set in `locals` per env
# '${var.project_name}-${var.eks_environment}-${local.eks_version}-cluster'
# 'atlas-eks-test-1-28-cluster'
# passed from the root module
cluster_name = "${var.env_name}-cluster"
# passed from the root module
cluster_version = var.eks_version
# 'eks_params' passed from the root module
cluster_endpoint_public_access = var.eks_params.cluster_endpoint_public_access
# 'eks_params' passed from the root module
cluster_enabled_log_types = var.eks_params.cluster_enabled_log_types
# 'eks_addons_version' passed from the root module
cluster_addons = {
coredns = {
addon_version = var.eks_addon_versions.coredns
configuration_values = jsonencode({
replicaCount = 1
resources = {
requests = {
cpu = "50m"
memory = "50Mi"
}
}
})
}
kube-proxy = {
addon_version = var.eks_addon_versions.kube_proxy
configuration_values = jsonencode({
resources = {
requests = {
cpu = "20m"
memory = "50Mi"
}
}
})
}
vpc-cni = {
# old: eks_addons_version
# new: eks_addon_versions
addon_version = var.eks_addon_versions.vpc_cni
configuration_values = jsonencode({
env = {
ENABLE_PREFIX_DELEGATION = "true"
WARM_PREFIX_TARGET = "1"
AWS_VPC_K8S_CNI_EXTERNALSNAT = "true"
}
})
}
aws-ebs-csi-driver = {
addon_version = var.eks_addon_versions.aws_ebs_csi_driver
# iam.tf
service_account_role_arn = module.ebs_csi_irsa_role.iam_role_arn
}
}
# make as one complex var?
# passed from the root module
vpc_id = var.vpc_id
# for WorkerNodes
# passed from the root module
subnet_ids = data.aws_subnets.private.ids
# for the Control Plane
# passed from the root module
control_plane_subnet_ids = data.aws_subnets.intra.ids
manage_aws_auth_configmap = true
# `env_name` make too long name causing issues with IAM Role (?) names
# thus, use a dedicated `env_name_short` var
eks_managed_node_groups = {
# eks-default-dev-1-28
"${local.env_name_short}-default" = {
# `eks_managed_node_group_params` from defaults here
# number, e.g. 2
min_size = var.eks_managed_node_group_params.default_group.min_size
# number, e.g. 6
max_size = var.eks_managed_node_group_params.default_group.max_size
# number, e.g. 2
desired_size = var.eks_managed_node_group_params.default_group.desired_size
# list, e.g. ["t3.medium"]
instance_types = var.eks_managed_node_group_params.default_group.instance_types
# string, e.g. "ON_DEMAND"
capacity_type = var.eks_managed_node_group_params.default_group.capacity_type
# allow SSM
iam_role_additional_policies = {
AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
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
}
}
}
# 'atlas-eks-test-1-28-node-sg'
node_security_group_name = "${var.env_name}-node-sg"
# 'atlas-eks-test-1-28-cluster-sg'
cluster_security_group_name = "${var.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 = [data.aws_vpc.eks_vpc.cidr_block]
type = "ingress"
}
}
# 'atlas-eks-test-1-28'
node_security_group_tags = {
"karpenter.sh/discovery" = var.env_name
}
cluster_identity_providers = {
sts = {
client_id = "sts.amazonaws.com"
}
}
# passed from the root module
aws_auth_users = var.eks_aws_auth_users
# locals flatten() 'eks_masters_access_role' + 'eks_github_auth_roles'
aws_auth_roles = local.aws_auth_roles
}
Ролі/юзери для aws-auth формуються в locals зі змінних у variables.tf та envs/test-1-28/test-1-28.tfvars, всі з system:masters – до RBAC ми ще не дійшли:
locals {
# create a short name for node names in the 'eks_managed_node_groups'
# 'test-1-28'
env_name_short = "${var.eks_environment}-${replace(var.eks_version, ".", "-")}"
# 'eks_github_auth_roles' passed from the root module
github_roles = [for role in var.eks_github_auth_roles : {
rolearn = role
username = role
groups = ["system:masters"]
}]
# 'eks_masters_access_role' + 'eks_github_auth_roles'
# 'eks_github_auth_roles' from the root module
# 'aws_iam_role.eks_masters_access' from the iam.tf here
aws_auth_roles = flatten([
{
rolearn = aws_iam_role.eks_masters_access_role.arn
username = aws_iam_role.eks_masters_access_role.arn
groups = ["system:masters"]
},
local.github_roles
])
}
В модулі EKS найбільша зміна це переключення authentication_mode на API_AND_CONFIG_MAP.
Кластер вже є, створений з версією 19.21.0.
Вкладка Access зараз виглядає так:
Access configuration == ConfgiMap, і в IAM access entries пусто.
Тепер робимо зміни в коді файла modules/atlas-eks/eks.tf:
міняємо версію на v20.0
видаляємо все, що пов’язано з aws_auth
додаємо authentication_mode зі значенням API_AND_CONFIG_MAP
Зміни поки виглядають так:
module "eks" {
source = "terraform-aws-modules/eks/aws"
#version = "~> 19.21.0"
version = "~> v20.0"
...
# removing for API_AND_CONFIG_MAP
#manage_aws_auth_configmap = true
# adding for API_AND_CONFIG_MAP
authentication_mode = "API_AND_CONFIG_MAP"
...
# removing for API_AND_CONFIG_MAP
# passed from the root module
#aws_auth_users = var.eks_aws_auth_users
# removing for API_AND_CONFIG_MAP
# locals flatten() 'eks_masters_access_role' + 'eks_github_auth_roles'
#aws_auth_roles = local.aws_auth_roles
}
Виконуємо terraform init, аби оновити модуль EKS:
...
Initializing modules...
Downloading registry.terraform.io/terraform-aws-modules/eks/aws 20.0.1 for atlas_eks.eks...
- atlas_eks.eks in .terraform/modules/atlas_eks.eks
- atlas_eks.eks.eks_managed_node_group in .terraform/modules/atlas_eks.eks/modules/eks-managed-node-group
- atlas_eks.eks.eks_managed_node_group.user_data in .terraform/modules/atlas_eks.eks/modules/_user_data
- atlas_eks.eks.fargate_profile in .terraform/modules/atlas_eks.eks/modules/fargate-profile
...
Глянемо aws-auth зараз – заодно будемо мати його “бекап” в YAML:
Виконуємо ще раз terraform init, ще раз робимо terraform plan, і тепер маємо новий ресурс для aws-auth:
А старий все ще буде видалятись – тобто, спочатку Terraform його видалить, а потім створить заново, просто з новим іменем та ID в стейті:
Аби Terraform не видалив ресурс aws-auth з кластеру – нам потрібно видалити його зі state-файла: тоді Terraform не буде нічого знати про цей ConfgiMap, а при створенні з нашого module "aws_auth" – просто створить записи у своєму state file, але не буде нічого виконувати в Kubernetes.
Important: про всяк випадок – зробіть бекап відповідного state-file, бо будемо робити state rm.
Видалення aws_auth з Terraform State
В моєму випадку треба перейти в каталог з оточенням, envs/test-1-28, і вже звідти виконувати операції зі state.
Note: уточнюю, бо випадково все ж таки видалив aws-auth ConfigMap з production-кластеру. Але просто заново виконав terraform apply на ньому – і все без проблем відновилось.
Знаходимо ім’я модуля, як він записаний в стейті:
$ cd envs/test-1-28/
$ terraform state list | grep aws_auth
module.atlas_eks.module.eks.kubernetes_config_map_v1_data.aws_auth[0]
Можна перевірити з terraform plan output, який робили вище, аби впевнитись, що видаляємо саме його:
$ terraform state rm 'module.atlas_eks.module.eks.kubernetes_config_map_v1_data.aws_auth[0]'
Acquiring state lock. This may take a few moments...
Removed module.atlas_eks.module.eks.kubernetes_config_map_v1_data.aws_auth[0]
Successfully removed 1 resource instance(s).
Releasing state lock. This may take a few moments...
Виконуємо terraform plan ще раз, і тепер нема ніяких “destroy” – тільки створення module.atlas_eks.module.aws_auth.kubernetes_config_map_v1_data.aws_auth[0]:
# module.atlas_eks.module.aws_auth.kubernetes_config_map_v1_data.aws_auth[0] will be created
+ resource "kubernetes_config_map_v1_data" "aws_auth" {
...
Plan: 1 to add, 10 to change, 0 to destroy.
Виконуємо terraform apply:
...
module.atlas_eks.module.eks.aws_eks_cluster.this[0]: Still modifying... [id=atlas-eks-test-1-28-cluster, 50s elapsed]
module.atlas_eks.module.eks.aws_eks_cluster.this[0]: Modifications complete after 52s [id=atlas-eks-test-1-28-cluster]
...
module.atlas_eks.module.aws_auth.kubernetes_config_map_v1_data.aws_auth[0]: Creation complete after 6s [id=kube-system/aws-auth]
...
Перевіряємо сам ConfigMap – ніяк змін:
$ kk -n kube-system get cm aws-auth -o yaml
apiVersion: v1
data:
mapAccounts: |
[]
mapRoles: |
- "groups":
- "system:masters"
"rolearn": "arn:aws:iam::492***148:role/atlas-eks-test-1-28-masters-access-role"
"username": "arn:aws:iam::492***148:role/atlas-eks-test-1-28-masters-access-role"
...
І глянемо, що змінилось в AWS Console > EKS > ClusterName > Access:
Тепер у нас тут значення EKS API and ConfigMap та дві Access Entries – для WorkerNodes, та для рутового юзера.
Перевіряємо NodeClaims – Karpenter працює?
Можна поскейлити ворклоади, аби впевнитись:
$ kk -n test-fastapi-app-ns scale deploy fastapi-app --replicas=20
deployment.apps/fastapi-app scaled
$ kk get nodeclaim
NAME TYPE ZONE NODE READY AGE
default-59ms4 t3.small us-east-1a ip-10-0-46-72.ec2.internal True 59s
default-5fc2p t3.small us-east-1a ip-10-0-39-114.ec2.internal True 7m6s
Тут в принципі все – можемо переходити до апгрейду модуля з Karpenter.
Тут я пішов “методом тика” – робимо terraform plan, дивимось, що не так в результатах – фіксимо – ше раз plan. Пройшло без проблем, хоча з деякими помилками – подивимось на них.
Поточний код Terraform для Karpenter
Поточний код в modules/atlas-eks/karpenter.tf:
module "karpenter" {
source = "terraform-aws-modules/eks/aws//modules/karpenter"
version = "19.21.0"
cluster_name = module.eks.cluster_name
irsa_oidc_provider_arn = module.eks.oidc_provider_arn
irsa_namespace_service_accounts = ["karpenter:karpenter"]
create_iam_role = false
iam_role_arn = module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_arn
irsa_use_name_prefix = false
# In v0.32.0/v1beta1, Karpenter now creates the IAM instance profile
# so we disable the Terraform creation and add the necessary permissions for Karpenter IRSA
enable_karpenter_instance_profile_creation = true
}
Деякі outputs з модулю Karpenter використовуються в helm_release – module.karpenter.irsa_arn:
Робимо terraform init та terraform plan, і дивимось на помилки:
...
An argument named "iam_role_arn" is not expected here.
...
│ An argument named "irsa_use_name_prefix" is not expected here.
...
│ An argument named "enable_karpenter_instance_profile_creation" is not expected here.
Міняємо імена параметрів, і з Karpenter Diff of Before (v19.21) vs After (v20.0) додаємо створення ресурсів для IRSA (IAM Role for ServiceAccounts – роль для Karpenter), бо вона у нас є в поточному сетапі:
iam_role_arn => node_iam_role_arn
irsa_use_name_prefix – тут трохи не зрозумів, бо по документації вона стала iam_role_name_prefix, але iam_role_name_prefix в inputs нема взагалі, тому просто закоментив
iam_role_name_prefix – теж не поняв – по документації вона стала node_iam_role_name_prefix, але знов-таки – такої нема, тому теж просто закоментив
Ще раз робимо terraform plan – тепер маємо іншу проблему, з довжиною імені ролі:
...
Plan: 1 to add, 3 to change, 0 to destroy.
╷
│ Error: expected length of name_prefix to be in the range (1 - 38), got KarpenterIRSA-atlas-eks-test-1-28-cluster-
│
│ with module.atlas_eks.module.karpenter.aws_iam_role.controller[0],
│ on .terraform/modules/atlas_eks.karpenter/modules/karpenter/main.tf line 69, in resource "aws_iam_role" "controller":
│ 69: name_prefix = var.iam_role_use_name_prefix ? "${var.iam_role_name}-" : null
...
Тому задав iam_role_use_name_prefix = false, і тепер весь оновлений код виглядає так:
module "karpenter" {
source = "terraform-aws-modules/eks/aws//modules/karpenter"
#version = "19.21.0"
version = "20.0"
cluster_name = module.eks.cluster_name
irsa_oidc_provider_arn = module.eks.oidc_provider_arn
irsa_namespace_service_accounts = ["karpenter:karpenter"]
# 19 > 20
#create_iam_role = false
create_node_iam_role = false
# 19 > 20
#iam_role_arn = module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_arn
node_iam_role_arn = module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_arn
# 19 > 20
#irsa_use_name_prefix = false
#iam_role_name_prefix = false
#node_iam_role_name_prefix = false
# 19 > 20
#enable_karpenter_instance_profile_creation = true
# 19 > 20
enable_irsa = true
create_instance_profile = true
# To avoid any resource re-creation
iam_role_name = "KarpenterIRSA-${module.eks.cluster_name}"
iam_role_description = "Karpenter IAM role for service account"
iam_policy_name = "KarpenterIRSA-${module.eks.cluster_name}"
iam_policy_description = "Karpenter IAM role for service account"
# expected length of name_prefix to be in the range (1 - 38), got KarpenterIRSA-atlas-eks-test-1-28-cluster-
iam_role_use_name_prefix = false
}
...
resource "helm_release" "karpenter" {
...
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: ${module.karpenter.iam_role_arn}
EOT
]
...
}
Виконуємо terraform plan – нічого не має видалитись:
...
Plan: 1 to add, 5 to change, 0 to destroy.
В плані у нас є:
буде додано module.atlas_eks.module.karpenter.aws_eks_access_entry.node: – трохи забігаючи наперед – це треба буде відключити, зараз побачимо, чому
в module.atlas_eks.module.karpenter.aws_iam_policy.controller: будуть оновлені політики – тут наче все ОК.
в module.atlas_eks.module.karpenter.aws_iam_role.controller: буде додано правило Allow з pods.eks.amazonaws.com – для роботи з EKS Pod Identities
Наче виглядає ОК – давайте деплоїти і тестити.
Логи Karpenter запущені, NodeClaim зараз вже є, пінги на тестовий Ingress/ALB йдуть.
EKS: CreateAccessEntry – access entry resource is already in use on this cluster
Робимо terraform apply, і – як неочікувано! – маємо помилку:
...
│ Error: creating EKS Access Entry (atlas-eks-test-1-28-cluster:arn:aws:iam::492***148:role/test-1-28-default-eks-node-group-20240710092944387500000003): operation error EKS: CreateAccessEntry, https response error StatusCode: 409, RequestID: 004e014d-ebbb-4c60-919b-fb79629bf1ff, ResourceInUseException: The specified access entry resource is already in use on this cluster.
Для Kaprneter теж є нова змінна create_access_entry, і вона теж з дефолтним значенням true.
Задаємо її в false, бо в моєму випадку ноди створенні з Karpenter використовують ту саму IAM Role, що ноди з module.eks.eks_managed_node_groups:
...
# To avoid any resource re-creation
iam_role_name = "KarpenterIRSA-${module.eks.cluster_name}"
iam_role_description = "Karpenter IAM role for service account"
iam_policy_name = "KarpenterIRSA-${module.eks.cluster_name}"
iam_policy_description = "Karpenter IAM role for service account"
# expected length of name_prefix to be in the range (1 - 38), got KarpenterIRSA-atlas-eks-test-1-28-cluster-
iam_role_use_name_prefix = false
# Error: creating EKS Access Entry ResourceInUseException: The specified access entry resource is already in use on this cluster.
create_access_entry = false
}
...
Ще раз виконуємо terraform apply – і тепер все пройшло без помилок.
В логах Karpenter взагалі нічого, тобто Kubernetes Pod не перестворювався, пінги на тестову апку продовжують йти.
Можна її поскейлити, аби тригернути створення нових Karpenter NodeClaims:
$ kk -n test-fastapi-app-ns scale deploy fastapi-app --replicas=10
deployment.apps/fastapi-app scaled
Основне – це зміни з aws-auth: модуль буде видалено, тому варто відразу вже переходити на authentication_mode = API, а для цього нам треба перенести всіх наших юзерів та ролі з aws-auth ConfigMap в EKS Access Entries.
Крім того, варто вже переходити на нову систему роботи з ServiceAccounts – EKS Pod Identities.
І виходить, що зміни буде дві:
в aws-auth є записи для IAM Roles та IAM Users: їх треба створити як EKS Access Entries
Але все це я вже буду робити новим проектом в окремому репозиторії, який буде займатись IAM та доступом до EKS і RDS. Можливо, опишу в наступному пості.
Виглядає прям дуже круто. Запустив, подивився на це діло – і побіг писати цей пост.
Запускається фактично в пару кліків з Docker Compose, файл є “в комплекті” експортера.
Як це працює?
Виявляється, у EcoFlow є сервіс mqtt.ecoflow.com, куди девайси відправляють телеметрію. В моєму мобільному застосунку я цього не знайшов, але нагуглив ось такий скрін в треді Home Automation – My Journey:
Тобто, коли ми реєструємо девайс в мобільному застосунку – EcoFlow починає відправляти метрики, використовуючи логін/пароль, з яким ми реєструємось в самому застосунку.
Ну і за такою ж логікою працює сам експорт – див. код у ecoflow_exporter.py – ми йому в параметрах задаємо свій логін-пароль (не банкінг, можна робити), він з цим логіном-паролем підключається до api.ecoflow.com/auth/login, отримує токен, і вже з цим токеном йде на mqtt.ecoflow.com:8883.
А з вже mqtt.ecoflow.com отримує прям купу всяких цікавих метрик, на які ми сьогодні і подивимось.
Запуск EcoFlow Prometheus Exporter
Тут все прям дуже просто, і описано в документації Quick Start.
Клонуємо репозиторій:
$ git clone https://github.com/berezhinskiy/ecoflow_exporter
$ cd ecoflow_exporter/docker-compose/
В файлі compose.yaml задаємо логіни-паролі – для Grafana та для EcoFlow API:
В DEVICE_SN вказуємо серійний номер девайсу – є в мобільному застосунку:
І запускаємо контейнери:
$ docker-compose up
Grafana dashboard
Логінимось в Grafana, переходимо до http://localhost:3000/dashboards, імпортуємо дашборду, ID 17812:
І маємо купу цікавих графіків:
Наприклад, ось момент, коли я відключив холодильник – запасу батареї відразу стало з 8 до 16 годин:
Або момент, коли у квартирі з’явилось світло, і почалася зарядка батарей:
Як ми і рахували в попередньому пості – навантаження в 1270 ват/годину, батареї 56 вольт – маємо 22.6 ампер струму – 19 в метриках плати BMS (Battery Management System), і ще 3 ампери, мабуть, на інвертор і інші системи станції.
При цьому з розетки струм 5.7 ампери (графік Current):
>>> 1270/220
5.7
Цікаві метрики і по температурі:
Інвертор під час роботи від батарей гріється аж 80 градусів – хоча на балконі +25 (стоїть окремий термометр біля зарядних).
А як тільки світло з’явилось, і станція відключила роботу з батарей (і, відповідно, інвертор) – то температура впала.
Хоча, можливо, інвертор працює постійно, якщо EcoFlow є Online UPS (див. Типи ДБЖ), але з меншим навантаженням. Але мені здається, що EcoFlow все ж є Line-Interactive системою.
Повний список метрик є в документації експортера, правда без деталей. Метрики дефолті від самого EcoFlow, просто конвертується ім’я: bms_bmsStatus.maxCellTemp -> bms_bms_status_max_cell_temp.
Алерти в Telegram
Алерти відправляються через Telegram API та бота, токен якого можемо задати в файлі конфігурації Alertmanager alertmanager/alertmanager.yml, а самі алерти описані в файлі prometheus/alerts/ecoflow.yml – можемо тут їх потюнити, чи написати власні.
З @BotFather і командою /newbot створюємо бота:
Створюємо канал:
Додаємо бота в канал:
Знаходимо Telegram Group Chat ID.
Самий простий спосіб, який я знайшов – це через web.telegram.org – відкриваємо цей канал, і зверху маємо ID:
Відправляти можемо напряму від бота до нашого юзера – тоді в chat_id вказуємо свій ID.
Якщо все ж використовуємо групу – то ID вказуємо разом зі знаком “-“, тобто в моєму випадку це “-1002162514981“.
Редагуємо файл alertmanager.yml, додаємо параметри для Телеграму:
Перезапускаємо контейнери, і перевіряємо в Prometheus:
Алерт затригерився.
Якщо відправку робили від бота до себе, як юзера – то знаходимо нашого бота, клікаємо Send Message, аби ініціювати чат, бо сам він першим вам писати не зможе – в логах Alertmanager буде помилка “bot can’t initiate conversation with a user“:
І відразу маємо від нього повідомлення з алертом:
Якщо робили через групу – то алерт відразу прийде туди:
Взагалі є сенс перевірити алерти, до, наприклад, в EcoFlowHalfBattery використовується метрика ecoflow_bms_bms_status_f32_show_soc, яка в мене порожня – перевіряємо на сторінці http://localhost:9090/graph:
Ще одна крута фіча, яку Амазон показав ще на минулому re:Invent в листопаді 2023 – це зміни в тому, як AWS Elastic Kubernetes Service виконує аутентифікацію та авторизацію юзерів. При чому це стосується не тільки саме користувачів кластеру, а і WorkerNodes.
Тобто, не дуже-то нова схема – але в мене ось тільки зараз дійшли руки до апгрейду кластеру з 1.28 на 1.30, заодно буду оновлювати версію модулю terraform-aws-modules/eks зі змінами ESK Access Management API, бо зараз ми на версії 19, а зміни були додані в версії 20 (див. v20.0.0 Release notes),
Про Terraform, мабуть, поговоримо в наступному пості, а сьогодні давайте глянемо як нова система працює, і що вона нам дозволяє. А вже знаючи це – візьмемо Terraform, і взагалі подумаємо про те, як організувати роботу з IAM з урахування змін в EKS Access Management API та EKS Pod Identities.
Загалом по аутентифікації/авторизації в Kubernetes можна ще глянути старі пости:
Раніше ми мали спеціальний aws-auth ConfigMap, в якому описувались WorkerNodes IAM Roles, всі наші юзери та їхні групи.
Відтепер, ми можемо керувати доступами в EKS напряму через його API використовуючи AWS IAM в ролі аутентифікатора. Тобто, юзер логіниться в AWS, AWS виконує аутентифікацію – перевіряє, що це саме той юзер, за якого він себе видає, а потім, коли юзер підключається до Kubernetes – то виконується його авторизація – перевірка прав доступу до кластеру і в самому кластері.
При цьому ця схема чудово працює з RBAC самого Kubernetes.
І ще одна дуже важлива деталь – що ми нарешті можемо позбутись “дефолтного root-юзера” – прихованого адміністратора кластера, від імені якого він створювався. При чому раніше ми не мали змоги ніде його побачити або змінити, що іноді спричиняло проблеми.
Отже, якщо раніше нам потрібно було самим керувати записами в aws-auth ConfigMap, і не дай боже його зламати (а в мене траплялось через чи то кривий маніфест, чи то не дуже прямі руки) – то тепер ми можемо винести управління доступами в окремий Terraform-код, і управляти доступами набагато простіше і з меншим ризиком.
Зміни в IAM та EKS
Тепер у нас в EKS є дві нові сутності – Access entries та Access policies:
Amazon EKS Access Entries – запис в EKS про об’єкт, який пов’язаний з AWS IAM роллю чи юзером
описує тип (звичайний юзер, EC2, etc), Kubernetes RBAC Groups, або EKS Access Policy
Amazon EKS Access Policy – політика в EKS, яка описує права для EKS Access Entries. І це політики саме EKS – ви не знайдете їх в IAM.
Підозрюю, що десь під капотом ці EKS Access Policy просто мапляться на Kubernetes ClusterRoles.
Ці політики ми підключаємо до IAM Role чи IAM user, і під час підключення до Kubernetes-кластеру EKS Autorizer перевіряє які саме права є у цього користувача.
Замість дефолтних AWS managed IAM Policy ми при створенні EKS Access Policy можемо вказати ім’я Kubernetes RBAC Group – і тоді замість EKS Autorizer буде використано механізм Kubernetes RBAC – далі подивимось, як це працює.
Для Terraform в terraform-provider-aws версії 5.33.0 були додані два нових відповідних типи resource – aws_eks_access_entry та aws_eks_access_policy_association. Але зараз все будемо робити руками.
Налаштування Cluster access management API
Перевіряти як воно працює будемо на існуючому кластері версії 1.28.
Зараз у нас включено ConfigMap (той самий aws-auth) – міняємо на EKS API and ConfigMap – так ми залишимо і старий механізм, і протестуємо новий (в Terraform це також можна зробити):
Звертаємо увагу на попередження “Once you modify a cluster to use EKS access entry API, you cannot change it back to ConfigMap only” – але terraform-aws-modules/eks версії 19.21 ці зміни ігнорує і нормально працює далі, тож можна міняти руками.
Тепер кластер буде виконувати авторизацію юзерів і з aws-auth ConfigMap, і з EKS Access Entry API, з перевагою до Access Entry API.
Після переключення на EKS Access Entry API відразу маємо нові EKS Access Entries:
І як раз тепер ми можемо побачити того самого “прихованого root-юзера” – assumed-role/tf-admin, бо Teraform працює саме від цієї IAM-ролі, і в моєму сетапі це робилось як через цей механізм EKS, від якого тепер можна буде позбутись.
Але з поточного aws-auth ConfigMap взято не все – роль для WorkerNodes є, а от решта записів (юзери з mapUsers та ролі з mapRoles) автоматично не додались. Хоча під час зміни параметра API_AND_CONFIG_MAP через Teraform це наче має відбутись – потім перевіримо.
З юзером закінчили – тепер треба з ним підключитись до кластеру.
EKS: створення Access Entry
Можемо зробити або через AWS Console:
Або з AWS CLI (з робочим профайлом, а не новим, бо в нього ніяких прав нема) і командою aws eks create-access-entry.
В параметрах передаємо ім’я кластера та ARN юзера чи ролі, яких підключаємо до кластеру (але, мабуть, більш коректно буде сказати “для яких створюємо точку входу на кластер“, бо сутність називається Access Entry):
Зверніть увагу на --access-scope type=cluster – зараз ми видали ReadOnly права на весь кластер, але можемо обмежити конкретним неймспейсом(ми) – далі спробуємо.
Глянемо ще раз в AWS Console:
Access Policy додано.
Пробуємо kubectl:
$ kubectl auth can-i get pod
yes
Але не можемо створити под – бо маємо ReadOnly права:
На майбутнє, відключити створення такого юзеру при створенні кластера з aws eks create-cluster можна параметром bootstrapClusterCreatorAdminPermissions=false.
А зараз давайте замінимо його – додамо нашому тестовому юзеру адмін-права і видалимо дефолтного root.
Повторяємо aws eks associate-access-policy, але тепер в --policy-arn вказуємо AmazonEKSClusterAdminPolicy:
$ aws --profile work eks delete-access-entry --cluster-name atlas-eks-test-1-28-cluster --principal-arn arn:aws:iam::492***148:role/tf-admin
Namespaced EKS Access Entry
Замість того, аби видавати права на весь кластер з --access-scope type=cluster – ми можемо зробити юзера адміном тільки у конкретних неймспейсах.
Не відключаємо нашого тестового юзера – бо наразі це наш єдиний адмін. Давайте візьмемо звичайного IAM User, то зробимо його адміном тільки в одному Kubernetes Namespace.
Створюємо новий неймспейс:
$ kk create ns test-ns
namespace/test-ns created
Створюємо нову EKS Access Entry для мого AWS IAM User:
$ aws --profile work eks create-access-entry --cluster-name atlas-eks-test-1-28-cluster --principal-arn arn:aws:iam::492***148:user/arseny
І підключаємо AmazonEKSEditPolicy, але в --access-scope задаємо тип namespace та вказуємо ім’я цього NS:
В access-scope ми можемо задати або clutser, або неймспейс. Не дуже гнучко – але для гнучкості у нас є RBAC.
Генеруємо новий kubectl context з --profile work, де profile work – мій звичайний AWS User, для якого ми створювали EKS Access Entry з AmazonEKSEditPolicy:
$ aws --profile work eks update-kubeconfig --name atlas-eks-test-1-28-cluster --alias test-cluster-arseny-user
Updated context test-cluster-arseny-user in /home/setevoy/.kube/config
І перевіряємо права – спочатку в default Namespace:
$ kubectl --namespace default auth can-i create pod
no
І в тестовому неймспейсі:
$ kubectl --namespace test-ns auth can-i create pod
yes
Nice!
Все працює.
EKS Access Entry та Kubernetes RBAC
Замість того, щоб підключати EKS Access Policy від AWS, яких всього чотири – ми можемо використати звичайний механізм Kubernetes Role-Based Access Control, RBAC.
Це виглядає так:
в EKS створюємо Access Entry
в параметрах Access Entry вказуємо Kubernetes RBAC Group
а далі за звичною схемою – використовуємо RBAC Group та Kubernetes RoleBinding
Тоді ми пройдемо аутентифікацію в AWS, після чого AWS “передасть” нас до Kubernetes, а той вже виконає авторизацію – перевірку наших прав в кластері – на основі нашої RBAC-групи.
Видаляємо створену Access Entry для мого IAM User:
$ aws --profile work eks delete-access-entry --cluster-name atlas-eks-test-1-28-cluster --principal-arn arn:aws:iam::492***148:user/arseny
Створюємо його заново, але тепер додаємо --kubernetes-groups:
Перший пост цієї “серії” (яка взагалі не планувалась як серія) – я написав ще у 2022, коли вперше довелося почати розбиратись з електрикою і тим, як забезпечити електрохарчування вдома під час відключень:
Цей пост я почав писати в кінці жовтня – на початку листопада 2023 року, коли готувався до зими, але недописав, та й зима пройшла без відключень.
Втім, раптом питання стало дуже актуальним влітку 2024 року – тож давайте повернемось до цієї теми.
Отже, про що будемо говорити:
згадаємо що таке вольти/ампери/вати
як порахувати потужність електроприладів вдома і прикинути необхідний запас енергії
як ми цей запас можемо тримати – які типи зарядних станцій та інших подібних рішень є
розглянемо питання, пов’язані з ДБЖ та інверторами – на що звертати увагу при виборі, як правильно користуватись
і поговоримо про акумулятори – які типи є, їхні характеристики, плюси-мінуси, і розглянемо питання зарядки
Відразу хочу нагадати, що я не електрик, а звичайний айтішник.
І цей пост – не “повний гайд” з точними рекомендаціями що вибрати і як налаштувати, а більше має на меті дати загальну інформацію по основним питанням.
Вольти: напруга – по суті схожа з величиною тиску води в трубі – чим він вище, тим с більшою силою йде вода з крана, тобто з якою “силою” електрони штовхаються через провідник. Позначається як В або V.
Ампер: сила струму – можна порівняти з кількістю води, що протікає через трубу за одиницю часу, або з кількістю даних, що передається через мережевий канал за одиницю часу (наприклад, кількість пакетів або бітів). В цьому випадку ампери визначають, скільки електричних зарядів проходить через провідник за одиницю часу.
Ват: потужність – можна порівняти з кількістю води в літрах, яке виллється з крана. Позначається як W (Вт).
Наприклад, якщо знаємо, що батареї EcoFlow працюють на 50 вольтах, а споживана потужність під час зарядки 2000 ват – то отримуємо силу струму зарядки у 2000/50 == 40 ампер. Але про це все будемо детальніше говорити далі.
Або навпаки: якщо зарядний пристрій працює на 12 вольтах і видає 10 ампер сили струму – то він передає 120 ват, а якщо 24 вольти на тих жеж 10 амперах – то вже 240 ват.
Ще досить гарне пояснення – побачив колись давно десь в коментарях на Ютубі, і мені воно прям дуже подобається:
– Напруга (V): ширина річки – Сила струму (A): швидкість течії води в річці Тому одну і ту ж потужність може дати й широка ріка (висока напруга, Вольт) з повільною течією (слабкий струм, Ампер) – і вузька річка (низька напруга, Вольт) зі швидкою течією (сильний струм, Ампер).
Чим швидше ріка і чим вона ширше – тим більше води (Ват) за одиницю часу.
Тобто:
Напруга (V) – ширина річки: широка річка може передати більше води при тій же швидкості течії (силі струму)
хоча ширина річки тут не дуже вдала аналогія, бо напруга (в вольтах) описує різницю електричних потенціалів, що штовхає електрони через провідник, подібно до того, як різниця висот штовхає воду вниз по річці
Сила струму (A) – швидкість течії води в річці:
умовна річка шириною в 10 метрів при швидкості течії води в 10 метрів на секунду передає 100 кубометрів води в секунду
умовна річка шириною в 100 метрів при швидкості течії води в 1 метр на секунду також передає 100 кубометрів води в секунду
Потужність (W) – кількість води за одиницю часу: потужність визначається як кількість енергії, переданої (або спожитої) за одиницю часу, і в цій аналогії це кількість води, що проходить через річку за певний проміжок часу (наші 100 кубометрів в секунду)
Потужність можна отримати за допомогою формули P=V×I, де P – потужність в ватах, V – напруга в вольтах, а I – сила струму в амперах.
Рахуємо потужність приладів вдома
Ми не будемо брати до уваги прилади по типу стиральної машини або кавоварки, бо під час відключення електроенергії ви навряд чи будете їх живити від батарей.
Але є така ось табличка, аби мати уявлення про потужність різних приладів:
Отже, що нам знадобиться? Рахуємо те, що має працювати постійно, тобто телевізор, ігрову приставку або ігровий ПК пропускаємо.
В моєму випадку це:
холодильник: 80 ват (маємо на увазі пусковий струм до х10 від номінального – але про це поговоримо, коли будемо розглядати інвертори та ДБЖ)
газовий котел для опалення і гарячої води для душа/кухні (у нас в ЖК свої скважини, насоси та генератори для них): 130 ват (і теж має пусковий струм, хоча тут, мабуть, поменше, бо нема компресора, як у холодильнику – тільки двигун насоса)
і “куточок користувача”:
настільна лампа: 8 ват
колонки: 10 ват
роутер: 18 ват
медіаконвертер (оптика => ethernet): 12 ват
монітор: 40 ват
ноутбук: 60 ват
Заміряти можна ватметром, наприклад мій холодильник:
Але холодильник і котел працюють не постійно, а вмикаються та вимикаються при потребі, коли змінюється температура. Давайте вважати, що вони працюють 50/50 від загального часу.
Тоді холодильник і котел працюючи одночасно споживають ~210 ват, умовно кажучи, годину через годину. Значить в розрахунок беремо 100 ват/годину.
Тепер додаємо решту – “куточок користувача”, і тут маємо ще ~150 ват/годин.
Отже, разом це 250 ват/годину – і тепер ми можемо рахувати необхідний запас.
Самий довгий блекаут 2022/2023 був три доби, а на добу нам потрібно:
холодильник і котел: працюють цілодобово, по 100 Вт/г – на добу потрібен запас у 2400 ват/годин
“куточок користувача”: працюють умовних 14 годин на добу, по 150 Вт/г – на добу потрібен запас у 2100 ват/годин
Тобто разом необхідний добовий запас енергії – 4500 ват/годин. Хоча взимку 2022/2023 я обходився без холодильника – м’ясо з морозильника вивішував в пакеті за вікном.
Тепер можна поговорити про те, як цей запас отримувати та зберігати.
Варіанти енергозабезпечення: зарядні станції, акумулятори, ДБЖ
Ну і тут вибір на всі смаки:
зарядні станції типу EcoFlow/Bluetti: найкращій варіант з точки зору безпеки та обслуговування, бо “it just works”, але і найдорожчий
саморобні станції від майстрів: дешевші від EcoFlow, прості в обслуговуванні, але є ризики що китайська начинка накриється, і станція перестане працювати, або можливі проблем з батареями (і навіть пожежа)
ДБЖ та акумулятори: більш складний варіант з точки зору зборки самої системи, більш небезпечний ніж EcoFlow (але безпечніший за саморобні станції), проте дешевше, ніш EcoFlow
окремо зарядне, інвертор та акумулятори: найскладніший з точку зору зборки системи та її обслуговування, і найбільш небезпечний при порушенні правил використання
Трохи окремо стоять сонячні батареї та генератори – але я про них писати не буду, бо живу у квартирі, і генератор ставити на балконі не можна, а сонячні батареї вішати хіба що за балкон – але тут є питання в їх ефективності, особливо взимку, бо балкон на захід, і питання їхнього монтажу – треба або шукати майстрів, а це не дуже просто, або робити самому – а в мене не настільки прямі руки.
Найбільші ризики використання всіх подібних приладів – це пожежа, і ці ризики треба мати на увазі, бо ми багато в новинах чуємо про черговий випадок вибухів на балконах.
Наприклад я собі “від гріха подалі” ще у 2022 купив два вогнегасники:
Ще – якщо дійдуть руки, буде час та натхнення – то хочу зробити з Adruino датчики тепла (міряти температуру на балконі, це стоять зарядні станції та акумулятори, і температуру самих акумуляторів), та датчик диму. Хоча можна просто купити готові рішення від того ж Ajax Systems.
Окей. Тепер давайте розберемо ці системи детальніше.
Зарядні станції EcoFlow/Bluetti/etc
Я їх не розглядав в минулих записах, бо в мене їх не було 🙂
Але в цьому році я все ж купив собі EcoFlow DELTA Max 2000 (2016 Вт·год). Обійшлася вона мені в ~59.000 грн (брав на Rozetka), і я вважаю, що воно того варте.
В мене Li-ion батареї, а аналогічна станція з LiFePo4 буде коштувати близько 100.000 (це зараз, при курсі долара ~40 грн). Про батареї будемо говорити окремо і детальніше далі, але в принципі LiFePo4 варті свої грошей.
Коли я себе вговорював купити її, то останнім було – “Блін, ти собі на день народження подарував Samsung S23 Ultra за 50.000 грн! І це просто телефон – а тут питання нормального функціювання!“.
Тобто – да, воно коштує грошей, і не мало, проте ви просто його вмикаєте – і спокійно собі живете.
Переваги:
простота: воно просто працює, і нічого в принципі від вас не вимагає
безпека: це, мабуть, максимальний рівень безпеки в плані як вибуху/пожежі, так і випадкового замикання дротів або удару током вас, як юзера
надійність: це, мабуть, максимальний рівень безпеки надійності з точки зору поломки – якщо правильно використовувати та не перенавантажувати
швидкість зарядки: це дуже важливий момент, і про нього будемо говорити далі, але щодо EcoFlow – то він свої 2000 Вт/год “заливає” за півтори години – і це дуже круто
мобільний додаток: керування параметрами та відображення даних по споживанню/запасу енергії – дуже зручно, дуже корисно
компактність: для розміщення такого девайсу знадобиться набагато менше місця, ніж для звичайних акумуляторів на той самий об’єм енергії
Недоліки:
фактично, він тут один – це ціна, інших поки не побачив
ще до недоліків можна додати, що ані станція, ані мобільний застосунок не попереджають про низький заряд батареї – станція просто мовчки виключається
Щодо шумності: EcoFlow DELTA Max доволі шумна, і включає кулери як під час зарядки, так і під час використання. Спати з такою станцією в одній кімнаті буде не дуже зручно (навряд чи можливо взагалі), тож у випадку однокімнатної квартири-студії треба або вимикати станцію на ніч, або ставити десь в кладовку чи на балкон, а враховуючи відносну пожежобезпечність такого приладу – цілком собі варіант тримати десь там.
Але слідкуйте за температурою, особливо якщо балкон на сонячній стороні, бо в мене станція іноді нагрівається до +45, і починає видавати попередження – доводиться ставити вентилятор. Або закривайте вікна балкона, наприклад Сонцезахисною плівкою.
Мобільний додаток для EcoFlow виглядає ось так – тут підключений ігровий ПК, монітор, роутер, колонки:
Налаштування девайсу:
І під час зарядки – 2000 ват (це на початку, потім зменшується сила струма на батареї, і, відповідно, споживана потужність):
EcoFlow: рекомендації з використання
Кілька рекомендацій по використанню EcoFlow, хоча це відноситься і до інших аналогічних девайсів.
Треба мати на увазі, що деградація батарей – це зменшення їх ємності. 800 циклів в документації EcoFlow – це зменшення ємності до 80% від початкової, а не повністю вихід з ладу. До того ж зараз наче можна легко замінити батареї на нові.
Мінімальний та максимальний рівень заряду
Має сенс виставити обмеження на мінімальний та максимальний рівень заряду – 20% мінімум, і 90% максимум. В такому випадку станція не буде повністю висаджувати батареї, що для них не є ОК, і не буде повністю їх заряджати. Хоча я впевнений, що контролер самої станції і так має потрібні обмеження – але зайвим не буде.
Не тримати постійно на зарядці
Друге, що я роблю – це не тримаю її на зарядці постійно, бо по документації вона розрахована на 800 циклів заряду/розряду (це при Li-ion батареях, для LiFePo4 це здається 3000 циклів). Так – доводиться робити додаткові рухи руками, і перемикати розетки/подовжувачі, коли вимикають/вмикають світло – зато проживе станція довше, бо світло вимикається кілька раз на добу, і кілька раз на добу вмикається зарядка батарей.
Хоча просто тримати її ввімкненою зручно, бо станція вміє працювати як UPS, тобто коли в мережі є енергія – станція живить прилади від мережі, а як в мережі пропадає – то переключається на батареї.
Не перенавантажувати станцію
Ну і, звісно, не можна перенавантажувати станцію. В неї заявлена видача у 2000 ват, є режим X Boost до 4600 ват. Тож не варто підключати стиральну машинку, кавоварку і чайник одночасно.
Вимикати інвертор
Також майте на увазі, що вбудований інвертор станції (взагалі будь-який інвертор) також споживає енергію, навіть якщо не підключено ніяких приладів, і споживає близько 20-30 ват на годину – тобто він з’їсть ~500 ват запасу за добу, навіть працюючи вхолосту. Тому можете або вимикати AC вручну, або налаштувати автовимкнення (дефолт – 12 годин).
Наприклад, з включеним AC при заряді в 34% станція видає 1 день роботи (без приладів):
А з відключеним – 99 годин:
Зменшувати швидкість зарядки
Можна зменшити максимальне навантаження станції під час зарядки батарей (2000 ват/годину в моїй).
Аби включити опцію власного налаштування – на задній панелі треба переключити Fast – Slow/Custom:
Після чого в мобільному застосунку стане доступним налаштування AC charge speed:
Це зменшить струм заряду, і батареї будуть почуватись краще.
Саморобні зарядні станції від “умільців”
Я не скажу, що рекомендував би такий варіант, бо тут занадто багато “як пощастить”: як пощастить з руками майстра, як пощастить з компонентами (Китай/не Китай і т.д.).
Але в цілому – варіант робочий, і в мене одне таке чудо є:
Заявлені такі ж самі 2000 Вт/годин, але реально вдається накопичити близько 1300 – тестував як раз на ігровому ПК, який споживає ~300 ват, і працює близько 4 годин.
В середині це чудо виглядає так:
Синя плата справа – це BMS, Battery Management System, яка контролює заряд акумуляторних батарей – рівень їх заряду/розряду, і додатково може вміти в захист від замикань, перенавантаження тощо.
Переваги:
швидкість зарядки: зазвичай такі станції роблять з потужними зарядками, наприклад в мене вона заряджається за пару годин
компактність: конкретно ця більша за EcoFlow, але це все ще менше місця, ніж 2 акумулятори і ДБЖ, плюс тут все в одному корпусі
ціна: на Olx такі системи продаються за ціною в районі 20.000 грн за 2000 Вт/г, що набагато дешевше, ніж EcoFlow, який, нагадаю, коштує майже 60.000 за 2.000 Вт/г
Недоліки:
надійність: Китай іноді інвертор починає пищати під час роботи, і я поки не зрозумів чому (5 коротких звукових сигналів – хтось, може, в курсі?)
безпека: тут знов-таки покладаємось на руки майстра – наскільки він все правильно зробив (якість пайки, сама схема роботи), наскільки якісні компоненти використовував (а враховуючи факт, що такі станції намагаються зробити не надто дорогими – то можливі нюанси)
шум: під час зарядки гудить, як літак на форсажі – набагато гучніша за EcoFlow
ДБЖ та зовнішні акумулятори
Ще один варіант – це купити окремо акумулятор, і окремо до нього – ДБЖ.
Про типи і ємність акумуляторів теж будемо говорити далі.
Але цей сетап більш резервний, і купувався ще у 2022, коли вибору особливо не було, бо тоді скупали просто все, що з’являлось.
Переваги:
ну, воно працює… в принципі це, мабуть, все
ціна: насправді, не впевнений, чи заносити це в плюси, чи в мінуси, але в порівнянні з EcoFlow цей варіант буде дешевшим (хоча в порівнянні з EcoFlow, мабуть, будь-що буде дешевшим 🙂 )
мені сам ДБЖ обійшовся у 2022 році в 23.000 грн, зараз такий коштує від 16.000 (на Розетці) до тих жеж 23.500 гривень
акумулятор – якщо брати ті ж умовні 2.000 ват/годин (200 ампер/годин на 12 вольт, тобто 1 шт), то за Li-ion це буде близько 20.000 грн, а за LiFePo4 – 35-50 тисяч гривень
надійність: через те, що тут два компоненти, то і говорити про них треба окремо, але щодо самого ДБЖ – то CyberPower наче доволі відома компанія, тож тут можна поставити плюс
безпека: при умовах правильного використання більш безпечне рішення в порівнянні в саморобними станціями, але залежить від самого ДБЖ та акумуляторів, але точно менш безпечне за EcoFlow
Недоліки:
шум: ДБЖ (принаймні цей) доволі шумний під час зарядки, в кімнаті не поставиш (приблизно на рівні EcoFlow)
надійність: а тут вже про акумулятор, і тут багато чого “It depends” – і виробник акумулятору, і те, наскільки ДБЖ правильно заряджає акумулятор(и), і температура в місці, де акумулятор стоїть – бо вони не люблять ані холод, ані жару
безпека: і тут знов-таки дуже багато залежить від акумулятора і умов експлуатації, тому запишу в мінуси, бо ризики вибуху/пожежі акумулятора є
швидкість зарядки: багато залежить від ДБЖ та акумуляторів, але скоріш за все це буде в кілька разів довше (якщо не в 10 раз), ніж той же EcoFlow
компактність: це система, яку треба один раз поставити, і забути, бо з місця на місце просто так не перенесеш – треба розбирати компоненти, і переносити окремо
Окремо зарядне, інвертор та акумулятори
В мене такої системи нема, але в Телеграм-чаті RTFM після публікації першого посту хтось скидав фотки системи вдома, де стоїть акумулятор, окремо до нього підключається зарядне, і окремо – інвертор.
Переваги:
ціна: єдина, мабуть, перевага такого рішення – це ціна, і то не впевнений
Недоліки:
простота: ніякої, бо маєте, по-перше, дуже ретельно вибрати всі компоненти – і акумулятор, і відповідний зарядний пристрій, по-друге – це все з’єднується окремо, і маєте або перемикати постійно вручну – або знов-таки мати мороку з якимись додатковими компонентами контролю/перемикання
безпека: трохи є, але гірше за інші рішення, бо дуже багато “It depends” – як правильно виберете компоненти, надійність компонентів, їхній моніторинг і так далі
компактність: і знов мінус, бо в рішення з ДБЖ+акумулятор у вас принаймні зарядне+інвертор в одному корпусі, а тут – купа проводів
Знайшов фотки з чату в Телеграмі:
І ще окремо сам зарядний пристрій:
Імхо – дуже так собі рішення, бо надто багато геморою.
Втім, на Youtube-каналі @izmailinvertor є багато відео, де людина з прямими руками розказує про такі системи.
Висновки
Тож якщо обирати рішення для дому – то, звісно, якщо є гроші – найкращим варіантом будуть станції по типу EcoFlow.
Другий варіант – це ДБЖ+зовнішні акумулятори. З недоліків – швидкість зарядки і компактність, але має бути дешевшим.
Варіант з саморобними зарядними станціями з Olx особисто я не рекомендував би, бо віри в надійність і безпеку мало. Хоча так – сам вдома таку станцію тримаю, але купував, бо на той час не було грошей, економив, а викинути тепер жалко – “запас карман нє тянєт”.
Ну і варіант “мати все окремо” – це треба бути або дуже шарящою людиною аби все зібрати самому, або мати надійних людей/компанії, які таку систему можуть зібрати для вас. Втім, це все одно доволі геморно з точки зору обслуговування, а я людина лінива.
ДБЖ та інвертори
Мабуть, окремо варто трохи поговорити про різницю між ними, і як їх вибирати.
Отже, ДБЖ – це “all inclusive” система, де в одному корпусі ви маєте і зарядний пристрій для акумуляторів, і інвертор. Зарядний пристрій, власне, заряджає батареї, а інвертор – “розряджає”, тобто передає струм з них на побутові прилади, при потребі збільшуючи струм з 12/24/etc вольт до звичних 220, і перетворюючи його з постійного на змінний.
Інвертор жеж – це тільки перетворення постійного струму 12 вольт з акумулятору на 220 вольт змінного струму, і нічого більше. Хіба що якийсь додатковий захист від замикань/перенавантаження тощо.
І не забуваємо, що будь-який інвертор, навіть в EcoFlow частину енергії витрачає на перетворення струму, приблизно 15%. Тобто, якщо батарея на 1000 ват/годин – то реальної ємності буде 850.
Вибір ДБЖ/інвертора
При виборі ДБЖ є три основних параметри, на які треба звертати увагу – це вихідна та зарядна потужність, і синусоїда.
Вихідна потужність ДБЖ та інверторів
На початку ми порахували, що разом в мене вдома споживається 250 ват/на годину, тож мінімально ДБЖ має бути з запасом хоча б х2, тобто 500 ват – мій CyberPower CPS1000E видає до 700 ват, а EcoFlow – до 2000.
Але тут треба враховувати нюанс з системами типу холодильника, у яких дуже високе споживання при старті двигуна/компресора – від 800 до 1500 ват, тому від CyberPower його захарчувати не вийде, бо або спрацює захист і ДБЖ виключиться – або захист не спрацює, і ДБЖ згорить.
Вольт-ампери та вати
Також потужність ДБЖ часто вказується у вольт-амперах (ВА, також позначається як “повна потужність”), а споживана потужність приладів зазвичай в ватах (“активна потужність”). В такому випадку, аби перевести потужність в вольт-амперах в вати – вольт-ампери множимо на коефіцієнт потужності приладу, зазвичай це 0.6 – 0.9, але має бути вказаний в документації приладу. Можна просто брати середнє значення – 0.85.
Зарядна потужність ДБЖ
І оце дуже важливий момент, про який будемо говорити далі, бо згадуючи зиму 2022/2023, коли світло іноді бувало по кілька годин на добу – то за ці кілька годин вам потрібно повністю зарядити ваші акумулятори.
При цьому є нюанс і з самими акумуляторами, які не дуже люблять високі токи, і якщо якийсь AGM кожен день по кілька раз заряджати на 10 амперах – то спасибі він вам не скаже. Втім, це знов-таки окрема тема, про яку говоритимо далі.
Типи ДБЖ
ДБЖ поділяються на різні типи – в залежності від задачі, для якої проектувались:
резервні ДБЖ (Off-Line, Standby): самі прості, призначені виключно для підстрахування на кілька хвилин – поки світло є, то живлення передається напряму з мережі і паралельно заряджаються акумулятори, а коли вмикається напруга в мережі – то вмикається живлення від акумуляторів
лінійно-інтерактивні (Line-Interactive): поки в мережі є напруга – то передають її на прилади і паралельно згладжують коливання напруги в електромережі, тобто працюють як стабілізатори напруги, при вимиканні світла або перепадах напруги – перемикаються на роботу від батарей
інверторні або ДБЖ безперервної дії (Online): найбільш просунуті системи, які постійно перетворюють струм з мережі в постійний струм, вирівнюють будь-які коливання, перетворюють назад в змінний, а потім передають на прилади
Форма вихідної напруги
ДБЖ можуть видавати чисту або модифіковану синусоїду змінного струму, і деякі прилади вимагають саме чисту – наприклад, двигун холодильника, газовий котел або медична апаратура.
Дуже важлива характеристика ДБЖ, яку треба мати на увазі.
Тему про акумулятори, мабуть, варто було б винести взагалі окремим постом, бо тема досить велика і місцями складна.
Але давайте спробуємо відносно стисло про неї поговорити.
Отже, що ми маємо мати на увазі при виборі акумулятора:
тип: кислотні, AGM, гелеві, LiFePo4 – вибір великий, і важливий
ємність: як правильно розрахувати наскільки вам вистачить акумулятору
швидкість заряду: як вибрати знов-таки тип акумулятору і ДБЖ для нього
Типи акумуляторів
Окрім описаних нижче також є звичайні свинцево-кислотні акумулятори (тягові або стартові – для старту двигуна авто), які також називають автомобільними. Можуть виділяти водень і кисень, а тому пожежонебезпечні, і для дому їх точно використовувати не варто.
Основні характеристики акумуляторів:
кількість циклів заряду-розряду: чим більше виконується циклів перезарядки – тим більше деградує (втрачає ємність батарея)
рівень саморозряду: як швидко акумулятор розряджається без підключених приладів
стійкість до глибокого розряду: наскільки деградує батарея при розряді до мінімальних значень
стійкість до перезаряду:
здатність акумулятора не перегріватися при надмірній зарядці, що впливає на його перегрів і може призвести до займання або вибуху
рівень деградації батареї під час зарядки високим струмом, особливо під час завершення зарядки
температурний режим: допустима/комфортна температура навколишнього середовища
чутливість до зарядного пристрою: різні типи акумуляторів мають різні характеристики процесу заряджання, і ДБЖ має це враховувати
вартість
“ефект пам’яті“: втрата ємності акумулятора при неповному розряді перед наступною зарядкою
швидкість зарядки: власне, як швидко можна зарядити акумулятор – залежить від максимальної сили струму і типу акумулятора
Absorbent Glass Mat (AGM) акумулятори
Також є свинцево-кислотними, але електроліт всередині знаходиться в абсорбованому стані, тому вони герметичні і їх можна встановлювати в будь-яке положення окрім “догори ногами”.
Характеристики:
кількість циклів заряду-розряду: 300-500
рівень саморозряду: низький (1-3% на місяць)
стійкість до глибокого розряду: середня
стійкість до перезаряду: середня
температурний режим: від -20°C до +50°C (оптимально 20-25°C)
чутливість до зарядного пристрою: середня
вартість: середня
“ефект пам’яті“: відсутній
швидкість зарядки: середня (можна заряджати до 20% від ємності на годину)
Переваги:
велика кількість циклів перезаряджання і тривалий термін служби
низький рівень саморозряду
немає “ефекту пам’яті” (можна заряджати у будь-який час, не чекаючи повної розрядки)
швидко заряджаються
Недоліки:
погано переносять перезаряджання (високий струм наприкінці зарядки), тому треба мати відповідний ДБЖ
обмежена кількість циклів заряду-розряду
відносно важкі (бо свинець)
Гелеві та мультигелеві акумулятори
За характеристиками подібні до AGM, але в якості електроліту використовується гель.
Характеристики:
кількість циклів заряду-розряду: 500-800
рівень саморозряду: низький (1-2% на місяць)
стійкість до глибокого розряду: висока
стійкість до перезаряду: висока
температурний режим: від -20°C до +55°C (оптимально 20-25°C)
чутливість до зарядного пристрою: висока
вартість: вище середньої
“ефект пам’яті“: відсутній
швидкість зарядки: низька (можна заряджати 10-15% від ємності на годину)
Переваги:
довго можуть бути розрядженими
пристосовані під циклічний характер роботи з глибоким розрядом
допустимість короткострокових глибоких розрядів
Недоліки:
висока вартість
чутливість до коротких замикань
повільна зарядка
більша чутливість до температур, хоча працюють при температурі від -30 до +50
чутливі до зарядного пристрою
LiFePO4
Нове покоління акумуляторів – літій-залізо-фосфатні. Також герметичні, зокрема використовується в електромобілях.
Характеристики:
кількість циклів заряду-розряду: 2000-5000
рівень саморозряду: дуже низький (менше 1% на місяць)
стійкість до глибокого розряду: дуже висока
стійкість до перезаряду: висока
температурний режим: від -20°C до +60°C (оптимально 15-35°C)
чутливість до зарядного пристрою: висока (потребує спеціального зарядного пристрою або просунутий ДБЖ)
вартість: висока
“ефект пам’яті“: відсутній
швидкість зарядки: дуже висока (можна заряджати до 1C, тобто струм заряду може дорівнювати ємності акумулятора, деякі моделі підтримують ще швидшу зарядку)
Переваги:
велика кількість циклів перезаряджання і тривалий термін служби – набагато більший, ніж у AGM та гелевих акумуляторів
низький рівень саморозряду
широкий діапазон робочих температур (від -15 до +60°C)
не бояться великих струмів (можна швидше заряджати)
висока швидкість заряджання – і за рахунок можливості подачі більш високого струму, ніж у AGM та гелевих акумуляторів, і за рахунок самої технології
Недоліки:
висока вартість в порівнянні з AGM та гелевими акумуляторами
потребують спеціальної системи управління зарядкою
Інші типи літій-іонних акумуляторів
Окрім LiFePO4 є і інші типи літій-іонних акумуляторів:
Li-ion (літій-кобальт оксид, LiCoO2): найпоширеніший тип, використовується в смартфонах, ноутбуках
LiMn2O4 (літій-марганець оксид): використовується в електроінструментах, медичному обладнанні
NMC (літій-нікель-марганець-кобальт): поширений в електромобілях та портативній електроніці
NCA (літій-нікель-кобальт-алюміній): використовується в електромобілях Tesla та деяких портативних пристроях.
Висновки
Свинцево-кислотні AGM: надійні, доступна ціна, хороший вибір для більшості домашніх систем
Гелеві акумулятори: підходять там, де необхідна глибока розрядка і довготривала робота без підзарядки, підходять для систем з частими і довгими відключеннями електроенергії
LiFePO4: все “най-” – найдовший термін служби, найшвидша зарядка, найкраща безпечність
Рекомендації по експлуатації акумуляторів
правильна зарядка:
використовуйте зарядні пристрої, призначені для конкретного типу акумулятора
уникайте перезаряду, особливо для свинцево-кислотних акумуляторів
для літій-іонних акумуляторів підтримуйте рівень заряду між 20% і 80%
температурний режим: зберігайте і експлуатуйте акумулятори в рекомендованому температурному діапазоні, особливо, якщо акумулятори десь на балконі, який влітку нагрівається від сонця
глибина розряду:
для свинцево-кислотних акумуляторів уникайте глибокого розряду (нижче 50%)
LiFePO4 акумулятори краще переносять глибокий розряд, але краще не розряджати нижче 20%
регулярне використання: періодично використовуйте акумулятори, не залишайте їх повністю зарядженими або розрядженими на тривалий час
зберігання: при тривалому зберіганні підтримуйте частковий заряд (40-60%)
Рівень розряду акумулятора та його ємність
Важливий момент, котрий треба враховувати при розрахунках: розряджати акумулятор бажано максимум до 30-40% його ємності, хоча це дуже залежить від типу – гелеві цього не люблять, AGM трохи краще переживає глибокий розряд, а LiFePO4 краще інших.
Давайте порахуємо скільки реальної (або корисної) ємності буде в акумуляторі.
Наприклад, маємо AGM 12 вольт на 100 ампер/годин – отримуємо 1200 ват/годин повної ємності.
Від цих 1200 віднімаємо хоча б 30% заряду, який потрібно залишати, аби батареї в акумуляторі почувались краще – і вже маємо 800 ват/годин.
І додатково від цих 800 Вт/г віднімаємо ще втрати на роботу інвертора – це відсотків 15, і в залишку корисної або реальної ємності вже маємо 680 ват/годин.
За цим треба або слідкувати самому (якщо використовуємо схему з окремим інвертором+зарядне+акумулятор), або сам ДБЖ має відключатись автоматично при низькому заряді батареї (і буде добре, якщо ДБЖ досить інтелектуальний, аби враховувати тип акумулятору).
Зарядка акумуляторів та важливість вибору правильного ДБЖ
Отже, від того, який саме у вас тип акумулятора дуже сильно залежить те, як він має заряджатись.
Ще один важливий нюанс при виборі акумулятору та ДБЖ – це скільки часу буде займати зарядка.
Час заряду залежить від:
типу акумулятора: різні типи мають різний рівень швидкості зарядки, і у LiFePo4 найшвидша зарядка
максимальний/рекомендований струм заряду:
свинцево-кислотні AGM, мультигель та гель мають рекомендований рівень в 0.1С (1/10 ємності), тобто акумулятор в 100 ампер-годин можна заряджати максимум на 10 амперах, в такому випадку повний цикл зарядки займе 10 годин
LiFePo4 можна заряджати на струмі 0.5С – 1С, тобто акумулятор на 100 ампер-годин можна заряджати струмом від 50 ампер, і повний цикл зарядки займе 2 години
При перевищені струму заряду батарея може перегрітись і вибухнути (проте вас покажуть по телевізору).
На струм заряду може впливати вбудована плата BMS, про яку згадував вище, бо вона може мати власні обмеження. Втім, не варто покладатись тільки на неї (тим більш, її може і не бути), а читати документацію до ДБЖ та акумулятора.
При такому підключенні всі плюси та всі мінуси батарей підключаються до плюса на ДБЖ. В такому випадку загальна ємність сумується, але напруга лишається такою ж самою:
Тобто, отримаємо 200 ампер/годин і 12 вольт.
Дозволяє використовувати акумулятори з різною ємністю, але може призвести до нерівномірного розряду акумуляторів.
Послідовне підключення
Позначається на акумуляторах як S (serial).
При такому підключенні мінус одного акумулятора підключається до плюса наступного. В результаті ємність залишається такою ж самою, але їхня напруга сумується:
Тобто, отримаємо 100 ампер/годин і 24 вольт.
Вимагає точного підбору акумуляторів за ємністю та станом.
12 вольт vs 24 вольти: а для чого?
Схема підключення залежить від того, який ДБЖ у вас використовується, бо ДБЖ на 12 вольт не видасть високої потужності. 1000-2000 ват потужності для таких ДБЖ, мабуть, максимум.
Наприклад, у нас є кавоварка, яка працює з напругою 220 вольт і споживає 1000 ват/годин. Маємо інвертор, до якого підключено акумулятор на 12 вольт.
В такому випадку аби забезпечити необхідну потужність у 1000 ват від інвертора до кавоварки нам потрібна сила струму у 1000W/220V=4.54 ампери. Але сила струму від акумулятора до інвертора вже буде 1000W/12V, тобто 83 ампери. Відповідно, великі втрати на передачу енергії, і потрібні більш товсті кабелі. А при використанні інвертора на 24 вольти – це було б 41.6 ампери.
Це дуже грубий приклад, але основна ідея така.
То що в результаті? І трохи про розетки.
Отже, якщо повернутись до початку цього посту:
у нас є 4500 ват-годин на добу споживання
ми хочемо зарядити акумулятори за умовні 2 години
Чому 2 години? Бо, по-перше, говорять, що цією зимою в найгіршому варіанті світло буде 4 години на добу, але давайте ми будемо песимістами х2, і припускати, що світло буде лише 2 години на добу.
По-друге – EcoFlow свої 2 кіловат/години заряджає менш ніж за 2 години, і хочеться побудувати щось хоча б приблизно таке ж саме.
Що ми можемо зробити, аби забезпечити себе хоча б на добу?
Варіант 1 – купити пару EcoFlow. Дорого, але надійно.
Варіант 2 – купувати ДБЖ та акумулятори. З урахування специфіки різних типів АКБ, якщо ми хочемо заряджати швидко – то нам потрібні LiFePo4.
4500 ват-годин запасу – це 375 ампер/годин при 12 вольтах – тобто, 2 акуми по 200 ампер-годин. І це якщо рахувати тільки повну ємність – без втрат на перетворення і “залишковий запас” акумулятора, аби не розряджати його повністю.
А аби їх зарядити за 2 години – нам потрібен ДБЖ/зарядне, яке буде видавати 100 ампер! І таке зарядне потрібне на кожен акумулятор окремо!
Таке, звісно, реалізувати можна, але я не зустрічав таких зарядних пристроїв. До того ж, аби витримати таку силу струму будуть потрібні ну прям дуже товсті кабелі.
Тому єдине, що можна зробити – це підключити акумулятори послідовно, аби мати напругу у 24 вольти (в EcoFlow, наприклад, батареї працюють на 48 вольтах – як раз для того, аби знизити необхідну силу струму).
А маючи 24 вольти – ми можемо заряджати їх при струмі в 50 ампер, що вже більш реально, і такі зарядні пристрої знайти можна.
Тоді будемо мати 50 ампер * 24 вольти == 1200 ват потужності, а повна зарядка акумуляторів займе ~4 години. А насправді навіть більше, бо насправді “профіль зарядки” виглядає не рівномірно. Див. ось цей момент у відео – там людина малює графіки заряду.
В такому випадку, якщо ми беремо собі ліміт в 50 ампер – то варіанти будуть такі:
зарядне на 50 ампер + 2 акумулятори по 200 А/г послідовно: отримуємо ~2000 ват/годин, зарядка 4 години
зарядне на 50 ампер + 2 акумулятори по 100 А/г послідовно: отримуємо ~1000 ват/годин, зарядка 2 години
І аби забезпечити свої 4500 ват/годин запасу на добу, нам потрібно або два комплекти з двома акумуляторами по 200 А/г – які будуть заряджатись 4 години, або 4 комплекти з двома акумуляторами по 100 А/г – які будуть заряджатись за 2 дві години.
Але навіть якщо ми підемо на зборку 4-х комплектів – нам їх потрібно заряджати одночасно!
А маючи 1200 ват потужності через розетку на зарядку одного такого блоку батарей в розетці будемо мати 1200/220 = 5.4 ампер, що в принципі нормально, бо стандартно розетки розраховані на 16 ампер максимум – і це треба мати на увазі.
Тобто в одну розетку ми можемо максимум одночасно включити 2 комплекти – сила струму через розетку буде близько 10 ампер.
А включати одночасно 2 EcoFlow в один блок розеток або навіть в одній кімнаті все ж не варто: один EcoFlow при зарядці споживає до 2000 ват, тобто 9 ампер з розетки, до якої від підключений.
Це грубі розрахунки, але плюс-мінус виходить так.
Підсумки і варіанти
А тепер давайте порахуємо вартість варіантів на ~2000 Вт/годину (але пам’ятаємо, що ми нарахували необхідний добовий запас в 4500 Вт/г).
Строк служби далі – доволі умовна одиниця, бо батарея не вмре повністю, а просто втратить частину своєї ємності.
EcoFlow на 2000 Вт/годин з Li-ion батареями
Найкращій варіант з усіх точок зору, окрім ціни:
ціна: ~60.000 гривень
строк служби: враховуючи заявлені 800 циклів заряду, при щоденних відключеннях електрики пропрацює ~2-3 роки
плюси:
просто працює
компактні, можна перенести
швидко заряджається
мінуси: ціна
Якщо ж брати відразу з LiFePo4 батареями (нагадаю – близько 100.000 гривень на сьогодні) – то це гарантовано років 5 і більше роботи.
Або за 105.000 взагалі відразу взяти EcoFlow DELTA Pro на 3600 Вт/годин з тими ж LiFePo4.
Ну і не EcoFlow єдиним – подібних рішень багато. Просто в мене вона є, тому пишу про неї.
ДБЖ з зовнішнім LiFePo4 акумулятором на 200 ампер/годин (~2000 Вт/годин)
Непоганий варіант, але не транспортабельний і за ціною може вийти не набагато дешевше за EcoFlow. Втім, прослужить скоріш за все довше за EcoFlow з Li-ion:
ціна:
ДБЖ: 10-15 тисяч гривень
акумулятор: від 35 до 50 тисяч
строк служби: враховуючи ~3000 циклів заряду, при щоденних відключеннях електрики – пропрацює ~4-5 років
плюси:
трохи дешевший за EcoFlow
при правильно підібраному зарядному пристрої – швидко заряджається
достатньо надійно і безпечно
мінуси:
швидкість заряду: залежить від зарядного, але навряд чи вам вдасться “залити” його повністю за 2-3 години, навіть використовуючи LiFePo4 (або брати менші за ємністю акумулятори в більшій кількості – а це вплине на загальну вартість системи)
не можна просто так перенести в інше приміщення
потребує додаткових знань, додаткового обслуговування, моніторингу, обережності
Можна замість LiFePo4 взяти AGM-акумулятори – вийде в пару раз дешевше, але вони і заряджатись будуть в кілька раз довше, і строк служби буде в кілька раз меншим.
Найбільш небезпечний, проте самий бюджетний варіант:
ціна: від 20-25 тисяч гривень
строк служби: на 1-2 роки можна розраховувати, але навряд чи більше (скоріш менше, і тут вже саме про те, скілька така станція пропрацює взагалі, а не втрата ємності батареї)
плюси:
компактні, можна перенести
швидко заряджається
мінуси:
надійність – може зламатись в будь-який момент
безпека і пожежонебезпечність: відносно до EcoFlow або ДБЖ з LiFePo4 акумулятором доволі (дуже?) небезпечне рішення
шумні
В цілому, це, мабуть, все.
І капелька про павербанки
Ну і павербанки ніхто не відміняв.
Це мій запас, який зробив ще в кінці минулого року:
Тут:
дві зарядні станції Kseni по 160.000 mAh (до них інвертор на 500 ват)
два павербанка FutureSolar по 160.000 mAh (до них два інвертори на 150 ват кожен – телевізор від них працює без проблем)
один павербанк ChinaNoName на 60.000 mAh
два павербанка ChinaNoName по 50.000 mAh
два павербанка Baseus по 30.000 mAh
два павербанка Xiomi по 20.000 mAh
Живити через інвертор від павербанок всякі ноутбуки/монітори/світильники можна спокійно, а от холодильник і котел – вже тільки від ДБЖ/EcoFlow. Втім тут ще пам’ятаємо, що сам інвертор буде з’їдати частину енергії (хоча ноутбук можна напряму, якщо потужність павербанка дозволяє).
Як рахувати ємність павербанка?
Тут вже зовсім коротенько.
Ємність на всіх банках вказується з розрахунком на 3.7 вольти, але по факту на виході більшість видає 5 вольт. Якщо через автомобільний “прикурювач” – то там 12 вольт:
Формула:
мА/г * Вольти / 1000
Тепер рахуємо:
павербанк з 60.000 номінальної ємності при 3.7 вольти – це 60.000 (міліампер/годин) * 3.7 вольти == 222 ват-години
той же павербанк при 5 вольтах на виході – це вже 222 (ват-години) / 5 (вольт) == 44.400 mAh
а він жеж, живлячи інвертор на 12 вольтах – 18.500 mAh
Готуємось мігрувати базу даних нашого Backend API з DynamoDB до AWS RDS з PostgreSQL, і нарешті вирішив спробувати що ж таке AWS RDS IAM database authentication, який з’явився здається ще десь у 2021.
IAM database authentication, як, в принципі, можна здогадатись з назви, дозволяє нам виконувати аутентифікацію в RDS за допомогою AWS IAM, а не логіна-пароля з самого сервера баз даних.
Втім, авторизація – тобто перевірка які саме доступи є у юзера в базі/базах, залишаються за самим сервером БД, бо IAM нам тільки дасть доступ до самого інстансу RDS.
Тож що будемо робити:
спочатку руками спробуємо як працює RDS IAM database authentication, і як вона конфігуриться
потім перейдемо до автоматизації з Terraform, і заодно поглянемо на те, як працює AWS EKS Pod Identities
напишемо код на Python, який буде запускатись в Kubernetes Pod з SericeAccount та підключатись до RDS використовуючи RDS IAM database authentication
поговоримо про проблеми при використанні RDS IAM database authentication та автоматизації з Terraform
Тестую на RDS, який створюється для Grafana, тому подекуди будуть імена з “monitoring“/”grafana“.
Загальна ідея – замість паролю до RDS використовується IAM-токен для IAM Role або IAM User, до яких підключено IAM Policy, яка описує ID Aurora-кластеру або RDS-інстансу та ім’я користувача.
Але, нажаль, на цьому роль IAM завершується, бо доступи та права в самому сервері баз даних створюються і керуються як і раніше, тобто через CREATE USER та GRANT PERMISSIONS.
IAM database authentication та Kubernetes ServiceAccount
Відносно Kubernetes Pod я, чесно кажучи, очікував трохи більшого, бо мені здавалось, що просто використовуючи IAM Role та Kubernetes ServiceAccount можна буде взагалі без паролю підключатись до RDS – як ми це робимо з доступом до інших ресурсів в AWS через AWS API.
Але з RDS схема виглядає трохи інакше:
створюємо інстанс RDS з параметром IAM authentication == true
створюємо IAM Role з IAM Policy
в PostgreSQL/MariaDB створюємо відповідного користувача, включаємо йому аутентифікацію через IAM
в Kubernetes створюємо ServiceAccount з цією роллю
підключаємо цей ServiceAccount до Kubernetes Pod
в Pod, використовуючи IAM Role з ServiceAccount, генеруємо IAM RDS Token для доступу для RDS
і вже з цим токеном підключаємось до серверу RDS
Давайте спочатку спробуємо руками, а потім глянемо, як це зробити з Terraform – бо там є свої нюанси.
RDS IAM authentication: перевірка
Отже, маємо вже створений RDS PostgreSQL з Password and IAM database authentication:
Для серверу вже маємо дефолтного master-юзера та пароль в SecretsManager – він знадобиться для додавання нового користувача.
Знаходимо ID інстансу – буде потрібен в IAM Policy:
Створення IAM Policy
Далі нам потрібна IAM Policy, яка буде дозволяти доступ юзеру до цього інстансу RDS.
Тут ми Allow виконати дію rds-db:connect до сервера бази даних в Resource використовуючи ім’я користувача db_test, і цього ж користувача db_test ми потім додамо з CREATE USER на самому сервері БД.
Зверніть увагу, що інстанс RDS вказується не як його ім’я – а саме як його ID – db-XXXYYYZZZ.
Зберігаємо політику:
Політику можемо підключити напряму до свого юзера AWS, або використати IAM Role.
З роллю спробуємо пізніше, коли будемо підключати Kubernetes Pod, а зараз для перевірки схеми в цілому давайте використаємо звичайного IAM User.
Знаходимо необхідного IAM User та додаємо пермішени:
Вибираємо Attach policies directly, знаходимо нашу IAM Policy:
$ psql "host=$RDSHOST sslmode=require dbname=ops_grafana_db user=db_test password=$PGPASSWORD"
psql: error: connection to server at "ops-monitoring-rds.***.us-east-1.rds.amazonaws.com" (10.0.66.79), port 5432 failed: FATAL: PAM authentication failed for user "db_test"
FATAL: PAM authentication failed for user “db_test”
В моєму випадку помилка виникла, бо я спершу генерив токен з “--region us-west-2“, а сервер знаходиться в us-east-1 (привіт, copy-paste з документації 🙂 ).
Тобто помилка виникає саме через помилки в налаштуваннях доступу – або в IAM Policy вказано інший username, або при CREATE USER вказано інше ім’я, або токен згенеровано для іншої IAM-ролі.
Перегенеримо токен, пробуємо ще раз:
$ psql "host=$RDSHOST sslmode=require dbname=ops_grafana_db user=db_test password=$PGPASSWORD"
psql (16.2, server 16.3)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.
ops_grafana_db=>
ops_grafana_db=> \dt
List of relations
Schema | Name | Type | Owner
--------+-----------------------------+-------+------------------
public | alert | table | ops_grafana_user
public | alert_configuration | table | ops_grafana_user
public | alert_configuration_history | table | ops_grafana_user
...
При чому password=$PGPASSWORD можна не вказувати – psql сам зчитає змінну PGPASSWORD, див. Environment Variables.
dbname=ops_grafana_db тут – бо сервер створюється для Grafana, і це її база.
Окей – це перевірили, працює.
Тепер час Kubernetes та автоматизації з Terraform – і тут наші пригоди тільки починаються.
Terraform та AWS EKS Pod Identity з IAM database authentication
Давайте глянемо, як ця схема буде працювати з Kubernetes Pod та ServiceAccount.
в Trusted Policy цієї IAM Role будемо мати pods.eks.amazonaws.com
роль додамо до кластеру через EKS IAM API
створимо Kubernetes Pod та ServiceAccount
в поді запустимо код на Python, який буде підключатись до RDS
Тобто, Kubernetes Pod для аутентифікації в AWS API буде використовувати IAM Role з Kubernetes ServiceAccount, потім, використовуючи цю роль, від AWS API отримає AWS RDS Token, і вже з цим токеном підключиться до RDS.
Створення AWS EKS Pod Identity з Terraform
Для AWS EKS Pod Identity є модуль eks-pod-identity, візьмемо його.
В Terraform описуємо aws_iam_policy_document з доступом до RDS:
Policy нова, і юзера використаємо теж нового – test_user.
В ${data.aws_caller_identity.current.account_id} ми маємо AWS Account ID:
data "aws_caller_identity" "current" {}
В ${module.monitoring_rds.db_instance_resource_id} – ID нашого RDS-інстансу, який створюється за допомогою модулю terraform-aws-modules/rds/aws з параметром iam_database_authentication_enabled:
module "monitoring_rds" {
source = "terraform-aws-modules/rds/aws"
version = "~> 6.7.0"
identifier = "${var.environment}-monitoring-rds"
...
# DBName must begin with a letter and contain only alphanumeric characters
db_name = "${var.environment}_grafana_db"
username = "${var.environment}_grafana_user"
port = 5432
manage_master_user_password = true
manage_master_user_password_rotation = false
iam_database_authentication_enabled = true
...
}
Далі з terraform-aws-modules/eks-pod-identity/aws описуємо EKS Pod Identity Association, де використовуємо aws_iam_policy_document.monitoring_rds_policy, який зробили вище:
# get info about a cluster
data "aws_eks_cluster" "eks" {
name = local.eks_name
}
Деплоїмо, і перевіряємо IAM:
Та Pod Identity associations в AWS EKS:
Тепер маємо IAM Role, до якої підключена IAM Policy, яка надає доступ юзеру test_user до RDS-інстансу з ID db-UZM***3SA, та маємо встановлений зв’язок між ServiceAccount з іменем eks-test-sa в Kubernetes-кластері та цією IAM-роллю.
Python, PostgreSQL та IAM database authentication
Що має відбуватись далі:
створимо Kubernetes Pod
створимо ServiceAccount з іменем eks-test-sa
напишемо код на Python, який буде:
використовуючи ServiceAccount та пов’язану з ним IAM Role підключатись до AWS API
отримає AWS RDS Token
з цим токеном підключиться до RDS
Знов заходимо на RDS з мастер-юзером, і створюємо нового користувача test_user (як вказано в IAM Policy) з роллю rds_iam:
ops_grafana_db=> CREATE USER test_user;
CREATE ROLE
ops_grafana_db=> GRANT rds_iam TO test_user;
GRANT ROLE
ops_grafana_db=> GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO test_user;
GRANT
Описуємо ServiceAccount eks-test-sa та Kubernetes Pod з цим ServiceAccount в namespace=ops-monitoring-ns:
$ kk apply -f eks-test-rds-irsa.yaml
serviceaccount/eks-test-sa created
pod/eks-test-pod created
Підключаємось в под:
$ kk exec -ti eks-test-pod -- bash
root@eks-test-pod:/#
Встановлюємо python-boto3 для отримання токену та python3-psycopg2 для роботи з PostgreSQL:
root@eks-test-pod:/# apt update && apt -y install vim python3-boto3
Пишемо код:
#!/usr/bin/python3
import boto3
import psycopg2
DB_HOST="ops-monitoring-rds.***.us-east-1.rds.amazonaws.com"
DB_USER="test_user"
DB_REGION="us-east-1"
DB_NAME="ops_grafana_db"
client = boto3.client('rds')
# using Kubernetes Pod ServiceAccount's IAM Role generate another AWS IAM Token to access RDS
db_token = client.generate_db_auth_token(DBHostname=DB_HOST, Port=5432, DBUsername=DB_USER, Region=DB_REGION)
# connect to RDS using the token as a password
conn = psycopg2.connect(database=DB_NAME,
host=DB_HOST,
user=DB_USER,
password=db_token,
port="5432")
cursor = conn.cursor()
cursor.execute("SELECT * FROM dashboard_provisioning")
print(cursor.fetchone())
conn.close()
В принципі він досить простий – підключаємось до AWS, отримуємо токен, підключаємось до RDS.
ServiceAccount через Pod Identity associations пов’язаний з IAM Role ops-monitoring-rds-role
IAM Role ops-monitoring-rds-role має IAM Policy з Allow на rds-db:connect
Kubernetes Pod використовує IAM Role з ServiceAccount для аутентифікації та авторизації в AWS
після чого в Python з boto3 та client.generate_db_auth_token отримує RDS Token
і використовує його для підключення для PostgreSQL
На самому RDS ми вже маємо створеного юзера test_user з rds_iam та доступом до баз даних.
Про те, як саме працює Kubernetes ServiceAccounts та токени на рівні Kubernetes Pod див. в AWS: EKS, OpenID Connect та ServiceAccounts (тільки там описано ще без Pod Identity associations, але механізм той самий).
Виглядає, як робочий варіант – але залишився ще один нюанс.
Terraform та IAM RDS Authentication: складнощі
Вся наша схема з Terraform наче робоча – але ми руками створювали test_user та давали йому пермішени.
І тут ще один недолік схеми RDS та IAM database authentication – бо нам все одно потрібно створювати юзера в сервері БД.
А звідси випливає ще одна проблема – як це робити з Terraform?
Я не став вже витрачати на це час, бо нам в принципі це не дуже актуально, бо буде лише кілька юзерів і їх можна зробити руками, а поточну автоматизацію це не блокує.
Але з часом, коли проект виросте, то це питання все одно доведеться вирішувати.
Отже, які проблеми і варіанти вирішення маємо:
ми можемо створити PostgreSQL (чи MariaDB) юзерів прямо з коду Terraform використовуючи PostgreSQL provider, і виконавши local-exec або використавши resource "postgresql_grant"
але для цього потрібен доступ до самого RDS, який знаходиться в приватній мережі VPC, а тому для CI/CD потрібен мережевий доступ до VPC, що, в принципі, можливо, якщо запускати GitHub Runners (в нашому випадку) в Kubernetes, чиї WorkerNodes мають доступ до приватних сабнетів – але зараз ми використовуємо Runners самого GitHub, і у них цього доступу нема
другий варіант – використовувати AWS Lambda, яка буде запускатись в VPC з доступом до RDS, і створювати юзерів
виглядає цілком робочим варіантом, крім того замість AWS Lambda ми можемо з Terraform описати запуск Kubernetes Pod, який буде виконувати необхідні дії – підключення до RDS та CREATE USER
Обидів схеми цілком робочі, і колись, можливо, опишу реалізацію одного з них (скоріш за все другого).
Але на даний момент не бачу сенсу витрачати на це час.
Висновки
А висновки насправді трохи неоднозначні.
Сама ідея з RDS IAM database authentication виглядає дуже цікаво, але той факт, що токен для RDS і звичайний токен аутентифікації в AWS API для IAM Role являють собою різні сутності трохи ускладнює реалізацію, бо якби до RDS можна було конектитись просто використовуючи ServiceAccount та IAM Role – це дуже спростило б використання.
Крім того, чомусь очікував, що й авторизація буде робитись на рівні IAM – тобто, прямо в IAM Policy можна буде вказати хоча б бази даних, до яких будуть доступи. Але це залишається на рівні сервера БД.
Друга проблема полягає в тому, що нам все одно доводиться створювати юзера в RDS і видавати йому права, що знов-таки створює додаткові складнощі в автоматизації.
Втім, в цілому свою задачу RDS IAM database authentication виконує – нам дійсно не потрібно створювати якийсь Kubernetes Secret з паролем для бази даних і маунтити його до Kubernetes Pod, а достатньо підключити ServiceAccount, а отримання токену ми вже “перекладаємо на плечі девелоперів” – тобто виконуємо на рівні коду, а не Kubernetes.
І, думаю, ми все ж будемо цей механізм використовувати у нас в Production.
Маємо API-сервіс в Kubernetes, який періодично видає 502, 503, 504 помилки.
Почав його дебажити, і виявив дивну штуку – в логах не було повідомлень про отриманий SIGTERM, а тому спочатку пішов розбиратись з Kubernetes – чому він його не відправляє?
The Issue
Отже, як це виглядає.
Маємо Kubernetes Pod:
$ kk get pod
NAME READY STATUS RESTARTS AGE
fastapi-app-89d8c77bc-8qwl7 1/1 Running 0 38m
Читаємо його логи:
$ ktail fastapi-app-59554cddc5-lgj42
==> Attached to container [fastapi-app-59554cddc5-lgj42:fastapi-app]
Вбиваємо його:
$ kk delete pod -l app=fastapi-app
pod "fastapi-app-6cb6b46c4b-pffs2" deleted
І бачимо, що у нас тут PID 1 – це процес /bin/sh, який через -c запускає gunicorn.
Тепер давайте в поді запустимо strace, і подивимось які сигнали він отримує:
root@fastapi-app-6cb6b46c4b-9pd7r:/app# strace -p 1
strace: Process 1 attached
wait4(-1,
Виконуємо kubect delete pod – але додамо time, аби заміряти час на виконання команди:
$ time kk delete pod fastapi-app-6cb6b46c4b-9pd7r
pod "fastapi-app-6cb6b46c4b-9pd7r" deleted
real 0m32.222s
32 секунди…
А що в strace?
root@fastapi-app-6cb6b46c4b-9pd7r:/app# strace -p 1
strace: Process 1 attached
wait4(-1, 0x7ffe7a390a3c, 0, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} ---
wait4(-1, <unfinished ...>) = ?
command terminated with exit code 137
Отже, що тут відбувалось:
kubelet відправив сигнал SIGTERM процесу з PID 1 – SIGTERM {si_signo=SIGTERM} – і PID 1 мав би передати цей процес своїм дочірнім процесам, зупинити їх, а потім завершитись сам
але процес не завершився – і kubelet почекав дефолтні 30 секунд, аби под коректно завершив свою роботу – див. Pod phase
після чого вбив контейнер, і процес завершився з “terminated with exit code 137“
Зазвичай код 137 – це про OutOfMemory Killer, коли процес вбивається з SIGKILL, але в нашому випадку OOMKill не було – а просто відбувся SIGKILL через те, що процеси в поді не завершились вчасно.
Добре – а куди подівся наш SIGTERM?
Давайте виконаємо сигнали напряму з контейнеру – спочатку kill -s 15, SIGTERM, потім kill -s 9, SIGKILL:
root@fastapi-app-6cb6b46c4b-r9fnq:/app# kill -s 15 1
root@fastapi-app-6cb6b46c4b-r9fnq:/app# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2576 920 ? Ss 12:02 0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
root 7 0.0 1.4 31852 27644 ? S 12:02 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
...
root@fastapi-app-6cb6b46c4b-r9fnq:/app# kill -s 9 1
root@fastapi-app-6cb6b46c4b-r9fnq:/app# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2576 920 ? Ss 12:02 0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
root 7 0.0 1.4 31852 27644 ? S 12:02 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
Шо? Як?
Чому ігнорується SIGTERM? А тим більш SIGKILL, який має бути “non ignorant signal” – див. man signal:
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
Linux kill() та PID 1
Бо PID 1 в Linux є особливим процесом, бо це перший процес, який запускається системою, і він має бути “захищений” від “випадкового вбивства”.
Якщо ми глянемо на man kill, то там це явно вказано, а також сказано про хендлери сигналів до процесу:
The only signals that can be sent to process ID 1, the init process, are those for which init has explicitly installed signal handlers. This is done to assure the system is not brought down accidentally
Перевірити які хендлери є у нашого процесу – тобто, які сигнали наш процес може перехопити і обробити – можна з файлу /proc/1/status:
SigCgt – це сигнали, які процес може перехопити і обробити сам. Решта будуть або проігноровані, або опрацьовані з SIG_DFL handler, а SIG_DFL handler ігнорує сигнали для PID 1, у якого нема власного хендлера.
перевірка PID 1 вказує нам, що він “розпізнає” тільки сигнали SIGHUP і SIGCHLD
SIGTERM та SIGKILL будуть ним проігноровані
Але як жеж тоді зупиняється контейнер?
Docker stop та сигнали
Процес зупинки контейнера в Docker (або Containerd) не відрізняється від того, як це в Kubernetes, бо по факту kubelet просто передає команди на container runtime. В AWS Kubernetes це зараз containerd.
Але задля простоти давайте виконаємо це локально з Docker.
Отже, запускаємо контейнер з того самого образу, який тестили в Kubernetes:
$ docker run --name test-app 492***148.dkr.ecr.us-east-1.amazonaws.com/fastapi-app-test:entry-2
[2024-06-22 14:15:03 +0000] [7] [INFO] Starting gunicorn 22.0.0
[2024-06-22 14:15:03 +0000] [7] [INFO] Listening at: http://0.0.0.0:80 (7)
[2024-06-22 14:15:03 +0000] [7] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2024-06-22 14:15:03 +0000] [8] [INFO] Booting worker with pid: 8
[2024-06-22 14:15:03 +0000] [8] [INFO] Started server process [8]
[2024-06-22 14:15:03 +0000] [8] [INFO] Waiting for application startup.
[2024-06-22 14:15:03 +0000] [8] [INFO] Application startup complete.
Пробуємо його зупинити через відправку SIGKILL до PID 1 – нічого не змінилося, він ігнорує сигнал:
$ docker exec -ti test-app sh -c "kill -9 1"
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
99bae6d55be2 492***148.dkr.ecr.us-east-1.amazonaws.com/fastapi-app-test:entry-2 "/bin/sh -c 'gunicor…" About a minute ago Up About a minute test-app
Пробуємо зупинити з docker stop – і знов дивимось на час:
$ time docker stop test-app
test-app
real 0m10.234s
І статус контейнера:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cab29916f6ba 492***148.dkr.ecr.us-east-1.amazonaws.com/fastapi-app-test:entry-2 "/bin/sh -c 'gunicor…" About a minute ago Exited (137) 52 seconds ago
Той жеж код 137, тобто контейнер зупинився з SIGKILL.
Але як, якщо сигнал відправляється до PID 1, котрий його ігнорує?
Я не знайшов цього в документації до docker kill, але ми можемо вбити процеси контейнеру двома шляхами:
вбити всі дочірні процеси в самому контейнері – і тоді parent (PID 1) вмре сам
вбити всю групу процесів на хості через їхній SID (Session ID) – що знову-таки призведе до того, що PID 1 проігнорує сигнал, але вмре сам, бо померли всі його дочірні процеси
Вбити PID 1 ми не можемо, бо він нас ігнорує – але ми можемо вбити PID 7!
Тоді він за собою вб’є PID 8, бо це його дочірній процес, а коли PID 1 виявить, що більше нема його дочірніх процесів – то він вмре сам, і контейнер зупиниться:
root@cddcaa561e1d:/app# kill 7
І логи контейнеру:
...
[2024-06-22 16:02:54 +0000] [7] [INFO] Handling signal: term
[2024-06-22 16:02:54 +0000] [8] [INFO] Shutting down
[2024-06-22 16:02:54 +0000] [8] [INFO] Error while closing socket [Errno 9] Bad file descriptor
[2024-06-22 16:02:54 +0000] [8] [INFO] Waiting for application shutdown.
[2024-06-22 16:02:54 +0000] [8] [INFO] Application shutdown complete.
[2024-06-22 16:02:54 +0000] [8] [INFO] Finished server process [8]
[2024-06-22 16:02:54 +0000] [7] [ERROR] Worker (pid:8) was sent SIGTERM!
[2024-06-22 16:02:54 +0000] [7] [INFO] Shutting down: Master
Але так як поди/контейнери вмирають з кодом 137, то вони були вбиті з SIGKILL, бо коли Docker або інший container runtime не може зупинити процес з PID 1 з SIGKILL – то він відправляє SIGKILL всім процесам в контейнері.
Тобто:
спочатку відправляється SIGTEM до PID 1
через 10 секунд відправляється SIGKILL до PID 1
якщо і це не допомогло – то SIGKILL відправляється всім процесам в контейнері
Наприклад, це можна зробити передавши Session ID (SID) команді kill.
В Kubernetes окрім метрик та логів з контейнерів ми маємо змогу отримувати інформацію про роботу компонентів за допомогою Kubernetes Events.
В евентах зазвичай зберігається інформація про статус подів (створення, Evict, kill, ready або not-ready статус подів), про WorkerNodes (статус серверів), про роботу Kubernetes Scheduler (неможливість запуску поду, тощо).
Типи Kubernetes Events
В цілому, всі ці евенти можна поділити на такі типи:
Failed events: коли виникає проблема з маніфестом або образом, з якого потрібно створити контейнер (ImagePullBackOff, CrashLoopBackOff)
Scheduler events: проблеми с запуском поду на WorkerNode, наприклад – коли Scheduler не може знайти ноду з достатніми ресурсами, щоб задовольнити Pod requests
Volume events: проблеми з підключенням PersistentVolume до поду (FailedAttachVolume, FailedMount)
Node events: проблеми в роботі WorkerNodes (NodeNotReady)
Kubernetes Events та kubectl
Отримати евенти можемо просто з kubectl – або зробивши kubectl describe pod <POD_NAME> чи kubectl decsribe deploy <DEPLOY_NAME>:
Або з kubectl get events, якому можна додати параметр --watch:
Також існує цікавий плагін podevents, який додає час евенту.
sloop – активний, система візуалізації евентів з можливістю фільтрації та пошуку
kspan – активний, створює OpenTelemetry Spans з евентів, які потім можна перевіряти в системах типу Jaeger
kubernetes-event-exporter – активний, вміє відправляти евенти, мабуть, в усе, що взагалі існує – і AWS SQS/SNS, і Opsgenie, і Slack, і Loki – можливо, він буде наступний в моєму кластері, коли поточне рішення стане недостатнім
Grafana Agent (Grafana Alloy) – теж вміє працювати з евентами, і писати їх у вигляді логів в Loki
Але, як на мене, то простіше всього їх мати у вигляді логів, і потім з Loki RecordingRules створювати метрики, а з них графіки в Grafana та/або алерти.
Для цього є дуже проста система max-rocket-internet/k8s-event-logger, яка слухає Kubernetes API, отримує всі евенти, і записує їх у вигляді лога в JSON.
$ kubectl -n ops-monitoring-ns get pods -l "app.kubernetes.io/name=k8s-event-logger"
NAME READY STATUS RESTARTS AGE
k8s-event-logger-5b548d6cc4-r8wkl 1/1 Running 0 68s
Чого прям ну дуже не вистачає в цій системі – це доступу до серверів по SSH, без якого почуваєшся… Ну, наче DevOps, а не Infrastructure Engineer. Короче – доступ по SSH іноді прям треба, але – сюрпрайз – Karpenter з коробки не дає можливості додати ключ на ноди, які він менеджить.
Хоча, здавалося б – в чому проблема в EC2NodeClass передати ключ, як це робиться в Terraform resource "aws_instance"з параметром key_name?
Але – ОК. Нема, то й нема. Можливо, додадуть пізніше.
Тож що будемо робити сьогодні – по черзі спробуємо всі три рішення, спочатку кожне будемо робити руками, потім дивитись як його додати в нашу автоматизацію, і потім вирішимо який варіант буде найпростішим.
Варіант 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
...
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.
Але після фіксу 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
В модулі версії 20.0 ім’я параметру змінилось – iam_role_additional_policies => node_iam_role_additional_policies, але у нас поки що версія 19.21.0, і роль додається таким чином:
Для Terraform тут буде потрібно в модулі EKS додавати node_security_group_additional_rules для доступу по SSH, і для VPC створювати EC2 Instance Connect Endpoint, бо у нас VPC та EKS створюються окремо.
Втім, тут треба передавати SecurityGroup ID з кластеру, а кластер у нас створюється після VPC, тому виникає проблема “куриця-яйце”.
Взагалі з Instance Connect виглядає якось трохи більш складно, чим з SSM, бо більше змін в коді, ще й в різних модулях.
Втім – варіант робочий, і якщо ваша автоматизація дозволяє – то можна використовувати його.
Варіант 3: дідовський спосіб з SSH Public Key через EC2 User Data
Ну і самий старий і, можливо, простий варіант – це самому створити SSH-ключ, і додавати його публічну частину на EC2 при створені інстансу.
З недоліків тут те, що додавати таким чином багато ключів буде складно, та й взагалі EC2 User Data іноді може вилізти боком, але якщо потрібно додати тільки один ключ, якийсь “супер-адмін” на крайній випадок – то цілком валідний варіант.
$ 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
...
Публічну частину можемо зберігати в репозиторії – копіюємо її:
Далі трохи костилів: 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"
}
AWS SessionManager: виглядає як найпростіший варіант з точку зору автоматизації, рекомендований самим AWS, але треба подумати, як робити той же scp через нього (хоча це начебто можливо через додаткові костилі – див. .SSH and SCP with AWS SSM)
AWS EC2 Instance Connect: прикольна фіча від Амазону, але якось більш геморно в автоматизації, тому не наш варіант
“дідовський” SSH: ну, старе – перевірене 🙂 але я не дуже люблю User Data, бо іноді може призвести до проблем з запуском інстансів; втім – теж простий з точки зору автоматиазції, і дає звичний SSH без додаткових тєлодвіженій