From 0832d7e0cb91b140364f64cb4a8dd8137c8b3f29 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sun, 11 Oct 2026 14:20:13 +0300 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20Terraform-=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=20IoT=20(nubes=5Fhttp:=20EMQX=20+=20iot-service)=20?= =?UTF-8?q?=E2=80=94=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B0=D0=B9=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=20nubes-test=203.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/terraform/.gitignore | 7 ++ deploy/terraform/.terraform.lock.hcl | 13 +++ deploy/terraform/README.md | 52 +++++++++++ deploy/terraform/main.tf | 21 +++++ deploy/terraform/outputs.tf | 16 ++++ deploy/terraform/resources.tf | 83 +++++++++++++++++ deploy/terraform/variables.tf | 132 +++++++++++++++++++++++++++ 7 files changed, 324 insertions(+) create mode 100644 deploy/terraform/.gitignore create mode 100644 deploy/terraform/.terraform.lock.hcl create mode 100644 deploy/terraform/README.md create mode 100644 deploy/terraform/main.tf create mode 100644 deploy/terraform/outputs.tf create mode 100644 deploy/terraform/resources.tf create mode 100644 deploy/terraform/variables.tf diff --git a/deploy/terraform/.gitignore b/deploy/terraform/.gitignore new file mode 100644 index 0000000..d939de1 --- /dev/null +++ b/deploy/terraform/.gitignore @@ -0,0 +1,7 @@ +# Terraform state и секреты — НЕ коммитить (.terraform.lock.hcl — коммитим) +.terraform/ +*.tfstate +*.tfstate.* +terraform.tfvars +*.tfvars +crash.log diff --git a/deploy/terraform/.terraform.lock.hcl b/deploy/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..f09ae30 --- /dev/null +++ b/deploy/terraform/.terraform.lock.hcl @@ -0,0 +1,13 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes" { + version = "3.1.0" + constraints = "3.1.0" + hashes = [ + "h1:grJn9gwqsPZu5THrt3ZfmOYstRK2N/Fe3Czmf5fwy5k=", + "zh:530e741fae503f959ce5d7e26a4488cbeee7f2294786edae1af6e807afe1d1f8", + "zh:65fda474cc7abc24a0053638088abcc188ff69ded2c56a0a1d3d4fab2bbaf355", + "zh:bf039029d7fb67bfeb24b693ca791993255e21be062a1f69b913ae651bcaf67f", + ] +} diff --git a/deploy/terraform/README.md b/deploy/terraform/README.md new file mode 100644 index 0000000..020628a --- /dev/null +++ b/deploy/terraform/README.md @@ -0,0 +1,52 @@ +# IoT на Nubes через Terraform + +Разворачивает IoT-стек двумя HTTP-контейнерами провайдера `nubes-test` (ресурс `nubes_http`): + +- `nubes_http.iot_emqx` — EMQX 5.5.1 (PG-auth) → `gitea.services.ngcloud.ru/nail/iot-emqx:v0.2.4` +- `nubes_http.iot_service` — монолит `iot-service` → `.../iot-service:v0.1.13` (bridge + consumer + api) + +`iot-service` создаётся **после** EMQX (`depends_on`), подключается к shared-SQS (`sqs.sinefo.ru`), +managed PostgreSQL (инстанс `foriot`) и EMQX по внутреннему адресу. + +> Источник провайдера: `tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes` (TEST-стенд), +> версия `3.1.0`. Референсный пример — `~/TF/tf_provider/TEST_STAND/Maria_CRUD`. + +## Предусловия (готово на 2026-10-11) + +- Образы в Gitea: `nail/iot-emqx:v0.2.4`, `nail/iot-service:v0.1.13`. +- PG (инстанс `foriot`): user `iotservice` (ddl_user → CREATEDB+CREATEROLE) + БД `iotdb`. +- shared-SQS (тенант `demo-service`): очереди `iot-telemetry` + `iot-telemetry-dlq`. + +## Запуск + +```bash +cd deploy/terraform +cp terraform.tfvars.example terraform.tfvars # заполнить секреты +terraform init +terraform plan +terraform apply # ← запускает ВЛАДЕЛЕЦ (не агент) +``` + +## Проверка + +```bash +terraform output iot_service_url # → https:///health +terraform output iot_mqtt_broker_url # → wss:///mqtt +curl -s https:///health # {"status":"ok",...} +curl -s https:///mqtt # 400 (норма: Cowboy без subprotocol) +``` + +В логах `iot-service` (ссылка в ЛК): `bridge: MQTT connected` → `bridge: subscribed` → +`consumer: SQS queue resolved`. + +## Нюансы + +- **`MQTT_HOST`**: по умолчанию вычисляется как `..svc.cluster.local`. + Если при первом apply адрес окажется другим — задать `mqtt_host` в `terraform.tfvars` (взять из ЛК). + `ws://` внутри кластера (порт `8083`, путь `/mqtt`). +- **`realm`**: ресурсная платформа K8s; должна существовать в ЛК, иначе + «Ресурсной платформы '' нету в Личном кабинете». Сверить у PG-инстанса. +- **Имя PG-пользователя**: платформенный regex `^[A-Za-z0-9]+$` — подчёркивание нельзя (`iotservice`, не `iot_service`). +- **`destroy` усыпляет** (`suspend_on_destroy=true`), не удаляет: удаление усыплённого — вручную в ЛК через 14 дней. +- **`reconcile`/`redeploy`** из Terraform недоступны (только ЛК). +- Секреты: `terraform.tfvars` и `*.tfstate` в `.gitignore` — НЕ коммитить. diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf new file mode 100644 index 0000000..735f699 --- /dev/null +++ b/deploy/terraform/main.tf @@ -0,0 +1,21 @@ +# main.tf — IoT на платформе Nubes через Terraform (провайдер nubes-test). +# +# Разворачивает два HTTP-контейнера: EMQX (первым) и iot-service (вторым). +# Образы — из Gitea container registry (gitea.services.ngcloud.ru/nail). +# +# Стенд: TEST. Для DEV/PROD менять source провайдера (namespace) + api_endpoint + токен. +# См. TEST_STAND/Maria_CRUD — референсный пример (nubes_http, nubes_mariadb). + +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 = var.api_endpoint +} diff --git a/deploy/terraform/outputs.tf b/deploy/terraform/outputs.tf new file mode 100644 index 0000000..e5af693 --- /dev/null +++ b/deploy/terraform/outputs.tf @@ -0,0 +1,16 @@ +# outputs.tf — адреса развёрнутых контейнеров. + +output "emqx_url" { + value = try(nubes_http.iot_emqx.state_out.connectionUrl, "") + description = "Публичный адрес EMQX (wss MQTT)" +} + +output "iot_service_url" { + value = try(nubes_http.iot_service.state_out.connectionUrl, "") + description = "Публичный адрес iot-service (REST/health)" +} + +output "iot_mqtt_broker_url" { + value = "wss://${var.emqx_domain}/mqtt" + description = "MQTT broker URL для клиентов/консоли" +} diff --git a/deploy/terraform/resources.tf b/deploy/terraform/resources.tf new file mode 100644 index 0000000..460e29d --- /dev/null +++ b/deploy/terraform/resources.tf @@ -0,0 +1,83 @@ +# resources.tf — два HTTP-контейнера: EMQX (первым) и iot-service (вторым). +# Образы: gitea.services.ngcloud.ru/nail/{iot-emqx,iot-service}. +# Провайдер nubes_http: docs-rendered/http.md. +# +# Порядок важен: iot-service подключается к EMQX (MQTT) → depends_on. + +locals { + # Внутренний адрес EMQX-контейнера (для bridge внутри кластера). + # Формат Nubes: ..svc.cluster.local. + # Если окажется иным — переопредели var.mqtt_host (см. ЛК → инстанс EMQX). + emqx_internal_host = "${nubes_http.iot_emqx.resource_name}.${nubes_http.iot_emqx.state_out.instanceUid}.svc.cluster.local" + mqtt_host = var.mqtt_host != "" ? var.mqtt_host : local.emqx_internal_host +} + +# ── EMQX (создаётся ПЕРВЫМ) ── +resource "nubes_http" "iot_emqx" { + resource_name = "iot-emqx" + resource_realm = var.realm + + domain = var.emqx_domain + registry_path = "${var.registry}/iot-emqx:${var.iot_emqx_version}" + + resource_cpu = 1000 + resource_memory = 1024 + resource_instances = 1 + + # EMQX auth/ACL — напрямую в PostgreSQL (таблица iot_devices). + # ⚠ НЕ задавать ...__URL (HTTP-auth старой схемы — уронит под). + json_env = jsonencode({ + EMQX_AUTHENTICATION__1__SERVER = "${var.pg_host}:${var.pg_port}" + EMQX_AUTHENTICATION__1__DATABASE = var.pg_db + EMQX_AUTHENTICATION__1__USERNAME = var.pg_user + EMQX_AUTHENTICATION__1__PASSWORD = var.pg_password + EMQX_AUTHORIZATION__SOURCES__1__SERVER = "${var.pg_host}:${var.pg_port}" + EMQX_AUTHORIZATION__SOURCES__1__DATABASE = var.pg_db + EMQX_AUTHORIZATION__SOURCES__1__USERNAME = var.pg_user + EMQX_AUTHORIZATION__SOURCES__1__PASSWORD = var.pg_password + EMQX_LOG__CONSOLE_HANDLER__LEVEL = "warning" + }) + + adopt_existing_on_create = true +} + +# ── iot-service (создаётся ВТОРЫМ, после EMQX) ── +resource "nubes_http" "iot_service" { + resource_name = "iot-service" + resource_realm = var.realm + + domain = var.iot_domain + registry_path = "${var.registry}/iot-service:${var.iot_service_version}" + + resource_cpu = 200 + resource_memory = 512 + resource_instances = 1 + + json_env = jsonencode({ + IOT_PG_DSN = "postgresql://${var.pg_user}:${var.pg_password}@${var.pg_host}:${var.pg_port}/${var.pg_db}?sslmode=require" + + SQS_ENDPOINT = var.sqs_endpoint + SQS_ACCESS_KEY = var.sqs_access_key + SQS_SECRET_KEY = var.sqs_secret_key + SQS_QUEUE_NAME = var.sqs_queue_name + SQS_DLQ_NAME = var.sqs_dlq_name + SQS_REGION = "us-east-1" + + MQTT_HOST = local.mqtt_host + MQTT_PORT = "8083" + MQTT_WS_PATH = "/mqtt" + MQTT_USERNAME = var.mqtt_username + MQTT_PASSWORD = var.mqtt_password + + JWT_HMAC_SECRET = var.jwt_hmac_secret + ADMIN_STATS_TOKEN = var.admin_stats_token + AUTH_TEST_MODE = "false" + LOG_LEVEL = "info" + API_PORT = "9090" + }) + + # iot-service стартует только после создания EMQX + depends_on = [nubes_http.iot_emqx] + + adopt_existing_on_create = true +} diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf new file mode 100644 index 0000000..f73dfff --- /dev/null +++ b/deploy/terraform/variables.tf @@ -0,0 +1,132 @@ +# variables.tf — входные переменные стека IoT. +# Значения — в terraform.tfvars (не коммитится, см. .gitignore). + +variable "api_token" { + type = string + sensitive = true + description = "Токен Nubes API (test.token)" +} + +variable "api_endpoint" { + type = string + default = "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc" + description = "Endpoint Nubes API (TEST). Менять вместе с source провайдера" +} + +variable "realm" { + type = string + description = "Ресурсная платформа K8s (должна существовать в ЛК; у PG Инстанса foriot — iot-naeel)" +} + +# ── Домены (короткое имя → платформа создаёт DNS в зоне realm; FQDN → проверка A-записи) ── +variable "emqx_domain" { + type = string + default = "exqx-iot" + description = "Домен EMQX (wss MQTT)" +} + +variable "iot_domain" { + type = string + default = "iot-api" + description = "Домен iot-service (REST)" +} + +# ── Реестр ── +variable "registry" { + type = string + default = "gitea.services.ngcloud.ru/nail" + description = "Gitea container registry (namespace nail)" +} + +variable "iot_service_version" { + type = string + default = "v0.1.13" +} + +variable "iot_emqx_version" { + type = string + default = "v0.2.4" +} + +# ── PostgreSQL (managed, инстанс foriot 509145c3) ── +variable "pg_host" { + type = string + default = "postgresqlk8s-master.509145c3-ba71-4687-a084-d9dcbc88d3d2.svc.cluster.local" +} + +variable "pg_port" { + type = string + default = "5432" +} + +variable "pg_db" { + type = string + default = "iotdb" +} + +variable "pg_user" { + type = string + default = "iotservice" + description = "PG-пользователь (платформенный regex ^[A-Za-z0-9]+$ — подчёркивание нельзя)" +} + +variable "pg_password" { + type = string + sensitive = true +} + +# ── shared-SQS (тенант demo-service) ── +variable "sqs_endpoint" { + type = string + default = "https://sqs.sinefo.ru" + description = "Публичный endpoint shared-sqs (на ВМ 213, не в кластере)" +} + +variable "sqs_access_key" { + type = string + sensitive = true +} + +variable "sqs_secret_key" { + type = string + sensitive = true +} + +variable "sqs_queue_name" { + type = string + default = "iot-telemetry" +} + +variable "sqs_dlq_name" { + type = string + default = "iot-telemetry-dlq" +} + +# ── MQTT (bridge iot-service → EMQX) ── +variable "mqtt_username" { + type = string + default = "iot-bridge" +} + +variable "mqtt_password" { + type = string + sensitive = true +} + +variable "mqtt_host" { + type = string + default = "" + description = "Внутренний адрес EMQX. Пусто → вычисляется из state_out EMQX (проверить в ЛК при первом apply)" +} + +# ── JWT API ── +variable "jwt_hmac_secret" { + type = string + sensitive = true +} + +variable "admin_stats_token" { + type = string + sensitive = true + default = "" +}