Terraform-модули для PostgreSQL RBAC: роли, grants и пользователи
Published: 2026-02-25
Управление ролями PostgreSQL обычно начинается с пары ручных GRANT-ов, а заканчивается недокументированным хаосом, который никто не решается трогать. Terraform с провайдером cyrilgdn/postgresql превращает его в версионируемый, проверяемый и идемпотентный инфраструктурный код.
Зачем Terraform для доступа к базе
Проблемы ручных grant-ов:
- Нет журнала изменений — непонятно, кто и что добавил
- Нельзя сравнить желаемое состояние с текущим
- Ошибки копирования между окружениями
- Отзыв доступов при увольнении — угадайка
С Terraform: каждый пользователь, каждая роль, каждый grant — в Git. Изменения доступа проходят code review. terraform plan показывает, что именно изменится, до применения.
Настройка провайдера
hcl# providers.tf
terraform {
required_providers {
postgresql = {
source = "cyrilgdn/postgresql"
version = "1.22.0"
}
}
required_version = ">= 0.13"
}
provider "postgresql" {
host = var.db_host
port = var.db_port
database = var.db_database
username = var.db_admin_user
password = var.db_admin_pass
sslmode = var.db_sslmode
connect_timeout = var.db_connect_timeout
}
Учётные данные берутся из terraform.tfvars вне репозитория или из переменных окружения (TF_VAR_db_admin_pass). Никогда не коммитьте пароли.
Модуль: db_role
hcl# modules/db_role/variables.tf
variable "role_name" {}
variable "database" {}
variable "schema" { default = "public" }
variable "privileges" { type = list(string) }
variable "schema_owner" { default = "postgres" }
hcl# modules/db_role/main.tf
resource "postgresql_role" "group" {
name = var.role_name
login = false
}
resource "postgresql_grant" "connect" {
database = var.database
role = postgresql_role.group.name
object_type = "database"
privileges = ["CONNECT"]
}
resource "postgresql_grant" "schema_usage" {
database = var.database
role = postgresql_role.group.name
schema = var.schema
object_type = "schema"
privileges = ["USAGE"]
}
resource "postgresql_grant" "tables" {
database = var.database
role = postgresql_role.group.name
schema = var.schema
object_type = "table"
objects = [] # пусто = все существующие таблицы
privileges = var.privileges
}
resource "postgresql_default_privileges" "future_tables" {
database = var.database
role = postgresql_role.group.name
schema = var.schema
object_type = "table"
privileges = var.privileges
owner = var.schema_owner
}
postgresql_grant с objects = [] даёт права на все существующие таблицы. postgresql_default_privileges распространяется на все будущие. Нужны оба ресурса: забыть про default_privileges — самая частая ошибка, без него у пользователей не будет доступа к вновь созданным таблицам.
Модуль: db_user
hcl# modules/db_user/main.tf
resource "postgresql_role" "user" {
name = var.username
password = var.password
login = true
}
resource "postgresql_grant_role" "membership" {
for_each = toset(var.member_of)
role = each.value
grant_role = postgresql_role.user.name
}
Собираем всё вместе
hcl# main.tf
module "role_rw" {
source = "./modules/db_role"
role_name = "rw"
database = "postgres"
privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}
module "role_ro" {
source = "./modules/db_role"
role_name = "ro"
database = "postgres"
privileges = ["SELECT"]
}
module "alice" {
source = "./modules/db_user"
username = "alice"
password = var.alice_password
member_of = ["rw"]
}
module "bob" {
source = "./modules/db_user"
username = "bob"
password = var.bob_password
member_of = ["ro"]
}
Новый член команды — это правка в три строки: блок модуля, member_of и переменная пароля.
Дизайн ролей
| Роль | Привилегии |
|---|---|
ro |
SELECT |
rw |
SELECT, INSERT, UPDATE, DELETE |
ddl |
rw + CREATE, DROP (для миграций) |
app |
rw (для сервисных пользователей приложений) |
DDL-права — только миграционному пользователю, никогда приложению.
Backend для remote state
hcl# backend.tf
terraform {
backend "s3" {
bucket = "terraform-state"
key = "postgresql/terraform.tfstate"
endpoint = "https://storage.yandexcloud.net"
region = "ru-central1"
skip_credentials_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
force_path_style = true
}
}
Пароли из Vault
hcldata "vault_generic_secret" "db_users" {
path = "secret/infra/postgresql"
}
module "alice" {
source = "./modules/db_user"
username = "alice"
password = data.vault_generic_secret.db_users.data["alice_password"]
member_of = ["rw"]
}
Так пароль не попадает в tfstate (который хранится как обычный JSON).
Интеграция с CI
yaml# .gitlab-ci.yml
terraform-plan:
image: hashicorp/terraform:1.8
script:
- terraform init
- terraform plan -out=tfplan
artifacts:
paths: [tfplan]
terraform-apply:
image: hashicorp/terraform:1.8
script:
- terraform init
- terraform apply tfplan
when: manual
needs: [terraform-plan]
Отладка
bash# Текущие grants
psql -U postgres -c "\dp orders.*"
# Список ролей и членства
psql -U postgres -c "\du"
# Default privileges
psql -U postgres -c "\ddp"
# Импорт существующего пользователя в Terraform state
terraform import module.alice.postgresql_role.user alice