Требования владельца: «СНАЧАЛА — кратко чё это вообще и ДЕЙСТВИЯ … всё остальное
описание — ПОСЛЕ»; «юзер НЕ МОЖЕТ вводить никакие команды … УБЕРИ ЭТО и подобное».
- раздел «Быстрый старт» — первым: клонирование, заполнение переменных, apply pg,
creds.json, apply apps, адреса приложений, предупреждение про пароль в файле;
подробное описание — ниже (READМE: Как это устроено / Что задаёт пользователь /
Код приложений / Повседневные операции / Файлы / Справка);
- из README убран раздел «Если apply упал», со страницы — «Диагностика, если
apply упал»: пользователь не может делать вызовы вида
GET {api_endpoint}/instanceOperations/... — вместо инструкции было ничего не
работающее; вместе с разделом убрана ссылка на HISTORY/60_stands/... (внутренний
репозиторий);
- убраны ссылки на внутренний код (provider/internal/.../crud.go:171,
refsvc_find.go:47) и на строки apps/locals.tf:45,57,68; убран абзац про прежние
имена tflucee/tfflask/tfnodejs (внутренняя история);
- со страницы убрано «смотрите журнал операции» в разделе «Особенности».
Проверено на живой странице (162257 байт, совпадает с локальной сборкой):
instanceOperations / errorLog / HISTORY/ / crud.go / refsvc_find — 0 вхождений;
первый раздел — «Быстрый старт».
12 KiB
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 |
Свои адреса — из state:
grep -o 'https://[a-z0-9.-]*\.dev\.nubes\.ru' apps/terraform.tfstate | sort -u
Добавьте запись в одном приложении — она появится в двух других (таблица crud_items общая).
⚠️ В
apps/creds.jsonпароль лежит открытым текстом: файл вapps/.gitignore— не коммитить и не пересылать.
Провайдер берётся из реестра: источник и версия заданы в pg/main.tf и apps/main.tf.
Все команды выполняются из каталога TEST_STAND/CRUD.
Подробности дальше: что задавать руками, как всё устроено, что делать при ошибке.
Что создаётся
| Ресурс | Имя (по умолчанию) | Что это |
|---|---|---|
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 |
Пути к репозиториям задаются в TEST_STAND/CRUD/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— рабочая ресурсная платформа. Если у неё нет ёмкости, операция падает с сообщением «Невозможно развернуть приложение в данной ресурсной платформе».