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 его тоже не меняли.
This commit is contained in:
Repinoid
2026-10-03 09:27:36 +03:00
parent 7d7fdb46ff
commit 15397298de
3 changed files with 259 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
# 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`, `?`), а единственная подстановка в текст запроса — имя таблицы из переменной окружения,
проверенное регуляркой.
+32
View File
@@ -0,0 +1,32 @@
# Требования к провайдеру. Источник — Terraform Registry платформы (namespace nubes-test = TEST-стенд),
# версия 3.1.0 (ломающе: postgres_conf → map-fixed; gate по isCreated — недосозданный
# инстанс не пишется в state). Менять namespace вместе со стендом: nubes / nubes-dev / nubes-test.
terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "3.1.0"
}
}
}
# Токен Nubes API. Значение задаётся в terraform.tfvars (файл в .gitignore) и не коммитится.
variable "api_token" {
type = string
sensitive = true
description = "Nubes API token"
}
# Ресурсная платформа (кластер K8s), в которой создаётся ВСЁ: и кластер MariaDB, и БД, и приложения.
# Значение должно совпадать у всех ресурсов стенда и существовать в ЛК, иначе получите:
# «Ресурсной платформы '<realm>' нету в Личном кабинете. Не удалось получить kubeconfig».
variable "realm" {
type = string
description = "Resource platform shared by the MariaDB cluster and its apps"
}
# api_endpoint — TEST-стенд. Для DEV/PROD менять вместе с source провайдера и токеном.
provider "nubes" {
api_token = var.api_token
api_endpoint = "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc"
}
+9
View File
@@ -0,0 +1,9 @@
# Шаблон для terraform.tfvars (сам terraform.tfvars в .gitignore — токен не коммитить).
# Токен Nubes API для TEST-стенда (secrets/test.token).
api_token = ""
# Ресурсная платформа (кластер K8s) — одна и та же для БД и всех приложений.
# Список доступных можно получить через API: GET /resourceRealms/available?svcId=115
# или посмотреть в ЛК в разделе кластеров/ресурсных платформ.
realm = "k8s-4-sandbox-nubes-ru"