Skip to content

PostgreSQL: кластер + пользователь + база + S3-бэкап

Рабочий сценарий: кластер PostgreSQL с пользователем, базой и S3-бэкапом.

Что создаётся

Ресурс Имя в манифесте Назначение
nubes_postgres main_pg Кластер PostgreSQL
nubes_postgres_user app_user Пользователь БД
nubes_postgres_database app_db База данных
nubes_s3bucket backups S3-бакет для бэкапов

Переменные

Создай terraform.tfvars (несекретные значения):

realm   = "<имя кластера Kubernetes>" # ресурсная платформа из вашей организации
s3_name = "my-s3"                    # ИМЯ экземпляра S3 (не UUID!)

Токен держи в отдельном файле secrets.auto.tfvars (добавь в .gitignore, не коммить):

api_token = "***"                    # токен из Личного кабинета

Проверить файл поможет ИИ

Кнопка «TF» справа внизу: вставьте содержимое terraform.tfvars — ИИ вернёт исправленный файл и список изменений. Секреты (api_token, пароли) не вставляйте.

Провайдер

terraform {
  required_providers {
    nubes = {
      source  = "tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes"
      version = "1.6.3"
    }
  }
}

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 = "https://lk-api-gateway.ngcloud.ru/api/v1/svc"
}

Кластер PostgreSQL

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"              # таймаут операции
}

Пользователь и база данных

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-бакет для бэкапов

resource "nubes_s3bucket" "backups" {
  resource_name = "pg-backup-bucket"     # любое имя
  s3_user_uid   = var.s3_name            # ИМЯ S3 (не UUID!)
  bucket_name   = "my-app-backups"       # имя бакета
}

Запуск

terraform init
terraform apply

Все 4 ресурса за один apply. Terraform сам выстроит порядок: кластер → пользователь → БД → бакет.

⚠️ Исключение — если приложение берёт пароль БД из vault_secrets (как в CRUD-стенде): нужен второй apply. В первом создаются кластер и пользователь, во втором приложения получают уже появившийся пароль. Иначе — Invalid index ... does not identify an element in this collection value.

Пароль пользователя БД и секреты Vault

Пароль пользователя, созданного через nubes_postgres_user, платформа пишет в Vault и отдаёт в выходе vault_secrets["users"] — это JSON-строка:

{ "<имя_пользователя>": { "password": "<пароль>" } }

Как брать в конфиге:

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)
}

Ловушки

  • 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 и пользователь создаются в одном прогоне, а приложения читают пароль: см. предупреждение в разделе «Запуск» выше.

Диагностика сбоя создания

Если создание кластера или пользователя завершилось ошибкой, Terraform выведет её текст в терминале (в поле error ресурса). Для анализа того, что именно ушло на платформу, смотрите журнал операции экземпляра в Личном кабинете Nubes.