223 lines
9.9 KiB
Markdown
223 lines
9.9 KiB
Markdown
# Миграция 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"
|
||
}
|
||
}
|
||
```
|