Files
Repinoid 15397298de Maria_CRUD: пример стенда (MariaDB + Lucee + Flask + Node.js + HTTP-проверка) размещён в репозитории примеров
Взят пользовательский набор отслеживаемых git файлов стенда TEST_STAND/Maria_CRUD
репозитория tf_provider: README.md, main.tf, resources.tf, terraform.tfvars.example.
Секреты и состояние не переносились: terraform.tfvars, terraform.tfstate* остались на
месте, корневой .gitignore закрывает *.tfvars.

Правки при переносе:
- README.md — клонирование теперь
  git clone https://gitea.services.ngcloud.ru/Nail/tf_examples.git && cd tf_examples/Maria_CRUD;
  в блоке «Провайдер» подставлены конкретные значения (source .../nubes-test/nubes,
  version 3.1.0, endpoint TEST-стенда) вместо плейсхолдеров {{...}} — как в примере CRUD;
- resources.tf — комментарий ссылался на несуществующий путь TEST_STAND/CRUD/pg/outputs.tf
  (CRUD переведён на плоскую схему), заменён на «см. пример CRUD в этом же репозитории».

Не делалось: apply из этой копии не запускался (работоспособность проверена на стенде
TEST_STAND/Maria_CRUD), корневой README репозитория примеров не менялся — он относит все
папки, кроме fullpipe_chain, к черновикам, и для CRUD его тоже не меняли.
2026-10-03 09:27:36 +03:00

