Готуємось мігрувати базу даних нашого 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 database authentication?
Документація – IAM database authentication for MariaDB, MySQL, and PostgreSQL.
Загальна ідея – замість паролю до 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.
Переходимо в IAM > Policy, створюємо нову політику, див. документацію Creating and using an IAM policy for IAM database access:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "rds-db:connect", "Resource": "arn:aws:rds-db:us-east-1:492***148:dbuser:db-UZM***3SA/db_test" } ] }
Тут ми 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:
Наступний крок – додати користувача в RDS.
PostgreSQL: створення database user
Документація – Creating a database account using IAM authentication.
Note: Make sure the specified database user name is the same as a resource in the IAM policy for IAM database access
Тобто при CREATE USER
маємо вказати того самого db_test
, який вказано в "Resource"
нашої IAM Policy :
... "Resource": "arn:aws:rds-db:us-east-1:492***148:dbuser:db-UZM***3SA/db_test" ...
Підключаємось з дефолтним юзером і паролем, які отримали при створенні сервера:
$ psql -h ops-monitoring-rds.***.us-east-1.rds.amazonaws.com -U master_user -d ops_monitoring_db
Створюємо нового користувача db_test
, і підключаємо йому аутентифікацію через роль rds_iam
:
ops_grafana_db=> CREATE USER db_test; CREATE ROLE ops_grafana_db=> GRANT rds_iam TO db_test; GRANT ROLE
Для MariaDB це буде AWSAuthenticationPlugin
.
Підключення з psql
Документація – Connecting to your DB instance using IAM authentication from the command line: AWS CLI and psql client.
Note: You cannot use a custom Route 53 DNS record instead of the DB instance endpoint to generate the authentication token.
Знаходимо URL ендпоінту серверу:
Задаємо змінну з адресою:
$ export RDSHOST="ops-monitoring-rds.***.us-east-1.rds.amazonaws.com"
З AWS CLI та командою aws rds generate-db-auth-token
отримуємо токен – саме він і буде нашим паролем:
$ export PGPASSWORD="$(aws --profile work rds generate-db-auth-token --hostname $RDSHOST --port 5432 --region us-east-1 --username db_test)"
Перевіримо його:
$ echo $PGPASSWORD ops-monitoring-rds.***.us-east-1.rds.amazonaws.com:5432/?Action=connect&DBUser=db_test&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=***%2F20240624%2Fus-east-1%2Frds-db%2Faws4_request&X-Amz-Date=20240624T142442Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Security-Token=IQo***942
Підключаємось:
$ 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.
Детальніше про нову схему роботи з Pod ServiceAccounts та IAM писав в AWS: EKS Pod Identities – заміна IRSA? Спрощуємо менеджмент IAM доступів, але досі в production не використовував – як раз потестуємо як його взагалі готувати.
Отже, що нам потрібно:
- IAM Role з IAM Policy
- в 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:
data "aws_iam_policy_document" "monitoring_rds_policy" { statement { effect = "Allow" actions = [ "rds-db:connect" ] resources = [ "arn:aws:rds-db:us-east-1:${data.aws_caller_identity.current.account_id}:dbuser:${module.monitoring_rds.db_instance_resource_id}/test_user" ] } }
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
, який зробили вище:
module "grafana_pod_identity" { source = "terraform-aws-modules/eks-pod-identity/aws" version = "~> 1.2.1" name = "${var.environment}-monitoring-rds-role" attach_custom_policy = true source_policy_documents = [data.aws_iam_policy_document.monitoring_rds_policy.json] associations = { atlas-eks = { cluster_name = data.aws_eks_cluster.eks.name namespace = "${var.environment}-monitoring-ns" service_account = "eks-test-sa" } } }
В namespace
задаємо ім’я, в якому буде створено ServiceAccount для Pod, а в service_account
– власне ім’я ServiceAccount.
data.aws_eks_cluster.eks.name
отримується з data "aws_eks_cluster"
:
# 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
:
apiVersion: v1 kind: ServiceAccount metadata: name: eks-test-sa namespace: ops-monitoring-ns --- apiVersion: v1 kind: Pod metadata: name: eks-test-pod namespace: ops-monitoring-ns spec: containers: - name: ubuntu image: ubuntu command: ['sleep', '36000'] restartPolicy: Never serviceAccountName: eks-test-sa
Деплоїмо:
$ 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.
Запускаємо, перевіряємо результат:
root@eks-test-pod:/# ./test-rds.py (1, 1, 'default', '/var/lib/grafana/dashboards/default/nodeexporter.json', 1719234200, 'c2ef5344baf3389f5238679cd1b0ca68')
Трохи про те, що саме відбувається “під капотом”:
- Kubernetes Pod має ServiceAccount
- 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"
- див. приклади в AWS RDS IAM Authentication with Terraform та grant privileges and permissions to a role via terrafrom is not working
- але для цього потрібен доступ до самого RDS, який знаходиться в приватній мережі VPC, а тому для CI/CD потрібен мережевий доступ до VPC, що, в принципі, можливо, якщо запускати GitHub Runners (в нашому випадку) в Kubernetes, чиї WorkerNodes мають доступ до приватних сабнетів – але зараз ми використовуємо Runners самого GitHub, і у них цього доступу нема
- другий варіант – використовувати AWS Lambda, яка буде запускатись в VPC з доступом до RDS, і створювати юзерів
- див. приклади в Securing AWS RDS with Fine-Grained Access Control using IAM Authentication, Terraform and Serverless та Automate post-database creation scripts or steps in an Amazon RDS for Oracle database
- виглядає цілком робочим варіантом, крім того замість 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.