Files
tf_provider/docs/curated/crud/three_apps.md
T
Repinoid 809431196f docs(crud): убраны команда по state и фраза про каталог — юзеру не нужны
Замечания владельца: «нахуя это юзеру???» (про grep по apps/terraform.tfstate) и
«Все команды выполняются из каталога TEST_STAND/CRUD — ЭТО ЧТО?????».
- убрано «Свои адреса — из state» с grep -o 'https://...' apps/terraform.tfstate
  (парсинг внутреннего state регуляркой; адреса и так даны таблицей выше);
- убрана фраза «Все команды выполняются из каталога TEST_STAND/CRUD» (после cd
  из шага 1 пользователь уже в этом каталоге — шум);
- README: убраны дублирующие подскобки про test-стенд и версию 3.0.0 (видно в
  main.tf) и хвост «что делать при ошибке» — раздел про ошибки удалён ранее;
- страница: TEST_STAND/CRUD/apps/locals.tf -> apps/locals.tf (как в README).
Проверено: живая страница 161643 байта, cmp с локальной сборкой совпал;
terraform.tfstate / «Все команды» / «при ошибке» — 0 вхождений.
2026-10-02 08:52:28 +03:00

12 KiB
Raw Blame History

CRUD-стенд: PostgreSQL + Lucee + Flask + Node.js

Проверенный пример: одна общая база PostgreSQL, три приложения на разных стеках (Lucee/CFML, Python, Node.js) и одна таблица crud_items. Запись, добавленная в любом из трёх приложений, видна в двух других.

Манифесты: TEST_STAND/CRUD/ в репозитории провайдера — два каталога: pg/ (база) и apps/ (приложения). DEV_STAND/CRUD/ — тот же пример, но по старой схеме: всё в одной папке.

Быстрый старт

# 1. получить манифесты (нужен установленный Terraform — проверьте `terraform version`)
git clone https://gitea.services.ngcloud.ru/terraform/tf_provider.git
cd tf_provider/TEST_STAND/CRUD

# 2. заполнить переменные (что именно — в разделе «Что пользователь задаёт сам»)
cd pg     && cp terraform.tfvars.example terraform.tfvars  # api_token, realm, s3_name
cd ../apps && cp terraform.tfvars.example terraform.tfvars  # api_token, realm — те же

# 3. база данных (кластер создаётся несколько минут)
cd ../pg && terraform init && terraform apply

# 4. передать креды БД приложениям
terraform output -json > ../apps/creds.json

# 5. три приложения
cd ../apps && terraform init && terraform apply

После этого приложения открываются в браузере (имена заданы в apps/locals.tf):

Приложение Адрес
Lucee https://lucee-crud.luceek8s.dev.nubes.ru
Flask https://flask-crud.pythonk8s.dev.nubes.ru
Node.js https://nodejs-crud.nodejsk8s.dev.nubes.ru

Добавьте запись в одном приложении — она появится в двух других (таблица crud_items общая).

⚠️ В apps/creds.json пароль лежит открытым текстом: файл в apps/.gitignore — не коммитить и не пересылать.

Провайдер берётся из реестра: источник и версия заданы в pg/main.tf и apps/main.tf.

Подробности дальше: что задавать руками и как всё устроено.

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

Ресурс Имя (по умолчанию) Что это
nubes_postgres pg4crud2 кластер PostgreSQL 17
nubes_postgres_user user4crudpg пользователь БД, роль ddl_user
nubes_postgres_database db4crudpg база, владелец — этот пользователь
nubes_lucee lucee-crud приложение на Lucee 5.4
nubes_flask flask-crud приложение на Python 3.12
nubes_nodejs nodejs-crud приложение на Node.js 22

Все три приложения ходят в одну базу и одну таблицу — так видно, что CRUD работает одинаково с любого стека.

Код приложений (git)

Приложение Репозиторий
Lucee (CFML) https://gitea.services.ngcloud.ru/terraform/tfluceecrud
Flask (Python) https://gitea.services.ngcloud.ru/terraform/tfflaskcrud
Node.js (Express) https://gitea.services.ngcloud.ru/terraform/tfnodejscrud

Пути к репозиториям задаются в apps/locals.tf (lucee_git_path, flask_git_path, nodejs_git_path). Платформа сама клонирует код — собирать и заливать вручную не нужно.

Каждое приложение получает в json_env переменную SERVICE_NAME (lucee / flask / nodejs) — это значение колонки created_by, чтобы было видно, кто добавил строку.

Структура манифестов

Два каталога — два отдельных файла состояния Terraform:

CRUD/
├── pg/     база данных: кластер PostgreSQL + пользователь + база
└── apps/   приложения: Lucee + Flask + Node.js
  • terraform apply и terraform destroy в apps/ меняют только приложения — база в pg/ не затрагивается;
  • terraform apply и terraform destroy в pg/ меняют только базу — приложения в apps/ не затрагиваются;
  • приложения можно пересоздавать сколько угодно, база при этом не меняется;
  • связь между каталогами — файл apps/creds.json: он делается из terraform output в pg/, поэтому после смены хоста или пароля его нужно обновить и повторить apply в apps/.

Что пользователь задаёт сам

Обязательные значения (без них apply не пройдёт)

