Files
tf_provider/TEST_STAND/CRUD/README.md
T
Repinoid 272d4051f4 docs(crud): в README не было клонирования и адресов приложений — добавлено
Владелец: «где про клонирование?? глазами юзера просмотри весь текст». Пройдено
по тексту целиком, добавлено то, без чего пользователь не начнёт и не проверит:
- раздел «Где взять манифесты»: git clone terraform/tf_provider, cd
  TEST_STAND/CRUD, проверка terraform version, откуда берётся провайдер и какой
  версии (обе папки: nubes-test/nubes 3.0.0);
- раздел «Запуск» стал «четыре шага»: шаг 4 «Проверить» с реальными адресами
  (lucee-crud.luceek8s.dev.nubes.ru, flask-crud.pythonk8s.dev.nubes.ru,
  nodejs-crud.nodejsk8s.dev.nubes.ru) и командой, как посмотреть свои адреса в
  state; проверено грепом по apps/terraform.tfstate;
- заменена заглушка <суффикс> на фактический суффикс nodejsk8s;
- добавлен раздел «Если apply упал» (журнал операции вместо errorLog);
- в «Файлы» добавлены terraform.tfvars.example;
- шаг 3: формулировка про имена приведена к исправленной (задаются в locals.tf).
Одновременно исправлена моя ошибка: предыдущая правка склеила две строки
таблицы «Файлы» (pg/main.tf и pg/postgres.tf) — восстановлено.
То же продублировано в docs/curated/crud/three_apps.md (файлы обязаны совпадать).
Проверено: 260 и 265 строк, блоки кода парные (22 и 24), заголовки на месте.
2026-10-02 08:33:17 +03:00

14 KiB
Raw Blame History

CRUD Test Stand

CRUD = Create / Read / Update / Delete — создать, прочитать, изменить, удалить.

Три приложения (Lucee, Flask, Node.js) работают с одной общей таблицей crud_items в PostgreSQL: в любом из них можно добавлять записи, просматривать их, редактировать и удалять.

Итог после запуска: добавьте запись в одном приложении — остальные два её увидят.


Где взять манифесты

Каталог примера — TEST_STAND/CRUD в репозитории провайдера. Клонируйте и перейдите в него:

git clone https://gitea.services.ngcloud.ru/terraform/tf_provider.git
cd tf_provider/TEST_STAND/CRUD

Нужен установленный Terraform:

terraform version

Провайдер берётся из реестра — источник и версия заданы в pg/main.tf и apps/main.tf (test-стенд: tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes, версия 3.0.0). Дальше все команды выполняются из каталога TEST_STAND/CRUD.


Как это устроено

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

CRUD/
├── pg/     база данных: кластер PostgreSQL + пользователь + база
└── apps/   приложения: Lucee + Flask + Node.js
  • terraform apply и terraform destroy в apps/ меняют только приложения (создают, изменяют, удаляют) — база в pg/ не затрагивается;
  • terraform apply и terraform destroy в pg/ меняют только базу — приложения в apps/ не затрагиваются;
  • приложения можно пересоздавать сколько угодно, база при этом не меняется;
  • связь между каталогами — файл apps/creds.json (см. шаг 2): он делается из 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, а провайдер ищет инстанс по имени внутри своего сервиса (provider/internal/resources_core/crud.go:171, provider/internal/core/refsvc_find.go:47). Если такое имя уже занято, он не станет создавать новый ресурс, а усыновит существующий — то есть при занятом имени можно подцепить чужой или старый инстанс. Если усыновление отключено, apply упадёт с «инстанс с resource_name … уже существует».

Живой пример: прежние имена tflucee, tfflask, tfnodejs заняты старыми инстансами, поэтому в стенде взяты lucee-crud, flask-crud, nodejs-crud (apps/locals.tf:45,57,68).

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

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.


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

Платформа сама клонирует код из репозиториев — собирать и заливать вручную не нужно:

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

Пути задаются в apps/locals.tf: lucee_git_path, flask_git_path, nodejs_git_path.


Запуск — четыре шага

1. База данных (pg/)

cd pg
cp terraform.tfvars.example terraform.tfvars
# заполнить api_token, realm, s3_name (раздел «Что пользователь задаёт сам»)
terraform init
terraform apply

Создадутся кластер (несколько минут), пользователь БД и база.

Обязательные значения — api_token, realm, s3_name. Полный список и правила по именам — в разделе «Что пользователь задаёт сам» выше.

2. Передать креды БД в apps/

# всё ещё в папке pg/
terraform output -json > ../apps/creds.json

Одна команда выгружает хост, порт, имя пользователя, имя БД и пароль в apps/creds.json. Приложения читают этот файл (см. apps/locals.tf).

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

Что именно выгружается и как это читают приложения — в разделе «Справка» в конце файла.

3. Приложения (apps/)

cd ../apps
cp terraform.tfvars.example terraform.tfvars
# заполнить api_token и realm — те же, что в pg/
terraform init
terraform apply

Создадутся три приложения, подключённые к общей БД.

Обязательные значения — api_token и realm (те же, что в pg/). Имена доменов и имена ресурсов приложений задаются в apps/locals.tf — см. раздел «Что пользователь задаёт сам».

4. Проверить

Откройте в браузере три адреса (это полные домены, которые платформа построила из имён в 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

Свои адреса можно посмотреть в state:

grep -o 'https://[a-z0-9.-]*\.dev\.nubes\.ru' apps/terraform.tfstate | sort -u

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


Повседневные операции

Задача Команда
Передеплоить приложения cd apps && terraform apply
Удалить приложения cd apps && terraform destroy — БД не трогается
Изменить БД cd pg && terraform apply
Удалить БД cd pg && terraform destroy — кластер уйдёт в Suspend, а не удалится

При destroy БД кластер переводится в Suspend, а пользователь и база остаются (keep_on_destroy = true). Полное удаление: снять keep_on_destroy в pg/postgres_user_db.tf и apply.


Если apply упал

Текст ошибки в поле errorLog у платформы не всегда отражает суть — реальная причина видна в журнале операции:

GET {api_endpoint}/instanceOperations/<UID>?fields=cfsParams,errorLog,stages

stages[].stageMsg — массив пар [заголовок, лог], смотреть последнюю запись; cfsParams — фактический набор параметров, ушедший на платформу. Разбор реального случая: HISTORY/60_stands/2026-10-01_test_crud_pg_create_failure.md.


Файлы

Файл Что делает
pg/main.tf провайдер, переменные кластера/пользователя/базы
pg/postgres.tf кластер nubes_postgres.main_pg
pg/postgres_user_db.tf пользователь crud_user_0 + база pg_db
pg/outputs.tf хост, порт, юзер, база, пароль — для выгрузки кредов
apps/main.tf провайдер, переменные приложений
apps/locals.tf чтение creds.json, домены, версии, размеры
apps/lucee.tf, apps/flask.tf, apps/nodejs.tf три приложения
pg/terraform.tfvars.example, apps/terraform.tfvars.example шаблоны для копирования в terraform.tfvars
apps/creds.json генерируется шагом 2, не в git

Справка: выходные параметры 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