Перенос в тематические папки сломал бы все ссылки, поэтому обновлены пути:
- HISTORY/OPUS/ -> HISTORY/90_llm/OPUS/, HISTORY/SONNET/ -> HISTORY/90_llm/SONNET/;
- 15 целевых файлов из корня получили свой тематический префикс
(HISTORY/<файл>.md -> HISTORY/<папка>/<файл>.md) — в 33 файлах репозитория.
Затронуто вне HISTORY: README.md, VERSIONS.md, HOW_TO/DEVOPS_BUILD_PIPELINE.md,
NOTES/README.md, NOTES/10_plans/, NOTES/20_prompts/, NOTES/30_analysis/,
NOTES/40_chat_summaries/, docs/curated/{crud,postgres}, docs/help/dev-reference/,
docs/ops/TESTING.md.
Проверки после правки:
- ссылок вида HISTORY/<дата> без тематической папки не осталось;
- все пути HISTORY/*.md из markdown-ссылок существуют (кроме трёх упоминаний,
которые не были файлами и до переноса: HISTORY/90_llm/OPUS/3006_1.md,
3006_0.md — планировавшиеся имена в старых транскриптах,
и HISTORY/HOWTO-UPLOAD.md — ссылка на старый внешний репозиторий tf_registry);
- ссылки по «голому» имени внутри 90_llm/OPUS/ остались корректными (соседние файлы).
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/60_stands/2026-10-01_test_crud_pg_create_failure.md`.
|