Новая страница docs/curated/crud/three_apps.md (раздел «Проверенные примеры» в mkdocs.yml):
- что создаётся (6 ресурсов: PG + user + db + Lucee + Flask + Node.js);
- ссылки на git: gitea.services.ngcloud.ru/terraform/{tfluceecrud,tfflaskcrud,tfnodejscrud}
(проверено HTTP 200), в манифестах — lucee_git_path/flask_git_path/nodejs_git_path;
- запуск: два apply и почему (пароль появляется только после create_user);
- доступ к БД: vault_secrets["users"] -> { "<username>": { "password" } }, и ловушка
state_out.users (метаданные без пароля);
- особенности: adopt_existing_on_create, уникальные домены, camelCase в postgres_conf, realm;
- диагностика: ошибку смотреть в stages операции, а не в errorLog.
Проверено локальной сборкой mkdocs: страница собирается, битых ссылок нет,
все три ссылки на репозитории присутствуют в HTML.
117 lines
6.3 KiB
Markdown
117 lines
6.3 KiB
Markdown
# 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 `{ "<username>": { "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/<UID>?fields=cfsParams,errorLog,stages
|
||
```
|
||
|
||
`stages[].stageMsg` — массив пар `[заголовок, лог]`, смотреть последнюю запись. Там же
|
||
`cfsParams` — фактический набор параметров, ушедший на платформу. Разбор реального случая:
|
||
`HISTORY/2026-10-01_test_crud_pg_create_failure.md`.
|