# CRUD-стенд: PostgreSQL + Lucee + Flask + Node.js Проверенный пример: одна общая база PostgreSQL, три приложения на разных стеках и один сквозной сценарий CRUD (создать/прочитать/обновить/удалить) в одной таблице `crud_items`. Манифесты: `TEST_STAND/CRUD/` (test-стенд). Аналог — `DEV_STAND/CRUD/`. Состояние на 2026-10-01, TEST-стенд: все 6 ресурсов созданы, сервисы `running`. ## Что создаётся | Ресурс | Имя | Что это | |---|---|---| | `nubes_postgres` | `pg4crud2` | кластер PostgreSQL 17 | | `nubes_postgres_user` | `user4crudpg` | пользователь БД, роль `ddl_user` | | `nubes_postgres_database` | `db4crudpg` | база, владелец — этот пользователь | | `nubes_lucee` | `crud-lucee` | приложение на CFML | | `nubes_flask` | `crud-flask` | приложение на Python | | `nubes_nodejs` | `crud-nodejs` | приложение на Node.js | Все три приложения ходят в одну базу и одну таблицу — так видно, что 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` | Ссылки на репозитории задаются в манифестах: `lucee_git_path`, `flask_git_path`, `nodejs_git_path` в `TEST_STAND/CRUD/locals.tf`. Платформа сама клонирует код из этих репозиториев — вручную ничего собирать и заливать не нужно. ## Провайдер и запуск ```hcl terraform { required_providers { nubes = { source = "{{PROVIDER_SOURCE}}" version = "{{VERSION}}" } } } provider "nubes" { api_token = var.api_token api_endpoint = "{{NUBES_API_ENDPOINT}}" } ``` ```hcl # terraform.tfvars api_token = "***" # ЛК → Профиль → Токены → «Технический» realm = "k8s-4-sandbox-nubes-ru" # кластер Kubernetes (из списка в ЛК) s3_name = "naeel-s3" # имя экземпляра S3 для бэкапов ``` ```bash terraform init terraform apply # 1-й прогон: PostgreSQL + пользователь + база terraform apply # 2-й прогон: приложения (пароль БД уже в Vault) ``` !!! warning "Нужны два `apply` — это не ошибка" Пароль пользователя БД платформа создаёт вместе с пользователем, а `vault_secrets` у ресурса кластера читаются на этапе его создания — то есть **до** `create_user`. Поэтому в первом прогоне пароля ещё нет, и приложения получают пустое значение. Во втором прогоне пароль уже в Vault, и приложения обновляются с верным значением. В манифестах чтение пароля обёрнуто в `try(...)`, чтобы первый прогон **не падал** с `Invalid index ... does not identify an element in this collection value`. Подробности: [PostgreSQL: пароль пользователя](../postgres/pg_user_db.md). ## Доступ приложений к базе Пароль берётся из выхода `vault_secrets["users"]` — это JSON `{ "": { "password": "..." } }`: ```hcl locals { pg_host = nubes_postgres.main_pg.state_out_flat["internalMaster"] pg_user = nubes_postgres_user.crud_user_0.username pg_pass = try(nonsensitive(jsondecode(nubes_postgres.main_pg.vault_secrets["users"]).user4crudpg.password), "") } ``` ⚠️ Не путать с выходом `state_out.users` — там **метаданные** (`role`, `rights`, `username`, `mtlsAccess`) и **пароля нет**. Пароль только в `vault_secrets`. ## Особенности этого примера - **`adopt_existing_on_create = true`** у всех ресурсов — если инстанс с таким именем уже есть (например после `destroy`, который приостанавливает, а не удаляет), провайдер его **усыновит**, а не упадёт с «ресурс с таким именем уже существует». - **Домены приложений должны быть уникальными** — `lucee_domain`, `flask_domain`, `nodejs_domain` в `locals.tf`. Иначе платформа откажет. - **`postgres_conf`** — ключи строго как в спецификации платформы: `paramName` / `paramValue` (camelCase). Допустимы только `log_connections` и `log_disconnections`. - **`realm`** — рабочая ресурсная платформа. Если у неё нет ёмкости, операция падает с сообщением «Невозможно развернуть приложение в данной ресурсной платформе» — смотрите журнал операции, а не только текст ошибки. ## Диагностика, если `apply` упал Текст ошибки в `errorLog` у платформы **не всегда отражает суть**. Реальная причина — в журнале операции: ```bash GET {api_endpoint}/instanceOperations/?fields=cfsParams,errorLog,stages ``` `stages[].stageMsg` — массив пар `[заголовок, лог]`, смотреть последнюю запись. Там же `cfsParams` — фактический набор параметров, ушедший на платформу. Разбор реального случая: `HISTORY/2026-10-01_test_crud_pg_create_failure.md`.