diff --git a/doc/architecture/overview.md b/doc/architecture/overview.md index 8b76c5e..6a31736 100644 --- a/doc/architecture/overview.md +++ b/doc/architecture/overview.md @@ -1,32 +1,74 @@ # Архитектура системы -Последнее обновление: 2026-03-18 (v0.1.34 + funcs-service v0.2.0) +Последнее обновление: 2026-04-04 (IoT telemetry storage architecture decision) ## Общее описание -Managed Serverless Functions Service для облачного провайдера nubes.ru. +Managed Serverless Functions Service + IoT Platform для облачного провайдера nubes.ru. +Два независимых компонента: sless (serverless) и iot (IoT), каждый со своим оператором. Пользователь загружает код через Terraform, сервис его собирает (kaniko) и запускает -по HTTP-триггеру, расписанию (cron) или вручную через one-shot Job. +по HTTP-триггеру, расписанию (cron), вручную или по событию от IoT устройства. + +## Namespace Layout + +``` +namespace: sless — платформа serverless + (sless-operator, event-dispatcher, RabbitMQ, Postgres invocations) +namespace: sless-{hash} — tenant функции (function pods каждого клиента) +namespace: iot — платформа IoT + (iot-operator, EMQX, Postgres telemetry) +namespace: iot-{hash} — tenant IoT ресурсы (IoTDevice CRDs) +``` ## Стек | Компонент | Технология | Где запущен | |-----------|-----------|-------------| -| Operator (API + Controllers) | Go (controller-runtime) | Kubernetes, namespace `sless` | -| funcs-service (глобальная консоль) | Go (net/http) | Kubernetes, namespace `sless` | -| PostgreSQL | PostgreSQL 16 | Kubernetes, namespace `sless` | +| sless-operator (API + Controllers) | Go (controller-runtime) | namespace `sless` | +| iot-operator (API + Controllers) | Go (controller-runtime) | namespace `iot` | +| PostgreSQL (invocations) | PostgreSQL 16 | namespace `sless` | +| PostgreSQL (telemetry) | PostgreSQL 16 | namespace `iot` | +| EMQX | EMQX 5.5.1 | namespace `iot` | +| RabbitMQ | RabbitMQ 3 | namespace `sless` | +| event-dispatcher | Go | namespace `sless` | +| iot-mqtt-bridge | Go | namespace `iot` | | S3 | Ceph (облачный) | `s3.msk-1.ngcloud.ru` | -| Container Registry | DockerHub (`naeel/`) | внешний | +| Container Registry | PearlHarbor (Nubes) | внешний | | Builder | kaniko (k8s Job) | namespace пользователя | -| Функции (HTTP) | k8s Deployment + Service | namespace пользователя | -| Функции (one-shot) | k8s Job | namespace пользователя | -| Функции (cron) | k8s CronJob | namespace пользователя | +| Функции (HTTP) | k8s Deployment + Service | namespace sless-{hash} | +| Функции (one-shot) | k8s Job | namespace sless-{hash} | +| Функции (cron) | k8s CronJob | namespace sless-{hash} | | Terraform Provider | Go (plugin framework v6) | localhost/CI | | nubes API | REST (облако) | `deck-api.ngcloud.ru` | -> Redis и RabbitMQ — отложены до v2. +## IoT Data Flow -## Компонент: funcs-service +``` +IoT устройство + ↓ ws://iot.kube5s.ru:80/mqtt (WebSocket, пока 1883 закрыт) +EMQX (namespace iot) + ↓ ACL: каждое устройство видит только свои топики {ns}/{deviceId}/# +iot-mqtt-bridge + ↓ +RabbitMQ (namespace sless) + ↓ +event-dispatcher + ↓ параллельно: + 1. INSERT INTO tenant_{ns}.iot_telemetry ← автоматически + 2. Вызов serverless function (если настроена) +``` + +## Изоляция данных + +- MQTT: ACL по username → топики только своего устройства +- Postgres: отдельная DATABASE per tenant, разные credentials +- k8s: отдельный namespace per tenant + +## Связь sless ↔ iot + +- Общий идентификатор tenant: `{hash}` в именах namespace +- Коммуникация через RabbitMQ endpoint (не через Go пакеты) +- Loose coupling — могут быть в разных кластерах Глобальный HTTP сервис — **одна копия** на весь кластер, для всех пользователей. diff --git a/doc/decisions/iot-telemetry-storage-2026-04-04.md b/doc/decisions/iot-telemetry-storage-2026-04-04.md new file mode 100644 index 0000000..7faa0bb --- /dev/null +++ b/doc/decisions/iot-telemetry-storage-2026-04-04.md @@ -0,0 +1,174 @@ +# Решение: IoT Telemetry Storage Architecture +# Дата: 2026-04-04 +# Агент: GitHub Copilot (Claude Sonnet 4.6) +# Статус: ПРИНЯТО + +--- + +## Контекст + +IoT платформа принимает данные с датчиков через MQTT. Данные проходят: +EMQX → iot-mqtt-bridge → RabbitMQ → event-dispatcher → function pod. + +Проблема: данные не сохраняются. Функция получает событие и забывает его. +Для клиентов (мониторинг объектов, счётчики, производство) нужно: +- Автоматическое хранение всей телеметрии +- Доступ к историческим данным +- Низкий порог входа — не требовать от клиента настройки БД + +--- + +## Решения + +### 1. Хранилище — Postgres, отдельная DATABASE per tenant + +**Выбрано**: один Postgres инстанс, отдельная DATABASE на каждого клиента. + +**Отклонено**: одна таблица с tenant_id колонкой. +- Причина: изоляция только программная. Ошибка в WHERE → утечка чужих данных. + +**Структура**: +``` +Postgres (StatefulSet в namespace iot) +├── sless_platform — системные данные платформы (tenants, etc) +├── tenant_{hash} — данные клиента A (полная изоляция) +└── tenant_{hash} — данные клиента B (полная изоляция) +``` + +**Безопасность**: +- Каждый tenant имеет свой Postgres USER с уникальным паролем (UUID) +- Пароль генерируется при создании tenant, хранится в k8s Secret +- Клиент B физически не может подключиться к DATABASE клиента A + +--- + +### 2. Доступ клиента — только через REST API + +**Выбрано**: клиент читает телеметрию через REST API платформы. + +**Отклонено**: прямой доступ к Postgres через connection string. +- Причина: Postgres внутри кластера, не должен торчать наружу. Security. + +**API**: +``` +GET /v1/namespaces/{ns}/iot/telemetry + ?device={device_id} + &from={RFC3339} + &to={RFC3339} + &limit={int} + +GET /v1/namespaces/{ns}/iot/devices/{id}/last +``` + +Авторизация — Bearer токен (тот же механизм что и для functions). + +--- + +### 3. Схема таблицы telemetry + +```sql +CREATE TABLE iot_telemetry ( + id BIGSERIAL PRIMARY KEY, + device_id TEXT NOT NULL, + ts TIMESTAMPTZ NOT NULL DEFAULT now(), + payload JSONB NOT NULL +); + +CREATE INDEX idx_iot_telemetry_device_ts + ON iot_telemetry (device_id, ts DESC); +``` + +**Почему JSONB**: у каждого клиента разные наборы данных: +- датчик температуры: `{"temp": 22.5, "humidity": 60}` +- GPS трекер: `{"lat": 55.75, "lon": 37.61, "speed": 60}` +- счётчик воды: `{"liters": 1234.5, "flow": 0.3}` + +Фиксированная схема невозможна. JSONB + индекс по (device_id, ts) даёт +достаточную производительность для малого и среднего бизнеса. + +--- + +### 4. schema.sql при деплое функции + +Клиент может положить `schema.sql` рядом с функцией: +``` +my-function/ +├── handler.py +├── schema.sql ← CREATE TABLE IF NOT EXISTS my_alerts (...) +└── requirements.txt +``` + +При деплое оператор выполняет `schema.sql` в БД tenant'а. +Это позволяет клиентам без знания Python настраивать дополнительные таблицы. + +--- + +### 5. DB_DSN в функцию + +При запуске function pod оператор прокидывает `DB_DSN` из Secret в env var: +``` +DB_DSN=postgresql://tenant_abc:password@iot-postgres.iot.svc:5432/tenant_abc +``` + +Функция использует стандартный драйвер, не знает о деталях платформы. + +--- + +### 6. Разделение sless и iot операторов + +**Решение**: sless-operator и iot-operator — ОТДЕЛЬНЫЕ компоненты с раздельными namespace. + +**Мотивация**: +- В будущем могут быть в разных кластерах +- Независимый деплой и масштабирование +- Разные команды могут владеть компонентами +- Нет cross-dependency в коде (loose coupling) + +**Namespace layout**: +``` +namespace: sless — платформа serverless + (sless-operator, event-dispatcher, RabbitMQ, Postgres invocations) +namespace: sless-{hash} — tenant функции (function pods каждого клиента) + +namespace: iot — платформа IoT + (iot-operator, EMQX, Postgres telemetry) +namespace: iot-{hash} — tenant IoT ресурсы (IoTDevice CRDs) +``` + +**Связь между sless и iot**: +- Общий идентификатор tenant: `{hash}` одинаковый в sless-{hash} и iot-{hash} +- MQTT событие → RabbitMQ в namespace sless → function pod в sless-{hash} +- iot-operator НЕ импортирует Go пакеты sless-operator +- Общение только через k8s API и RabbitMQ endpoints + +**Postgres**: +- sless: отдельный Postgres для invocations логов +- iot: отдельный Postgres для telemetry per-tenant +- Разные StatefulSet, разные PVC, разные credentials + +--- + +### 7. Postgres инстанс для IoT + +**Выбрано**: `postgres:16-alpine` StatefulSet в namespace `iot`. + +**Причина**: простота для разработки. При передаче в production девопсы +заменят на Managed Postgres от Nubes — connection string поменяется, код не меняется. + +**Ресурсы**: +- PVC: 10Gi (начальный размер, увеличивается по мере роста) +- Memory limit: 512Mi +- CPU: 0.5 cores + +--- + +## План реализации + +1. StatefulSet Postgres в namespace `iot` +2. iot-operator: provisioning при создании IoTDevice namespace + - CREATE USER tenant_{ns} PASSWORD '{uuid}' + - CREATE DATABASE tenant_{ns} OWNER tenant_{ns} + - CREATE TABLE iot_telemetry + индекс +3. iot-mqtt-bridge: INSERT telemetry при получении MQTT сообщения +4. iot-operator: REST API `/v1/namespaces/{ns}/iot/telemetry` +5. sless-operator: при деплое function → прокинуть DB_DSN + выполнить schema.sql diff --git a/doc/progress.md b/doc/progress.md index 48d5ad0..c07d283 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -1597,5 +1597,51 @@ G15 перезапущен → **21/21 PASS ✅** | 4 | nginx `client_max_body_size` ограничивает upload → 413 (не настроено явно) | G13F-4 NOTE | ### Версия оператора -`v0.1.51` — задеплоен, работает +`v0.1.52` — задеплоен, работает + +--- + +## 2026-04-04 — IoT WebSocket workaround + MQTT ACL + Архитектура телеметрии + +### Выполнено + +#### MQTT WebSocket workaround (порт 1883 закрыт NSX-T) +- Создан Ingress `emqx-mqtt-websocket`: `iot.kube5s.ru/mqtt → EMQX:8083` +- DNS A-запись `iot.kube5s.ru → 185.247.187.147` создана пользователем +- Отлажена цепочка: убран `configuration-snippet` (заблокирован в nginx v1.12.6), + добавлен `ssl-redirect: false`, `pathType: Exact` +- Тест: `Connected rc=0` через paho-mqtt WebSocket ✅ +- Коммит: `6e3e473` (ветка Ioter) + +#### MQTT ACL изоляция топиков +- Найдена уязвимость: `authorization { no_match = allow }` — любой клиент мог + читать топики других клиентов после успешного CONNECT +- EMQX 5.x: ACL в ответе auth игнорируется (это EMQX 4.x фича) +- Добавлен endpoint `POST /internal/mqtt/acl` в sless-operator +- Обновлён `emqx.conf`: HTTP authorization backend, `no_match = deny` +- Тест: `sless/iot-bridge/#` → ALLOWED, `sless/other-device/telemetry` → DENIED + disconnect +- Лог EMQX: `authorization_permission_denied` ✅ +- Оператор v0.1.52 задеплоен +- Коммит: `b23ae40` (ветка Ioter) + +### Архитектурные решения (обсуждение, не реализовано) + +Принято решение о хранении IoT телеметрии: +- Отдельная DATABASE per tenant в одном Postgres инстансе +- REST API для доступа (не прямой доступ к Postgres) +- JSONB payload (разные данные у разных клиентов) +- Отдельный iot-operator независимо от sless-operator +- schema.sql при деплое функции +- DB_DSN в env var функции + +Подробно: `doc/decisions/iot-telemetry-storage-2026-04-04.md` + +### Следующий этап (ветка iot-pg-telemetry) + +- [ ] Postgres StatefulSet в namespace `iot` +- [ ] Provisioning БД при создании tenant +- [ ] INSERT telemetry из iot-mqtt-bridge +- [ ] REST API чтения телеметрии +- [ ] DB_DSN в function pod env +- [ ] schema.sql при деплое функции diff --git a/doc/thinking/2026-04-04-02.md b/doc/thinking/2026-04-04-02.md index de0ace7..1aaba81 100644 --- a/doc/thinking/2026-04-04-02.md +++ b/doc/thinking/2026-04-04-02.md @@ -102,3 +102,114 @@ listeners.ws.default { ``` Ждём подтверждения от пользователя перед реализацией. + +--- + +## Архитектурная дискуссия — IoT телеметрия и хранение данных + +### Контекст разговора + +Пользователь задал вопрос: "куда пишутся данные с IoT датчиков?" + +Выяснилось что сейчас данные теряются — function pod получает событие но никуда не сохраняет. Это нормально для serverless (пользователь сам решает), но для IoT платформы нужно автоматическое хранение. + +### Анализ сценариев использования + +Реалистичные клиенты для Nubes (облачный провайдер СНГ, малый/средний бизнес): +1. Мониторинг объектов (склады, серверные, торговые точки) — температура, влажность, протечка +2. Умные счётчики / ЖКХ — снятие показаний без выезда +3. Небольшое производство / агро — теплицы, мини-заводы + +Общий паттерн для всех: датчик → данные в БД → алерт если порог → график + +### Решение по хранению данных + +**Вопрос**: один большой Postgres или отдельный на каждого? +**Ответ**: один Postgres инстанс, но отдельная DATABASE на каждого tenant. + +Причины: +- Вариант со одной таблицей + tenant_id — изоляция программная, ошибка в коде = утечка +- Отдельная DATABASE — физическая изоляция, разные connection string, разные пароли +- Клиент B не может подключиться к DATABASE клиента A даже при баге в коде платформы + +Структура: +``` +Postgres инстанс +├── sless_platform DB — системные таблицы (tenants, invocations) +├── tenant_abc DB — только данные клиента A +└── tenant_def DB — только данные клиента B +``` + +### Решение по доступу клиента + +**Вопрос**: давать клиенту прямой доступ к Postgres? +**Ответ**: нет. Только через REST API платформы. + +Причины: +- Postgres внутри кластера, снаружи не торчит (security) +- Единый endpoint `iot.kube5s.ru` +- Легко добавить rate limit, биллинг, кеш +- Клиент не зависит от деталей реализации хранилища + +API: +``` +GET /v1/namespaces/{ns}/iot/telemetry?device=X&from=T&to=T +GET /v1/namespaces/{ns}/iot/devices/{id}/last +``` + +### Решение по schema.sql + +Клиент может положить `schema.sql` рядом с функцией. При деплое платформа выполняет его в БД tenant'а. +Это даёт низкий порог входа — клиент не шарит в Python, но может написать SQL по шаблону. + +### Ключевое архитектурное решение — разделение операторов + +**Решение**: sless-operator и iot-operator — ОТДЕЛЬНЫЕ компоненты. +Пока в одном кластере, но сделать так чтобы могли быть в разных. + +**Namespace layout:** +``` +namespace: sless — платформа sless (operator, event-dispatcher, RabbitMQ, Postgres invocations) +namespace: sless-{hash} — tenant функции (function pods) +namespace: iot — платформа IoT (iot-operator, EMQX, Postgres telemetry) +namespace: iot-{hash} — tenant IoT (IoTDevice CRDs) +``` + +**Связь**: +- Общий идентификатор tenant: `{hash}` одинаковый в обоих namespace +- MQTT событие → RabbitMQ в sless → function pod в sless-{hash} +- IoT operator НЕ импортирует пакеты sless-operator (loose coupling) +- Общение только через k8s API и RabbitMQ + +**Postgres**: +- sless имеет свой Postgres (invocations) +- iot имеет свой Postgres (telemetry per tenant) +- Разные StatefulSet, разные PVC + +### Что делает пользователь + +Клиент: +1. Подключает устройство → данные автоматически пишутся в его `iot_telemetry` +2. Пишет функцию которая реагирует на события +3. Функция получает `DB_DSN` в env var (автоматически из Secret) +4. Может делать SELECT/INSERT в свою БД через обычный SQL в коде функции +5. Может читать телеметрию через REST API + +### Plan — следующие шаги (этап IoT Postgres) + +1. Поднять Postgres StatefulSet в namespace `iot` +2. В iot-operator при создании IoTDevice namespace → `CREATE USER`, `CREATE DATABASE`, `CREATE TABLE iot_telemetry`, `CREATE TABLE iot_devices` +3. Credentials → k8s Secret `iot-tenant-{ns}-pg` +4. В iot-mqtt-bridge при получении MQTT сообщения → INSERT в tenant БД +5. REST API endpoint для чтения телеметрии +6. При деплое function → прокинуть `DB_DSN` в env var из Secret +7. При деплое function → если есть `schema.sql` → выполнить в tenant БД + +### Технические решения + +- Postgres: `postgres:16-alpine` StatefulSet с PVC 10Gi в namespace `iot` +- Connection pool: pgxpool (pgx v5) per-tenant, lazy init, max 5 conn per tenant +- Таблица telemetry: `(id bigserial, device_id text, ts timestamptz default now(), payload jsonb)` +- Индекс: `(device_id, ts DESC)` для быстрых запросов по устройству за период +- Retention: пока без TTL, добавить позже через pg_partman или cron job +