diff --git a/doc/2026-08-13-migration-to-managed.md b/doc/2026-08-13-migration-to-managed.md new file mode 100644 index 0000000..a012ee6 --- /dev/null +++ b/doc/2026-08-13-migration-to-managed.md @@ -0,0 +1,222 @@ +# Миграция 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" + } +} +``` diff --git a/doc/2026-08-13-migration-to-nubes.md b/doc/2026-08-13-migration-to-nubes.md new file mode 100644 index 0000000..c96b77f --- /dev/null +++ b/doc/2026-08-13-migration-to-nubes.md @@ -0,0 +1,56 @@ +# Миграция shared-sqs: k8s → Nubes Managed +## Сессия 2026-08-13 + +--- + +## Текущая архитектура (k8s) + +- Go-бинарник, HTTP-сервер, ~50MB RAM +- Deployment в k8s: `naeel/shared-sqs:v0.1.22` +- Redis sidecar — write-through persistence +- Exposed через Ingress + TLS + +## Целевая архитектура (Nubes Managed) + +| Компонент | Сейчас | После | +|---|---|---| +| Рантайм | k8s Deployment | Nubes Simple HTTP Container | +| Persistence | Redis pod в k8s | Nubes Managed Redis | +| Секреты | k8s Secret | Env vars в Container | +| Ingress/TLS | k8s Ingress | встроен в Simple HTTP Container | + +**Код не меняется вообще.** Только инфраструктура. + +## Совместимость + +Сервис протестирован как AWS SQS совместимый — работает с AWS CLI, AWS SDK, Yandex. + +## Открытые вопросы (уточнить перед деплоем) + +1. Managed Redis: TLS или plain endpoint? → влияет на флаг `REDIS_INSECURE` в коде +2. Simple HTTP Container: поддержка custom domain или только `*.containerk8s.services.ngcloud.ru`? + +## План миграции + +**Шаг 1** — Создать Managed Redis на Nubes + +**Шаг 2** — Задеплоить образ в Simple HTTP Container: +``` +image: naeel/shared-sqs:v0.1.22 +PORT=4100 +SHARED_SQS_ADMIN_TOKEN=<секрет> +REDIS_ADDR= +REDIS_USER= +REDIS_PASSWORD= +SHARED_SQS_SEED_DEMO=false +``` + +**Шаг 3** — Проверить работоспособность (AWS CLI / SDK) + +**Шаг 4** — Выключить k8s Deployment + +## Что НЕ нужно делать + +- Переписывать код на Node.js/Flask — не нужно +- Менять Redis persistence layer — не нужно +- Мигрировать данные вручную — Redis восстановит состояние автоматически при старте (LoadAllQueues / LoadAllTenantsRaw)