219 lines
16 KiB
Markdown
Raw Permalink 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-стенд: MariaDB + Lucee + Flask + Node.js + HTTP-проверка
Проверенный пример: одна общая база MariaDB, три приложения на разных стеках (Lucee/CFML,
Python, Node.js) и одна таблица `crud_items`. Запись, добавленная в любом из трёх приложений,
видна в двух других. Рядом — отдельный контейнер HTTP-проверки, который подтверждает, что
приложение видит базу.
Манифесты: репозиторий примеров `tf_examples`, папка `Maria_CRUD` — **один каталог, один файл состояния**.
Креды базы передаются приложениям внутри того же `apply` (подробнее — раздел
«Особенности этого примера», пункт про `vault_secrets`), поэтому отдельный `creds.json` и
второй каталог не нужны.
## Быстрый старт
```bash
# 1. получить манифесты (нужен установленный Terraform — проверьте `terraform version`)
git clone https://gitea.services.ngcloud.ru/Nail/tf_examples.git
cd tf_examples/Maria_CRUD
# 2. заполнить переменные (что именно — в разделе «Что пользователь задаёт сам»)
cp terraform.tfvars.example terraform.tfvars # api_token, realm
# 3. создать всё: кластер MariaDB + база + пользователь + три приложения + HTTP-проверка
terraform init
terraform apply
```
Кластер создаётся несколько минут; приложения поднимаются после него.
> ⛔ Перед `apply` образ HTTP-проверки должен быть **опубликован** в реестре
> (`gitea.services.ngcloud.ru/terraform/mariadb-check:0.1`) — иначе создание `nubes_http` упадёт.
После этого сервисы открываются в браузере:
| Сервис | Адрес |
|---|---|
| Lucee | <https://maria-crud-lucee.luceek8s.dev.nubes.ru> |
| Flask | <https://maria-crud-flask.pythonk8s.dev.nubes.ru> |
| Node.js | <https://maria-crud-nodejs.nodejsk8s.dev.nubes.ru> |
| HTTP-проверка | <https://maria-crud-http.containerk8s.dev.nubes.ru/readyz> |
Добавьте запись в одном приложении — она появится в двух других (таблица `crud_items` общая).
HTTP-проверка отвечает `{"service":"maria-http","status":"ready","database":"connected"}`,
когда подключение к базе живо.
## Что создаётся
| Ресурс | Имя (по умолчанию) | Что это |
|---|---|---|
| `nubes_mariadb` | `maria-crud-db` | кластер MariaDB 9.4.0 |
| `nubes_mariadb_database` | — | база `maria` |
| `nubes_mariadb_user` | — | пользователь `cruduser` с полными правами (`ALL PRIVILEGES`) на `maria` |
| `nubes_lucee` | `maria-crud-lucee` | приложение на Lucee 5.4 |
| `nubes_flask` | `maria-crud-flask` | приложение на Python 3.12 |
| `nubes_nodejs` | `maria-crud-nodejs` | приложение на Node.js 23 |
| `nubes_http` | `maria-crud-http` | контейнер проверки (образ `mariadb-check:0.1`) |
Все три приложения ходят в одну базу и одну таблицу — так видно, что CRUD работает одинаково
с любого стека. Файл состояния один, поэтому `apply`/`destroy` действуют на весь стенд сразу.
## Что пользователь задаёт сам
### Обязательные значения (без них `apply` не пройдёт)
| Переменная | Где | Что это |
|---|---|---|
| `api_token` | `terraform.tfvars` | токен Nubes: ЛК → Профиль → Токены → создать «Технический» |
| `realm` | там же | ресурсная платформа (кластер Kubernetes), например `k8s-4-sandbox-nubes-ru`; **должна существовать в ЛК**, иначе операция падает с «Ресурсной платформы … нету в Личном кабинете. Не удалось получить kubeconfig» |
```bash
cp terraform.tfvars.example terraform.tfvars
```
### Значения в самом конфиге, которые надо перепроверить
| Что | Где задаётся | Правило |
|---|---|---|
| `resource_name` всех ресурсов | `resources.tf` | уникально **внутри своего сервиса**: провайдер ищет инстанс по имени, и при `adopt_existing_on_create = true` займёт существующий, а не создаст новый |
| `domain` приложений | `resources.tf` (`access_configuration` / `domain`) | задаётся **не полный домен, а имя**: суффикс платформа добавляет сама (`maria-crud-flask` → `maria-crud-flask.pythonk8s.dev.nubes.ru`). Домены должны быть уникальными |
| `s3_uid` | `backup_configuration` | имя **или** UUID экземпляра S3 (в примере `naeel-s3`). Имя резолвится провайдером в UUID; если такого экземпляра на стенде нет — платформа упадёт с `invalid input syntax for type uuid` |
| `registry_path` | `nubes_http` | **полный** путь до образа: `<реестр>/<namespace>/<образ>:<тег>`. Короткий путь (без хоста реестра) платформа не найдёт |
| `cruduser` / `crudpass` | `nubes_mariadb_user` | логин и пароль БД задаются здесь; логин — см. «Особенности» (без `_`) |
### Можно не задавать (есть значения в манифесте)
`cluster_configuration` кластера: `cpu=500` мили-ядер (1000 = 1 vCPU), `memory=1024` МБ, `replicas=1`,
`disk=1` ГБ;
`backup_configuration`: `retain=24` (часы), `schedule="0 0 * * *"`; `master_ip_space="no-needed"`
(внешний доступ не нужен), `master_access_list=["10.0.0.0/8"]`; версия MariaDB `9.4.0`.
Размеры приложений: Lucee `300` мК / `512` МБ, Flask и Node.js `300` мК / `256` МБ,
HTTP-проверка `100` мК / `256` МБ, `1` экземпляр.
## Код приложений
| Приложение | Источник |
|---|---|
| Lucee (CFML) | `https://gitea.services.ngcloud.ru/terraform/maria_lucee.git` |
| Flask (Python) | `https://gitea.services.ngcloud.ru/terraform/maria_flask.git` |
| Node.js (Express) | `https://gitea.services.ngcloud.ru/terraform/maria_nodejs.git` |
| HTTP-проверка | образ `gitea.services.ngcloud.ru/terraform/mariadb-check:0.1` |
Пути задаются в `resources.tf` (`app_configuration.git_path` / `registry_path`) — платформа сама
клонирует код и собирает приложения, вручную собирать и заливать не нужно (кроме образа
HTTP-проверки: он уже должен быть в реестре).
Каждое приложение получает в `json_env` переменную `SERVICE_NAME` (`lucee` / `flask` / `nodejs`) —
это значение колонки `created_by`, чтобы было видно, кто добавил строку.
## Провайдер
Так он объявлен в `main.tf`:
```hcl
terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "3.1.0"
}
}
}
provider "nubes" {
api_token = var.api_token
api_endpoint = "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc"
}
```
## Как проверить, что всё работает
```bash
# 1. HTTP-проверка: процесс жив и база доступна
curl -s https://maria-crud-http.containerk8s.dev.nubes.ru/healthz # {"status":"ok"}
curl -s https://maria-crud-http.containerk8s.dev.nubes.ru/readyz # {"service":"maria-http","status":"ready","database":"connected"}
```
2. Открыть любое приложение в браузере (адреса — в разделе «Быстрый старт»), добавить запись
и убедиться, что она появилась в двух других приложениях и в колонке «КЕМ» указано, какое
приложение её добавило.
## Повседневные операции
| Задача | Команда |
|---|---|
| Изменить/передеплоить стенд | `terraform apply` |
| Остановить стенд | `terraform destroy` — кластер переводится в состояние `Suspend` (под останавливается, ресурсы не потребляются, данные на диске сохраняются); приложения выключаются |
> При `destroy` кластер **не удаляется**: срабатывает `suspend` (у кластера `keep_on_destroy` не задан,
> действует `suspend_on_destroy = true`) — кластер переходит в состояние `Suspend` (остановлен,
> данные не удаляются). Пользователь и база остаются внутри кластера —
> у них `keep_on_destroy = true`, поэтому `delete_user` / `delete_database` не вызываются.
> Повторный `apply` возвращает кластер из состояния `Suspend` (усыновление + `resume`), содержимое сохраняется.
### Если операция завершилась ошибкой
Платформа работает асинхронно, поэтому и `apply`, и `destroy` изредка могут падать по причинам,
не связанным с конфигурацией: таймаут до кластера Kubernetes (`Kubernetes cluster unreachable`,
`TLS handshake timeout`), сбой аутентификации платформы (IAM) и др. В таких случаях просто
повторите команду — обычно она проходит.
Проверить, что записано в состоянии Terraform и что реально создано:
```bash
terraform state list # какие ресурсы Terraform считает созданными
terraform plan # что Terraform собирается изменить (без реальных изменений)
```
Состояние автоматически перезаписывается только при `apply`; `plan` показывает разницу «на бумаге»,
ничего не меняя.
### Если нужно удалить пользователя или базу
`keep_on_destroy = true` означает «при destroy не удалять», поэтому удаление блока из файла
само по себе объект **не** удалит (провайдер возьмёт значение из состояния). Порядок такой:
1. у нужного подресурса поставить `keep_on_destroy = false`, выполнить `terraform apply`
(в облаке ничего не меняется — это только флаг жизненного цикла);
2. убрать блок ресурса из `resources.tf`, выполнить `terraform apply` → сработает
`delete_user` / `delete_database`.
Альтернатива одним действием — удалить объект вручную в ЛК (операция `delete_user`).
## Файлы
| Файл | Что делает |
|---|---|
| `main.tf` | провайдер и переменные (`api_token`, `realm`) |
| `resources.tf` | все ресурсы стенда: кластер, база, пользователь, три приложения, HTTP-проверка |
| `terraform.tfvars.example` | шаблон для копирования в `terraform.tfvars` (сам `terraform.tfvars` — в `.gitignore`) |
## Особенности этого примера (важные грабли)
- **Логин БД — без подчёркиваний.** Формально regex платформы `_` допускает, но имя k8s-объекта
`Grant` собирается из логина, а `_` ломает RFC 1123 (`metadata.name: Invalid value`). Дефис тоже
нельзя — его не пропускает сам regex. Поэтому `cruduser`, а не `crud_user`.
- **Креды берутся из подресурса `nubes_mariadb_user`, а не из `vault_secrets` кластера.** Внутри
одного `apply` `vault_secrets` кластера ещё пуст, и ссылка на него падает с `Invalid index`.
- **`retain` — в часах, а не в днях**, и платформа требует минимум `24`. При суточном расписании
меньшие значения оставляют базу без валидной копии бэкапа.
- **Версия MariaDB** — только `8.4.0` или `9.4.0` (версию можно только повышать).
- **`db_name` пользователя** — не строка, а JSON-массив существующих баз:
`jsonencode([{ dbname = "maria" }])` = полный доступ к базе `maria`. База должна существовать
заранее, поэтому у пользователя есть `depends_on` на `nubes_mariadb_database`.
- **`registry_path` — полный путь** до образа (`<реестр>/<namespace>/<образ>:<тег>`); короткий
путь уходит в API как есть и платформой не находится.
- **`realm`** должна существовать в ЛК (см. «Обязательные значения»); при этом список
`/resourceRealms/available` может показывать её как доступную, хотя объекта платформы нет.
- **4-байтовые символы (эмодзи) не сохраняются.** Таблицу `crud_items` создаёт то приложение,
которое стартовало первым; у Lucee и Flask в `CREATE TABLE` не задан `DEFAULT CHARSET`
(utf8mb4 задан только у Node.js), поэтому колонка `value` может получиться в utf8mb3 и вставка
эмодзи падает с `Incorrect string value`. Обычный набор (латиница, кириллица, спецсимволы)
работает.
- **`adopt_existing_on_create = true`** у всех ресурсов: если инстанс с таким именем уже есть
(например после `destroy`, который приостанавливает, а не удаляет), провайдер его **усыновит**,
а не упадёт с «ресурс с таким именем уже существует».
- **SQL-инъекции закрыты**: во всех приложениях значения идут параметрами (`cfqueryparam`,
`%s`, `?`), а единственная подстановка в текст запроса — имя таблицы из переменной окружения,
проверенное регуляркой.