# 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 | | | Flask | | | Node.js | | | HTTP-проверка | | Добавьте запись в одном приложении — она появится в двух других (таблица `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` | **полный** путь до образа: `<реестр>//<образ>:<тег>`. Короткий путь (без хоста реестра) платформа не найдёт | | `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` — полный путь** до образа (`<реестр>//<образ>:<тег>`); короткий путь уходит в 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`, `?`), а единственная подстановка в текст запроса — имя таблицы из переменной окружения, проверенное регуляркой.