Взят пользовательский набор отслеживаемых 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 его тоже не меняли.
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 и
второй каталог не нужны.
Быстрый старт
# 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» |
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:
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"
}
Как проверить, что всё работает
# 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"}
- Открыть любое приложение в браузере (адреса — в разделе «Быстрый старт»), добавить запись и убедиться, что она появилась в двух других приложениях и в колонке «КЕМ» указано, какое приложение её добавило.
Повседневные операции
| Задача | Команда |
|---|---|
| Изменить/передеплоить стенд | 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 и что реально создано:
terraform state list # какие ресурсы Terraform считает созданными
terraform plan # что Terraform собирается изменить (без реальных изменений)
Состояние автоматически перезаписывается только при apply; plan показывает разницу «на бумаге»,
ничего не меняя.
Если нужно удалить пользователя или базу
keep_on_destroy = true означает «при destroy не удалять», поэтому удаление блока из файла
само по себе объект не удалит (провайдер возьмёт значение из состояния). Порядок такой:
- у нужного подресурса поставить
keep_on_destroy = false, выполнитьterraform apply(в облаке ничего не меняется — это только флаг жизненного цикла); - убрать блок ресурса из
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кластера. Внутри одногоapplyvault_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,?), а единственная подстановка в текст запроса — имя таблицы из переменной окружения, проверенное регуляркой.