Files
SQS-service/doc/2026-08-13-migration-to-managed.md
T

9.9 KiB
Raw Blame History

Миграция 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

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 зависимости

{
  "dependencies": {
    "express": "^4",
    "aedes": "^0.51",
    "websocket-stream": "^5",
    "@aws-sdk/client-sqs": "^3",
    "pg": "^8",
    "uuid": "^9"
  }
}