# PostgreSQL: кластер + пользователь + база + S3-бэкап Проверенный сценарий. Протестировано 2026-08-10 на TEST-стенде. ## Что создаётся | Ресурс | Имя в манифесте | Назначение | |--------|----------------|-----------| | `nubes_postgres` | `main_pg` | Кластер PostgreSQL | | `nubes_postgres_user` | `app_user` | Пользователь БД | | `nubes_postgres_database` | `app_db` | База данных | | `nubes_s3bucket` | `backups` | S3-бакет для бэкапов | ## Переменные Создай `terraform.tfvars`: ```hcl api_token = "***" # токен из Личного кабинета realm = "k8s-3-sandbox-nubes-ru" # кластер Kubernetes (из организации) s3_name = "my-s3" # ИМЯ экземпляра S3 (не UUID!) ``` ## Провайдер ```hcl terraform { required_providers { nubes = { source = "{{PROVIDER_SOURCE}}" version = "{{VERSION}}" } } } variable "api_token" { type = string; sensitive = true } variable "realm" { type = string } variable "s3_name" { type = string } provider "nubes" { api_token = var.api_token api_endpoint = "{{NUBES_API_ENDPOINT}}" } ``` ## Кластер PostgreSQL ```hcl resource "nubes_postgres" "main_pg" { resource_name = "my-postgres" # любое имя # На каком кластере Kubernetes развернуть startup_configuration = { resource_realm = var.realm # имя кластера } # Ресурсы пода cluster_configuration = { cpu = 500 # milicores (1000 = 1 ядро) memory = 512 # megabytes replicas = 1 # 1, 3, 5 или 7 disk = 10 # gigabytes } # Сетевой доступ access_configuration = { master_ip_space = "no-needed" # Ip-Space для внешнего IP мастера master_access_list = jsonencode(["10.0.0.0/8"]) # кому доступ к мастеру slave_ip_space = "no-needed" # Ip-Space для внешнего IP слейва slave_access_list = jsonencode([]) # кому доступ к слейву ([] = всем) } # Версия и настройки PostgreSQL postgres_configuration = { version = "17" # 16 или 17 ssl_required = true # требовать SSL pooler_master = false # PgBouncer для записи pooler_slave = false # PgBouncer для чтения } # Резервное копирование backup_configuration = { s3_uid = var.s3_name # ИМЯ S3 (не UUID!) retain = 14 # сколько бэкапов хранить schedule = "0 0 * * *" # cron (ежедневно в полночь) } # Автоскейлинг диска autoscale_configuration = { enabled = false # выключен schedule = 0 # час окна (0-23) percent = 10 # % расширения quota = 100 # макс. размер диска (GB) } # Mutual TLS mtls_configuration = { type = "off" # off | manual | auto duration_ca = 175200 # время жизни CA (часы) duration_server = 87600 # время жизни сертификата (часы) } operation_timeout = "11m" # таймаут операции } ``` ## Пользователь и база данных ```hcl resource "nubes_postgres_user" "app_user" { postgres_id = nubes_postgres.main_pg.id # ссылка на кластер username = "app_user" # имя пользователя role = "ddl_user" # app_user (DML) | ddl_user (DDL) mtls_access = false # mTLS для пользователя } resource "nubes_postgres_database" "app_db" { postgres_id = nubes_postgres.main_pg.id # ссылка на кластер db_name = "my_app_db" # имя базы данных db_owner = nubes_postgres_user.app_user.username # владелец } ``` ## S3-бакет для бэкапов ```hcl resource "nubes_s3bucket" "backups" { resource_name = "pg-backup-bucket" # любое имя s3_user_uid = var.s3_name # ИМЯ S3 (не UUID!) bucket_name = "my-app-backups" # имя бакета } ``` ## Запуск ```bash terraform init terraform apply ``` Все 4 ресурса за один `apply`. Terraform сам выстроит порядок: кластер → пользователь → БД → бакет. > ⚠️ **Исключение — если приложение берёт пароль БД из `vault_secrets` (как в CRUD-стенде): нужен второй `apply`.** > В первом создаются кластер и пользователь, во втором приложения получают уже появившийся пароль. > Иначе — `Invalid index ... does not identify an element in this collection value`. ## Пароль пользователя БД и секреты Vault Проверено 2026-10-01 на TEST-стенде (инстанс `pg4crud2`). Пароль пользователя, созданного через `nubes_postgres_user`, платформа пишет в Vault и отдаёт в выходе `vault_secrets["users"]` — это **JSON-строка**: ```json { "<имя_пользователя>": { "password": "<пароль>" } } ``` Как брать в конфиге: ```hcl locals { pg_host = nubes_postgres.main_pg.state_out_flat["internalMaster"] pg_user = nubes_postgres_user.crud_user_0.username pg_pass = nonsensitive(jsondecode(nubes_postgres.main_pg.vault_secrets["users"])[local.pg_user].password) } ``` Проверить вручную (то же, что читает провайдер): ```bash GET {api_endpoint}/instances//vault/users ``` ### Ловушки - **`state.out.users` — это метаданные** (`role`, `rights`, `username`, `mtlsAccess`), **пароля там нет**. Пароль лежит только в `vault_secrets["users"]`. Это два разных объекта, их легко перепутать. - Список доступных имён секретов — выход `vault_fields` (у PostgreSQL это `["users"]`). - Ключей `adminUser` / `adminPass` / `standbyUser` / `standbyPass` у новых инстансов **нет** — эндпоинт `/vault/<имя>` отдаёт 404. Они встречаются в старых материалах (например, в `docs/30_registry/guides/getting-started.md`). - ⚠️ **Нужно два `apply`**, если PG и пользователь создаются в одном прогоне, а приложения читают пароль: см. предупреждение в разделе «Запуск» выше. ### Диагностика сбоя операции `errorLog` у платформы **не отражает суть**. Реальная причина — в `stages`: ```bash GET {api_endpoint}/instanceOperations/?fields=cfsParams,errorLog,stages ``` `stages[].stageMsg` — JSON-массив пар `[заголовок, лог]`, смотреть последнюю запись. Там же `cfsParams` — фактический payload, ушедший на платформу. Разбор реального случая: `HISTORY/2026-10-01_test_crud_pg_create_failure.md`.