# Миграция IoT из Kubernetes → Managed Services (Nubes) ## Сессия 2026-08-13 --- ## 1. Что изучили — текущая архитектура ### Стек - **Язык:** Go 1.25 - **Паттерн:** Kubernetes Operator (controller-runtime) - **Модуль:** `gitea.services.ngcloud.ru/Nail/IoT` - **Docker Hub:** `naeel/iot-operator:v0.2.6` ### Три бинарника в одном образе | Бинарник | Роль | |---|---| | `cmd/iot-operator` | Controller-manager (CRD IoTDevice) + REST API :9090 | | `cmd/mqtt-bridge` | MQTT (EMQX) → shared-SQS (AWS SDK v2) | | `cmd/sqs-consumer` | shared-SQS → per-tenant Postgres | ### Поток данных (текущий) ``` IoT Device (MQTT CONNECT, username="{ns}_{deviceId}") ↓ EMQX 5.5.1 (HTTP auth/acl → iot-operator:9090/internal/mqtt/auth) ↓ MQTT PUBLISH "{ns}/telemetry/{deviceId}" iot-mqtt-bridge (подписка "+/telemetry/+") ↓ AWS SDK SQS SendMessage shared-SQS (namespace shared-sqs, очередь "iot-telemetry") ↓ long polling (WaitTimeSeconds=20) iot-sqs-consumer (at-least-once, DeleteMessage после успешной записи) ↓ Managed PostgreSQL 17 (per-tenant DB: tenant_{namespace}) ↓ REST API (iot-operator:9090) → Пользователь (UI/Terraform) ``` ### Ключевые компоненты k8s - **CRD IoTDevice** (`iot.kube5s.ru/v1alpha1`) — регистрация устройств - **k8s Secret** `iot-{deviceId}` — хранит MQTT пароль (64 hex, crypto/rand) - **OwnerReference** — каскадное удаление Secret при удалении IoTDevice - **RBAC** — ClusterRole для чтения/записи IoTDevice + Secrets - **Namespace = ID тенанта** — изоляция устройств и данных ### Мультитенантность - MQTT topic изоляция: `{ns}/telemetry/{deviceId}` — ACL на уровне EMQX - Per-tenant Postgres DB: `tenant_{namespace}` (дефисы → подчёркивания) - MQTT username: `{namespace}_{deviceId}` — глобально уникален - `sync.Map` кэш `*sql.DB` per tenant — lazy init ### Наблюдения / потенциальные проблемы 1. **`authTestMode = true`** в `middleware/auth.go` — тестовый режим включён в проде, JWT подпись не проверяется 2. **`ADMIN_STATS_TOKEN = "iot-admin-2026"`** захардкожен в YAML манифесте вместо Secret 3. Устаревшие файлы: `deployments/k8s/kafka.yaml`, `iot-kafka-consumer.yaml` (Kafka удалена, манифесты остались) 4. Bridge clientID `"sless-iot-bridge"` захардкожен и в bridge коде и в ACL логике 5. `examples/main.tf` и `handler.py` описывают устаревший поток (RabbitMQ/serverless) --- ## 2. Почему хотим уйти из k8s - Цель: использовать только **managed services** облака Nubes - Nubes предоставляет: **Managed Node.js, Flask, Lucee, PostgreSQL** - Никаких VPS, никакого k8s — только managed-платформа - SQS тоже **наш собственный сервис** (не сторонний облачный), работает сейчас в k8s, тоже надо вынести --- ## 3. Принятые решения ### Node.js — выбранный стек | Задача | Node.js | |---|---| | REST API | Express/Fastify | | MQTT-брокер | `aedes` (embedded, over WebSocket) | | MQTT-клиент для publish | `mqtt` npm | | SQS клиент | `@aws-sdk/client-sqs` | | Postgres | `pg` npm | **Почему не Flask:** сложнее держать persistent MQTT и SQS polling в фоне **Почему не Lucee:** не подходит для long-running background workers **Почему Node.js — монолит:** bridge и consumer нельзя масштабировать независимо (один MQTT-клиент = одна подписка), смысла разделять нет ### EMQX → aedes (embedded в Node.js) - Managed Node.js открывает только HTTP/HTTPS порты - MQTT over WebSocket = HTTP upgrade → работает на любой managed-платформе - Устройства подключаются через `wss://` (уже сейчас так, через ingress emqx-ws-ingress.yaml) - **aedes** — полноценный MQTT-брокер на Node.js, встраивается в Express HTTP-сервер - Auth/ACL становится обычной функцией внутри того же процесса (быстрее, проще) ### Мультитенантность сохраняется полностью - `namespace` — просто строка в таблице `iot_devices` вместо k8s namespace - Per-tenant Postgres DB остаётся (чистый SQL, без k8s) - MQTT topic изоляция остаётся (`{ns}/telemetry/{deviceId}`) - ACL по топику остаётся — просто функция вместо HTTP endpoint ### k8s CRD/Secret → таблица в Postgres ```sql CREATE TABLE iot_devices ( namespace TEXT NOT NULL, name TEXT NOT NULL, device_id TEXT NOT NULL, enabled BOOLEAN NOT NULL DEFAULT true, mqtt_password TEXT NOT NULL, -- было в k8s Secret metadata JSONB, phase TEXT, created_at TIMESTAMPTZ DEFAULT now(), PRIMARY KEY (namespace, device_id) ); ``` --- ## 4. Итоговая архитектура на Nubes Managed ``` IoT Device (wss://iot.example.ru/mqtt) ↓ Managed Node.js — IoT сервис ├── aedes MQTT-брокер (over WebSocket, порт :3000/mqtt) │ auth/acl → функция → таблица iot_devices в PG │ on publish → SQS SendMessage ├── SQS consumer (long polling, фоновый setInterval/async loop) │ → per-tenant Postgres (tenant_{namespace}) └── REST API (Express) GET/POST /v1/namespaces/{ns}/iot/devices GET /v1/namespaces/{ns}/iot/telemetry POST /internal/mqtt/auth (совместимость, опционально) GET /console (embedded HTML) GET /iot-admin/stats ↓ Managed Node.js — shared-SQS сервис ← другие сервисы тоже ↓ Managed PostgreSQL — IoT данные ``` ### Два managed Node.js сервиса | Сервис | Назначение | |---|---| | **shared-SQS** | AWS SQS-совместимая очередь, multi-tenant, HTTP API | | **iot-service** | aedes MQTT + REST API + SQS consumer + PG | --- ## 5. Порядок миграции ### ⚠️ Сначала shared-SQS, потом IoT **Причина:** IoT зависит от SQS. SQS независим — мигрирует первым. ``` Шаг 1: Вынести shared-SQS из k8s → Managed Node.js на Nubes Шаг 2: Переписать IoT сервис на Node.js (aedes + REST + consumer) Шаг 3: Деплой IoT на Managed Node.js на Nubes Шаг 4: Отключить k8s деплой IoT ``` --- ## 6. Детали для нового чата — shared-SQS миграция ### Что сейчас - SQS сервис живёт в `namespace: shared-sqs` в кластере `iot-naeel` - Endpoint: `https://qu.kube5s.ru` - Multi-tenant: tenant `iot-service` (id: `t-96afe7e9f781f6ca`), очередь `iot-telemetry` - IoT использует: `SQS_ENDPOINT=https://qu.kube5s.ru`, `SQS_ACCESS_KEY`, `SQS_SECRET_KEY` - Протокол: AWS SQS-совместимый (SendMessage, ReceiveMessage, DeleteMessage, GetQueueUrl, GetQueueAttributes) ### Что нужно от нового SQS сервиса - AWS SQS-совместимый HTTP API (те же методы что сейчас) - Multi-tenant (разные access key / secret key для разных тенантов) - Очереди создаются по имени (`GetQueueUrl` + `CreateQueue`) - Long polling: `ReceiveMessage` с `WaitTimeSeconds` до 20 - Хранение сообщений: in-memory или Postgres/Redis ### Клиенты SQS в IoT коде 1. **mqtt-bridge** (`cmd/mqtt-bridge/main.go`): `SendMessage` при каждом MQTT сообщении 2. **sqs-consumer** (`cmd/sqs-consumer/main.go`): `ReceiveMessage` (polling) + `DeleteMessage` 3. **iot-admin-stats** (`internal/api/handler/iot_admin_stats_handler.go`): `GetQueueAttributes` для мониторинга ### Env vars для IoT → SQS ``` SQS_ENDPOINT=https://qu.kube5s.ru (поменяется на новый managed URL) SQS_ACCESS_KEY=... SQS_SECRET_KEY=... SQS_QUEUE_NAME=iot-telemetry (default) SQS_REGION=us-east-1 (default, не важен для self-hosted) ``` --- ## 7. Что переписывается в IoT (Node.js) ### Соответствие Go → Node.js | Go файл | Node.js файл | |---|---| | `cmd/iot-operator/main.go` | `src/index.js` (точка входа) | | `internal/api/router.go` | `src/routes.js` | | `internal/api/handler/iot_device_handler.go` | `src/handlers/devices.js` | | `internal/api/handler/iot_telemetry_handler.go` | `src/handlers/telemetry.js` | | `internal/api/handler/iot_admin_stats_handler.go` | `src/handlers/admin.js` | | `internal/api/middleware/auth.go` | `src/middleware/auth.js` | | `internal/storage/iotpg/iot_telemetry_store.go` | `src/storage/pg.js` | | `cmd/mqtt-bridge/main.go` | встроен в `src/mqtt.js` (aedes) | | `cmd/sqs-consumer/main.go` | встроен в `src/sqsWorker.js` | | `controllers/iotdevice_controller.go` | **не нужен** (заменён CRUD в БД) | | `api/v1alpha1/device_types.go` | **не нужен** (таблица iot_devices) | ### npm зависимости ```json { "dependencies": { "express": "^4", "aedes": "^0.51", "websocket-stream": "^5", "@aws-sdk/client-sqs": "^3", "pg": "^8", "uuid": "^9" } } ```