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