Владелец: «где про клонирование?? глазами юзера просмотри весь текст». Пройдено по тексту целиком, добавлено то, без чего пользователь не начнёт и не проверит: - раздел «Где взять манифесты»: 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), заголовки на месте.
14 KiB
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 |