Files
tf_provider/TEST_STAND/CRUD/README.md
T
Repinoid 272d4051f4 docs(crud): в README не было клонирования и адресов приложений — добавлено
Владелец: «где про клонирование?? глазами юзера просмотри весь текст». Пройдено
по тексту целиком, добавлено то, без чего пользователь не начнёт и не проверит:
- раздел «Где взять манифесты»: 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), заголовки на месте.
2026-10-02 08:33:17 +03:00

261 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CRUD Test Stand
CRUD = Create / Read / Update / Delete — создать, прочитать, изменить, удалить.
Три приложения (Lucee, Flask, Node.js) работают с одной общей таблицей `crud_items` в PostgreSQL:
в любом из них можно добавлять записи, просматривать их, редактировать и удалять.
Итог после запуска: добавьте запись в одном приложении — остальные два её увидят.
---
## Где взять манифесты
Каталог примера — `TEST_STAND/CRUD` в репозитории провайдера. Клонируйте и перейдите в него:
```bash
git clone https://gitea.services.ngcloud.ru/terraform/tf_provider.git
cd tf_provider/TEST_STAND/CRUD
```
Нужен установленный Terraform:
```bash
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 |
Файлы создаются из примеров и нужны в обеих папках:
```bash
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`](https://gitea.services.ngcloud.ru/terraform/tfluceecrud) |
| Flask (Python) | [`terraform/tfflaskcrud`](https://gitea.services.ngcloud.ru/terraform/tfflaskcrud) |
| Node.js (Express) | [`terraform/tfnodejscrud`](https://gitea.services.ngcloud.ru/terraform/tfnodejscrud) |
Пути задаются в `apps/locals.tf`: `lucee_git_path`, `flask_git_path`, `nodejs_git_path`.
---
## Запуск — четыре шага
### 1. База данных (`pg/`)
```bash
cd pg
cp terraform.tfvars.example terraform.tfvars
# заполнить api_token, realm, s3_name (раздел «Что пользователь задаёт сам»)
terraform init
terraform apply
```
Создадутся кластер (несколько минут), пользователь БД и база.
Обязательные значения — `api_token`, `realm`, `s3_name`. Полный список и правила по именам —
в разделе «Что пользователь задаёт сам» выше.
### 2. Передать креды БД в `apps/`
```bash
# всё ещё в папке pg/
terraform output -json > ../apps/creds.json
```
Одна команда выгружает хост, порт, имя пользователя, имя БД и пароль в `apps/creds.json`.
Приложения читают этот файл (см. `apps/locals.tf`).
> ⚠️ В `creds.json` пароль лежит **открытым текстом**: файл в `apps/.gitignore`, не коммитить и не пересылать.
Что именно выгружается и как это читают приложения — в разделе «Справка» в конце файла.
### 3. Приложения (`apps/`)
```bash
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:
```bash
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` у платформы **не всегда отражает суть** — реальная причина видна
в журнале операции:
```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`.
---
## Файлы
| Файл | Что делает |
|---|---|
| `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": ... }`:
```json
{
"pg_host": { "sensitive": false, "type": "string", "value": "<хост master>" },
"pg_password": { "sensitive": true, "type": "string", "value": "<пароль>" }
}
```
Поэтому в `apps/locals.tf` значение берётся через `.value`:
```hcl
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` |