AWS: RDS з IAM database authentication, EKS Pod Identities та Terraform

Автор |  27/06/2024

Готуємось мігрувати базу даних нашого 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, і створювати юзерів

Обидів схеми цілком робочі, і колись, можливо, опишу реалізацію одного з них (скоріш за все другого).

Але на даний момент не бачу сенсу витрачати на це час.

Висновки

А висновки насправді трохи неоднозначні.

Сама ідея з 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.