Переменная Где Что это
api_token pg/terraform.tfvars и apps/terraform.tfvars токен Nubes: ЛК → Профиль → Токены → создать «Технический»
realm там же, одно и то же значение ресурсная платформа (кластер Kubernetes), например k8s-4-sandbox-nubes-ru; в apps/ обязан совпасть с pg/, иначе приложения не увидят кластер с базой
s3_name pg/terraform.tfvars имя (или UUID) экземпляра S3 для бэкапов: ЛК → S3

Файлы создаются из примеров и нужны в обеих папках:

cd pg && cp terraform.tfvars.example terraform.tfvars
cd ../apps && cp terraform.tfvars.example terraform.tfvars

Имена: придумать самому, и они обязаны быть уникальными

Имя Где задаётся Правило
pg_resource_name (кластер) pg/terraform.tfvars уникально в пределах стенда
pg_username, pg_db_name pg/terraform.tfvars уникальны в пределах кластера; служебные имена (admin, postgres, standby) платформа не примет
lucee_resource_name, flask_resource_name, nodejs_resource_name apps/locals.tf уникальны в пределах стенда
lucee_domain, flask_domain, nodejs_domain apps/locals.tf здесь задаётся не полный домен, а имя — суффикс платформа добавляет сама: lucee-crud → lucee-crud.luceek8s.dev.nubes.ru, flask-crud → flask-crud.pythonk8s.dev.nubes.ru, nodejs-crud → nodejs-crud.nodejsk8s.dev.nubes.ru. Полные имена уникальны (это обычные DNS-имена), поэтому и задаваемое имя должно быть уникальным

Почему это важно: все ресурсы создаются с adopt_existing_on_create = true, а провайдер ищет инстанс по имени внутри своего сервиса. Если такое имя уже занято, он не станет создавать новый ресурс, а усыновит существующий — то есть при занятом имени можно подцепить чужой или старый инстанс. Если усыновление отключено, apply упадёт с «инстанс с resource_name … уже существует».

Можно не задавать (есть значения по умолчанию)

pg/main.tf: pg_cpu=500, pg_memory=512, pg_replicas=1, pg_disk=10, pg_version="17", pg_retain=14, pg_schedule="0 0 * * *", pg_timeout="11m", pg_username="user4crudpg", pg_role="ddl_user", pg_db_name="db4crudpg". Размеры приложений (*_cpu, *_memory, *_replicas) — в apps/locals.tf.

Провайдер

terraform {
  required_providers {
    nubes = {
      source  = "{{PROVIDER_SOURCE}}"
      version = "{{VERSION}}"
    }
  }
}

provider "nubes" {
  api_token    = var.api_token
  api_endpoint = "{{NUBES_API_ENDPOINT}}"
}

Справка: выходные параметры pg/

В pg/outputs.tf объявлено шесть выходов:

Выход Значение Откуда
pg_host внутренний хост master state_out_flat["internalMaster"] кластера
pg_port 5432 константа в outputs.tf
pg_username имя пользователя БД выход подресурса nubes_postgres_user
pg_db_name имя базы выход nubes_postgres_database
pg_password пароль, sensitive = true выход подресурса nubes_postgres_user: платформа генерирует пароль сама при создании пользователя, провайдер читает его из Vault и отдаёт выходом подресурса
pg_ssl_mode require константа в outputs.tf

terraform output -json кладёт в файл не голые значения, а объекты вида { "sensitive": ..., "type": ..., "value": ... }:

{
  "pg_host":     { "sensitive": false, "type": "string", "value": "<хост master>" },
  "pg_password": { "sensitive": true,  "type": "string", "value": "<пароль>" }
}

Поэтому в apps/locals.tf значение берётся через .value:

creds   = jsondecode(file("${path.module}/creds.json"))
pg_host = local.creds.pg_host.value
pg_pass = local.creds.pg_password.value

Дальше apps/*.tf передают эти значения приложениям как переменные окружения:

Выход pg/ Flask Node.js Lucee
pg_host PGHOST PGHOST PGHOST, testds_connectionString
pg_port PGPORT PGPORT PGPORT, testds_connectionString
pg_username PGUSER PGUSER PGUSER, testds_username
pg_password PGPASSWORD PGPASSWORD PGPASSWORD, testds_password
pg_db_name PGDATABASE PGDATABASE testds_connectionString, DATABASE_URL
pg_ssl_mode PGSSLMODE PGSSLMODE PGSSLMODE

Особенности этого примера

  • adopt_existing_on_create = true у всех ресурсов — если инстанс с таким именем уже есть (например после destroy, который приостанавливает, а не удаляет), провайдер его усыновит, а не упадёт с «ресурс с таким именем уже существует».
  • keep_on_destroy = true у пользователя и базы: при destroy в pg/ кластер уходит в Suspend, а пользователь и база остаются. Иначе база исчезла бы вместе с кластером. Повторный apply возвращает их в state (усыновление).
  • Домены приложений должны быть уникальными — lucee_domain, flask_domain, nodejs_domain в apps/locals.tf. Иначе платформа откажет.
  • postgres_conf — ключи строго как в спецификации платформы: paramName / paramValue (camelCase). Допустимы только log_connections и log_disconnections.
  • realm — рабочая ресурсная платформа. Если у неё нет ёмкости, операция падает с сообщением «Невозможно развернуть приложение в данной ресурсной платформе».