feat(deploy): Terraform-стек IoT (nubes_http: EMQX + iot-service) — провайдер nubes-test 3.1.0
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
# Terraform state и секреты — НЕ коммитить (.terraform.lock.hcl — коммитим)
|
||||||
|
.terraform/
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.*
|
||||||
|
terraform.tfvars
|
||||||
|
*.tfvars
|
||||||
|
crash.log
|
||||||
Generated
+13
@@ -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",
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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://<iot_domain>/health
|
||||||
|
terraform output iot_mqtt_broker_url # → wss://<emqx_domain>/mqtt
|
||||||
|
curl -s https://<iot_domain>/health # {"status":"ok",...}
|
||||||
|
curl -s https://<emqx_domain>/mqtt # 400 (норма: Cowboy без subprotocol)
|
||||||
|
```
|
||||||
|
|
||||||
|
В логах `iot-service` (ссылка в ЛК): `bridge: MQTT connected` → `bridge: subscribed` →
|
||||||
|
`consumer: SQS queue resolved`.
|
||||||
|
|
||||||
|
## Нюансы
|
||||||
|
|
||||||
|
- **`MQTT_HOST`**: по умолчанию вычисляется как `<resource_name>.<instanceUid>.svc.cluster.local`.
|
||||||
|
Если при первом apply адрес окажется другим — задать `mqtt_host` в `terraform.tfvars` (взять из ЛК).
|
||||||
|
`ws://` внутри кластера (порт `8083`, путь `/mqtt`).
|
||||||
|
- **`realm`**: ресурсная платформа K8s; должна существовать в ЛК, иначе
|
||||||
|
«Ресурсной платформы '<realm>' нету в Личном кабинете». Сверить у PG-инстанса.
|
||||||
|
- **Имя PG-пользователя**: платформенный regex `^[A-Za-z0-9]+$` — подчёркивание нельзя (`iotservice`, не `iot_service`).
|
||||||
|
- **`destroy` усыпляет** (`suspend_on_destroy=true`), не удаляет: удаление усыплённого — вручную в ЛК через 14 дней.
|
||||||
|
- **`reconcile`/`redeploy`** из Terraform недоступны (только ЛК).
|
||||||
|
- Секреты: `terraform.tfvars` и `*.tfstate` в `.gitignore` — НЕ коммитить.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 для клиентов/консоли"
|
||||||
|
}
|
||||||
@@ -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: <resource_name>.<instanceUid>.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
|
||||||
|
}
|
||||||
@@ -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 = ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user