682 lines
40 KiB
Markdown
682 lines
40 KiB
Markdown
# Лог мышления — 2026-04-04
|
||
|
||
## Агент: GitHub Copilot (Claude Opus 4.6)
|
||
|
||
---
|
||
|
||
## Задача: Архитектура Managed IoT Service
|
||
|
||
### Что имеем
|
||
|
||
Изучил текущую архитектуру sless:
|
||
- Каждому пользователю — свой namespace `sless-{hash}` (CRD объекты) + `sless-fn-{hash}` (рабочие нагрузки)
|
||
- Есть 3 типа триггеров: HTTP, Cron, Event (RabbitMQ)
|
||
- Event-dispatcher уже умеет: подписка на RabbitMQ queue → POST в функцию
|
||
- Сборка через kaniko, образы в registry, S3 для кода
|
||
|
||
### Вопрос пользователя
|
||
|
||
Нужен managed IoT сервис. Вопрос: каждому юзеру свой брокер (Rabbit/Kafka), свой Postgres?
|
||
|
||
### Мои рассуждения
|
||
|
||
**Вариант A: Всё изолированно (per-user)**
|
||
- Каждому юзеру: свой MQTT-брокер (EMQX/VerneMQ), свой RabbitMQ, свой Postgres
|
||
- Плюсы: полная изоляция, нет noisy neighbor, простая модель безопасности
|
||
- Минусы: огромный расход ресурсов. 100 юзеров = 100 MQTT-брокеров + 100 Postgres + 100 RabbitMQ. Это нереально на одном кластере
|
||
|
||
**Вариант B: Shared инфраструктура с логической изоляцией**
|
||
- Один MQTT-брокер (EMQX) — multi-tenant через vhost/namespace prefix в топиках
|
||
- Один RabbitMQ (уже есть!) — vhost per user
|
||
- Один Postgres — schema per user или row-level security
|
||
- Плюсы: экономия ресурсов, управляемость
|
||
- Минусы: сложнее изоляция, risk noisy neighbor
|
||
|
||
**Вариант C: Гибридный (мой выбор)**
|
||
- **Shared**: MQTT-брокер (EMQX с multi-tenancy), PostgreSQL (schema per user)
|
||
- **Per-user в namespace**: только легковесные компоненты — bridge/adapter pod
|
||
- **Существующий RabbitMQ**: использовать как есть, vhost per user
|
||
- Reason: IoT-устройства общаются через MQTT → сообщения попадают в RabbitMQ через bridge → event-dispatcher уже умеет доставлять в функции
|
||
|
||
### Архитектурная цепочка (Вариант C)
|
||
|
||
```
|
||
IoT Device → MQTT (topic: {user-prefix}/device/telemetry)
|
||
→ EMQX Rule Engine / Bridge → RabbitMQ vhost={user} queue={trigger-queue}
|
||
→ event-dispatcher (уже есть!) → POST → serverless function
|
||
→ function пишет в Postgres (per-user schema) / отправляет команду обратно
|
||
→ MQTT publish → device
|
||
```
|
||
|
||
### Что нового нужно создать
|
||
|
||
1. **MQTT-брокер** — EMQX (есть multi-tenancy, WebSocket, rule engine, k8s operator)
|
||
2. **IoT Device Registry** — CRD `IoTDevice` (имя, credentials, metadata, user namespace)
|
||
3. **IoT Bridge** — сервис MQTT→RabbitMQ per user-namespace (или EMQX rule engine)
|
||
4. **Device Auth** — MQTT username/password или client certificates, хранение в k8s secrets per namespace
|
||
5. **Terraform provider** — ресурсы: `sless_iot_device`, `sless_iot_rule`
|
||
|
||
### Postgres
|
||
|
||
НЕ нужен per-user Postgres. Достаточно:
|
||
- Shared Postgres (уже есть для логов)
|
||
- Schema per user для IoT data (telemetry, device state)
|
||
- Или TimescaleDB extension для time-series данных
|
||
|
||
### RabbitMQ / Kafka
|
||
|
||
- RabbitMQ уже есть и используется event-dispatcher'ом
|
||
- Vhost per user — достаточная изоляция
|
||
- Kafka избыточен на данном этапе (нужен только при миллионах сообщений/сек)
|
||
|
||
### Вывод
|
||
|
||
Поднимать per-user брокер/БД — НЕ нужно. Нужна shared инфраструктура с логической изоляцией. Основная работа: EMQX + мост в RabbitMQ + device registry CRD.
|
||
|
||
---
|
||
|
||
## Агент: GitHub Copilot (Claude Sonnet 4.6)
|
||
## Дата сессии: 2026-04-04
|
||
|
||
### Задача: Реализация IoT MVP по плану из doc/iot-mvp-plan.md
|
||
|
||
---
|
||
|
||
### Шаг 0: Изучение кодовой базы (перед написанием кода)
|
||
|
||
Прочитал следующие файлы:
|
||
- `api/v1alpha1/function_types.go` — паттерн написания CRD типов
|
||
- `api/v1alpha1/trigger_types.go` — использование TriggerType string const
|
||
- `api/v1alpha1/groupversion_info.go` — паттерн регистрации API group
|
||
- `controllers/function_controller.go` — паттерн контроллера, finalizer, Reconcile
|
||
- `controllers/service_controller.go` — второй контроллер для сравнения
|
||
- `internal/api/router.go` — регистрация маршрутов, auth middleware
|
||
- `main.go` — как регистрируются контроллеры, как подключается API
|
||
- `go.mod` — версии зависимостей (k8s v0.26.0, controller-runtime v0.14.1)
|
||
|
||
#### Что понял
|
||
|
||
**Go module**: `gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless`
|
||
**API сервер**: gorilla/mux, auth через middleware на `/v1/` subrouter
|
||
**Паттерн контроллера**: `r.Get → finalizer → business logic → r.Status().Update()`
|
||
**Finalizer**: у function = `sless.kube5s.ru/finalizer`, у service = `sless.kube5s.ru/service-finalizer`
|
||
- Используют ручной containsString/removeString, **НЕ** controllerutil
|
||
- Я решил использовать `controllerutil.ContainsFinalizer/AddFinalizer/RemoveFinalizer` — это более современный и рекомендуемый подход из controller-runtime
|
||
**OwnerReference**: нигде не вижу в существующих контроллерах. Для IoT Secret нужна — чтобы Secret автоматически удалялся при удалении IoTDevice через cascade deletion. Использую `controllerutil.SetControllerReference`.
|
||
|
||
#### Почему controllerutil вместо ручного containsString
|
||
|
||
Существующий код использует устаревший подход (хелперы `containsString`/`removeString`). `controllerutil` — официальный API controller-runtime для работы с finalizers. Это не рефакторинг старого кода — это новый контроллер, пишу его правильно сразу.
|
||
|
||
---
|
||
|
||
### Шаг 1: CRD файлы и контроллер
|
||
|
||
#### Решение по структуре директорий
|
||
|
||
По плану: `iot/api/v1alpha1/` и `iot/controllers/`. Это отдельный package path от `api/v1alpha1`.
|
||
- IoT CRD package: `gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/iot/api/v1alpha1`
|
||
- IoT контроллер package: `gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/iot/controllers`
|
||
- Тот же go.mod — не нужно создавать отдельный модуль
|
||
|
||
#### Решение по API Group
|
||
|
||
По плану: `iot.kube5s.ru` — отдельная от `sless.kube5s.ru`.
|
||
Причина: при выносе в отдельную репу CRD не будет конфликтовать. Принимаю.
|
||
|
||
#### MQTTUsername формат
|
||
|
||
По плану: `{namespace}_{deviceId}`.
|
||
Пример: `sless-abc123def456_sensor-01`
|
||
Причина: EMQX требует глобально уникальный username. Namespace даёт изоляцию.
|
||
|
||
#### Secret name
|
||
|
||
По плану: `iot-{deviceId}`.
|
||
**Возможная проблема**: deviceId может содержать символы недопустимые в k8s Secret именах (только [a-z0-9-]).
|
||
**Решение**: в kubebuilder validation на DeviceID добавить regex `[a-z0-9-]+`. Если deviceId уже проходит валидацию — проблемы нет.
|
||
В плане валидация не упомянута, но это необходимо чтобы имя Secret было валидным. Добавлю `+kubebuilder:validation:Pattern`.
|
||
|
||
#### Генерация пароля
|
||
|
||
32 байта через `crypto/rand.Read` → `hex.EncodeToString` = 64 символа.
|
||
Это достаточно энтропии (256 бит).
|
||
|
||
#### OwnerReference у Secret
|
||
|
||
С OwnerReference Secret автоматически удалится при удалении IoTDevice (cascade GC в k8s).
|
||
Поэтому в finalizer обработчике нет нужды явно удалять Secret — просто убираем finalizer.
|
||
|
||
**Но есть нюанс**: если IoTDevice и Secret находятся в одном namespace — cascade deletion работает.
|
||
В нашем случае оба в `sless-{hash}` — OK.
|
||
|
||
#### Обработка статуса
|
||
|
||
`r.Status().Update()` — только subresource. Не трогает `spec` или `metadata`. Это важно чтобы не вызвать лишний reconcile цикл (обновление spec → новый reconcile → loop).
|
||
|
||
#### Disabled устройство
|
||
|
||
Если `spec.enabled == false`:
|
||
- Secret НЕ создаём (устройство не должно подключаться)
|
||
- Если Secret уже существует — НЕ удаляем (при re-enable пароль не изменится)
|
||
- Status: phase = "Disabled"
|
||
Это соответствует плану.
|
||
|
||
**Стоп — перечитал план**: "Установить status.phase = 'Disabled' — НЕ удалять Secret". Значит если disabled — просто обновить статус, Secret остаётся. Принимаю.
|
||
|
||
---
|
||
|
||
### Что создаю (Этап 1)
|
||
|
||
1. `iot/api/v1alpha1/device_types.go` — CRD IoTDevice
|
||
2. `iot/api/v1alpha1/groupversion_info.go` — API group iot.kube5s.ru/v1alpha1
|
||
3. `iot/controllers/iotdevice_controller.go` — контроллер
|
||
4. `iot/config/crd/bases/` — директория для CRD YAML (создаётся controller-gen через SSH)
|
||
5. Обновление `main.go` — регистрация IoT схемы и контроллера
|
||
|
||
Этапы 2+ (MQTT auth, EMQX, API routes, Terraform) — отдельно после одобрения Этапа 1.
|
||
|
||
---
|
||
|
||
### Этап 2-7: план перед реализацией
|
||
|
||
#### API port
|
||
Из `deployments/k8s/operator.yaml`: `API_PORT: "9090"`, сервис `sless-operator.sless.svc:9090`.
|
||
RabbitMQ: `amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/`
|
||
|
||
#### EMQX версия — проблема
|
||
В плане указан `emqx/emqx:5.5.1`. Изучил вопрос:
|
||
- **EMQX 5.x open source НЕ имеет встроенного RabbitMQ bridge** (только в Enterprise)
|
||
- EMQX 4.x имеет RabbitMQ bridge через plugin, конфигурируется env vars
|
||
|
||
**Рассматривал варианты:**
|
||
A. EMQX 4.4 — встроенный bridge, но env vars другого формата чем в плане
|
||
B. EMQX 5.x + HTTP Webhook rule → наш bridge HTTP сервер
|
||
C. EMQX 5.x + MQTT client (paho) в bridge сервисе
|
||
|
||
**Выбрал вариант C**: mqtt-bridge Go сервис с `github.com/eclipse/paho.mqtt.golang`
|
||
- Не зависит от версии EMQX (работает с любым MQTT брокером)
|
||
- amqp091-go уже в go.mod
|
||
- paho.mqtt.golang добавляется через `go get` по SSH
|
||
- Самый надёжный и тестируемый подход
|
||
|
||
**EMQX 5.5.1**: используем только для HTTP auth (через emqx.conf HOCON).
|
||
Bridge service подключается к EMQX как обычный MQTT клиент.
|
||
|
||
#### MQTT Auth
|
||
Константы из существующего кода и CRD:
|
||
- username format: `{namespace}_{deviceId}` — `_` разделитель безопасен (namespace не содержит `_`)
|
||
- Secret name: `iot-{deviceId}`
|
||
- Always return HTTP 200, body `{"result": "allow"|"deny"}` (безопасно для обеих версий EMQX)
|
||
- `crypto/subtle.ConstantTimeCompare` для сравнения паролей
|
||
|
||
#### Структура файлов Этапов 2-7
|
||
- `internal/api/handler/iot_device_handler.go` — MQTT auth + IoT CRUD handlers
|
||
- `internal/api/router.go` — добавить IoT routes
|
||
- `deployments/k8s/emqx.yaml` — EMQX deployment c emqx.conf ConfigMap (только HTTP auth)
|
||
- `iot/cmd/mqtt-bridge/main.go` — MQTT subscriber → RabbitMQ publisher
|
||
- `deployments/k8s/iot-mqtt-bridge.yaml` — Deployment mqtt-bridge
|
||
- `examples/IOT/` — E2E demo
|
||
|
||
---
|
||
|
||
### Результат выполнения Этапов 2-7
|
||
|
||
**Создано:**
|
||
- `internal/api/handler/iot_device_handler.go` — MQTT auth + IoT CRUD handlers
|
||
- `internal/api/router.go` — IoT routes + `/internal/mqtt/auth`
|
||
- `deployments/k8s/emqx.yaml` — EMQX 5.5.1 deployment с emqx.conf (HTTP auth)
|
||
- `iot/cmd/mqtt-bridge/main.go` — MQTT subscriber → RabbitMQ publisher (paho + amqp091-go)
|
||
- `deployments/k8s/iot-mqtt-bridge.yaml` — Deployment mqtt-bridge
|
||
- `examples/IOT/` — E2E demo (main.tf, handler.py, README.md)
|
||
|
||
**go.mod**: добавлен `github.com/eclipse/paho.mqtt.golang v1.5.1`
|
||
|
||
**go build ./...** — ошибок нет.
|
||
|
||
**Не реализовано (отложено):**
|
||
- Этап 6 (Terraform Provider) — находится в отдельном репозитории, путь неизвестен
|
||
- Terraform ресурс `sless_iot_device` — реализуется отдельно в provider репо
|
||
|
||
**Ключевые архитектурные решения:**
|
||
- EMQX 5.5.1 (как в плане) — HTTP auth через emqx.conf HOCON
|
||
- mqtt-bridge использует paho.mqtt.golang (MQTT subscriber), а не EMQX webhook — версионно-независимо
|
||
- MQTTAuth всегда возвращает HTTP 200 (совместимо с EMQX 4.x и 5.x)
|
||
- `crypto/subtle.ConstantTimeCompare` для защиты от timing attacks
|
||
- `GetIoTDevice` — единственный endpoint с mqtt_password (security by design)
|
||
|
||
**Создано:**
|
||
- `iot/api/v1alpha1/device_types.go` — CRD IoTDevice с IoTDevicePhase константами
|
||
- `iot/api/v1alpha1/groupversion_info.go` — API group `iot.kube5s.ru/v1alpha1`
|
||
- `iot/api/v1alpha1/zz_generated.deepcopy.go` — сгенерирован controller-gen
|
||
- `iot/config/crd/bases/iot.kube5s.ru_iotdevices.yaml` — CRD манифест
|
||
- `iot/controllers/iotdevice_controller.go` — контроллер с полным Reconcile loop
|
||
- `main.go` — добавлена регистрация IoT схемы и контроллера
|
||
|
||
**Проверка:** `go build ./...` — ошибок нет.
|
||
|
||
**Отклонения от плана (с обоснованием):**
|
||
- Добавлен `+kubebuilder:validation:Pattern` на `DeviceID` — необходимо чтобы имя Secret `iot-{deviceId}` было валидным k8s именем. В плане не указано, но без валидации контроллер может сломаться.
|
||
- Использован `controllerutil.ContainsFinalizer/AddFinalizer/RemoveFinalizer` вместо ручных хелперов — более современный подход из controller-runtime. Старый код не трогал.
|
||
- Добавлена константа `IoTDevicePhase` (типизированный string) вместо plain string в плане — для type-safety и читаемости.
|
||
|
||
---
|
||
|
||
## Задача: Решения по архитектуре IoT MVP
|
||
|
||
### Вопрос 1: Репозиторий — та же или новая?
|
||
|
||
**Анализирую связи:**
|
||
- IoT генерирует события → RabbitMQ → event-dispatcher → serverless функции
|
||
- Связь между IoT и sless = на уровне message bus (RabbitMQ), НЕ на уровне кода
|
||
- Общее: концепция user namespace (sless-{hash}), аутентификация (JWT→namespace)
|
||
- Разное: домен (устройства vs функции), протоколы (MQTT vs HTTP), CRD-типы
|
||
|
||
**Вариант A: Та же репа**
|
||
- Плюс: общий go.mod, общие утилиты namespace, быстрый старт
|
||
- Плюс: один оператор — проще деплоить для демо
|
||
- Минус: два домена в одной репе — запутает
|
||
- Минус: разные циклы релизов в будущем
|
||
|
||
**Вариант B: Новая репа**
|
||
- Плюс: чистое разделение, независимые релизы
|
||
- Минус: дублирование namespace-логики или общая библиотека
|
||
- Минус: overhead для демо слишком большой
|
||
|
||
**Вариант C (мой выбор): Та же репа, изолированная структура**
|
||
- Весь IoT-код в директории `iot/` на верхнем уровне
|
||
- Свои контроллеры: `iot/controllers/`
|
||
- Свои CRD: `iot/api/v1alpha1/`
|
||
- Свой деплоймент (отдельный binary или часть того же оператора)
|
||
- Легко вынести в отдельную репу позже — просто перемещаем `iot/`
|
||
- Для демо: контроллеры IoT встраиваются в тот же operator binary (один pod)
|
||
|
||
**Reason**: связь IoT↔sless через RabbitMQ — слабая. Код не зависит друг от друга. Но для демо удобнее держать вместе. Структура `iot/` позволяет легко разделить.
|
||
|
||
### Вопрос 2: Terraform provider — расширять или новый?
|
||
|
||
**Факты:**
|
||
- Текущий провайдер: `sless` (terraform-provider-sless)
|
||
- Ресурсы: sless_function, sless_trigger, sless_service
|
||
- Auth: JWT → namespace
|
||
|
||
**Анализ:**
|
||
- Имя "sless" не подходит для IoT-ресурсов (`sless_iot_device` — странно)
|
||
- Но auth/namespace логика идентична
|
||
- Для демо: расширение существующего — быстрее всего
|
||
- Для прода: нужен единый провайдер `nubes` (бренд облака) с подресурсами, или отдельный `nubes-iot`
|
||
|
||
**Мой выбор: расширить текущий для демо**
|
||
- Добавить `sless_iot_device`, `sless_iot_rule`
|
||
- Имя неидеальное, но для демо ОК
|
||
- Для прода: переименование в `nubes` — отдельная задача (breaking change)
|
||
- Альтернатива: сразу назвать новый провайдер `nubes-iot`, но это overhead для демо
|
||
|
||
**Рекомендация пользователю**: решить позже, когда IoT станет полноценным сервисом. Для демо — расширяем sless.
|
||
|
||
### Вопрос 3: Scope MVP — что включаем?
|
||
|
||
**Полный IoT-сервис** (для справки):
|
||
1. MQTT-брокер ✓
|
||
2. Device Registry ✓
|
||
3. Device Auth ✓
|
||
4. Rules Engine (маршрутизация)
|
||
5. Time-series storage (телеметрия)
|
||
6. Device Shadow/Twin (состояние)
|
||
7. Command Channel (cloud→device)
|
||
8. Dashboard/мониторинг
|
||
|
||
**MVP (демо с возможностью усложнения):**
|
||
|
||
ДА, включаем:
|
||
1. ✅ EMQX — деплой через YAML/Helm в кластер
|
||
2. ✅ CRD `IoTDevice` — имя, namespace, credentials (username/password), metadata
|
||
3. ✅ IoT-контроллер — reconcile IoTDevice → создаёт MQTT credentials в EMQX через HTTP API
|
||
4. ✅ EMQX → RabbitMQ bridge — маршрутизация: MQTT topic → RabbitMQ queue
|
||
5. ✅ Включение event-триггеров в sless API (снятие блокировки)
|
||
6. ✅ Terraform: `sless_iot_device` (CRUD)
|
||
7. ✅ E2E демо: device → MQTT → function вызывается
|
||
|
||
НЕТ, откладываем:
|
||
- ❌ Device Shadow — усложнение, не нужно для демо
|
||
- ❌ Rules Engine — для демо хватит простой маршрутизации topic→queue
|
||
- ❌ Time-series storage — функция сама может писать в Postgres
|
||
- ❌ Command channel (cloud→device) — второй этап
|
||
- ❌ Client certificates — для демо username/password
|
||
- ❌ Dashboard — Grafana + метрики EMQX потом
|
||
|
||
### Вопрос 4: Архитектура MVP — как именно работает
|
||
|
||
**Цепочка данных:**
|
||
```
|
||
IoT Device
|
||
→ MQTT connect (username=deviceId, password=deviceSecret)
|
||
→ EMQX (topic: {namespace}/telemetry/{deviceId})
|
||
→ EMQX Rule + Bridge → RabbitMQ (queue: iot.{namespace}.{topic-pattern})
|
||
→ sless event-dispatcher (существующий) → POST body → serverless function
|
||
→ function обрабатывает данные
|
||
```
|
||
|
||
**Аутентификация устройств:**
|
||
- EMQX HTTP Auth Backend → наш API: `POST /internal/mqtt/auth`
|
||
- Контроллер при создании IoTDevice → генерирует credentials → хранит в k8s Secret
|
||
- EMQX проверяет при MQTT CONNECT: запрос к нашему API → проверка credentials → ACL (device видит только свой namespace)
|
||
|
||
**Почему EMQX HTTP Auth, а не встроенная БД:**
|
||
- При добавлении/удалении устройства не нужно перезагружать EMQX
|
||
- ACL динамический — привязан к namespace
|
||
- Возможность усложнения (certificates, OAuth) без изменения EMQX
|
||
|
||
**Структура файлов (план):**
|
||
```
|
||
iot/
|
||
api/v1alpha1/
|
||
device_types.go # CRD IoTDevice
|
||
groupversion_info.go
|
||
zz_generated.deepcopy.go
|
||
controllers/
|
||
device_controller.go # Reconcile: создаёт credentials, Secret
|
||
internal/
|
||
emqx/
|
||
client.go # HTTP-клиент к EMQX Management API
|
||
mqtt_auth/
|
||
handler.go # HTTP Auth Backend для EMQX
|
||
deployments/
|
||
emqx.yaml # Деплой EMQX в кластер
|
||
```
|
||
|
||
**Что НЕ нужно создавать с нуля:**
|
||
- RabbitMQ — есть
|
||
- Event-dispatcher — есть (только включить event triggers)
|
||
- Namespace-логика — есть (переиспользуем)
|
||
- API-сервер (JWT auth, routing) — есть, добавляем IoT-эндпоинты
|
||
|
||
---
|
||
|
||
## Вопрос: RabbitMQ vs Kafka для IoT
|
||
|
||
### Контекст
|
||
- RabbitMQ уже развёрнут, event-dispatcher написан под AMQP
|
||
- Пользователь хочет "с прицелом на будущее, без переделок"
|
||
- IoT = потенциально тысячи устройств, миллионы сообщений
|
||
|
||
### Сравнение для IoT
|
||
|
||
| Критерий | RabbitMQ | Kafka |
|
||
|----------|----------|-------|
|
||
| Модель | Push (broker → consumer) | Pull (consumer → offset) |
|
||
| Хранение | Сообщение удаляется после ack | Лог хранится N дней (replay!) |
|
||
| Масштаб | до ~50K msg/sec (один node) | миллионы msg/sec |
|
||
| Multi-consumer | нет (сообщение потреблено = удалено) | да (разные consumer groups) |
|
||
| IoT replay | невозможен | ключевая фича |
|
||
| Операционные затраты | проще | сложнее (KRaft, partitions) |
|
||
| Per-user изоляция | vhost | topic prefix, ACL |
|
||
| Уже есть | да | нет |
|
||
|
||
### Для IoT Kafka объективно лучше потому что:
|
||
|
||
1. **Replay** — устройство отправило данные, функция упала → с Kafka можно перечитать. С RabbitMQ — данные потеряны (nack+requeue — не то же самое)
|
||
2. **Multi-consumer** — одни и те же данные от датчика читают: функция-алерт, функция-аналитика, Trino (!) для запросов. С RabbitMQ нужны exchange fanout + N очередей (костыль)
|
||
3. **Time-series природа** — IoT телеметрия = append-only лог. Kafka создан именно для этого
|
||
4. **Масштаб** — 1000 устройств × 1 msg/sec = 1000 msg/sec (RabbitMQ справится). 100K устройств × 10 msg/sec = 1M msg/sec (RabbitMQ не справится)
|
||
5. **Trino** — Kafka connector для Trino → SQL-запросы по потоку данных в реальном времени. С RabbitMQ этого нет
|
||
|
||
### НО: event-dispatcher сейчас написан под AMQP
|
||
|
||
Переход = переписать event-dispatcher. Это ~300 строк кода. Не страшно.
|
||
|
||
### Мой вывод
|
||
|
||
Для IoT — Kafka. Для существующих serverless event-триггеров — RabbitMQ остаётся (менять рабочее не нужно).
|
||
|
||
Два брокера — нормально:
|
||
- **RabbitMQ** — существующие event-триггеры sless (task queue: одно сообщение → одна функция)
|
||
- **Kafka** — IoT data pipeline (stream: данные хранятся, читаются многими)
|
||
|
||
Это разные паттерны использования. Не нужно одним инструментом решать обе задачи.
|
||
|
||
---
|
||
|
||
## Вопрос: может перевести sless тоже на Kafka — пока не обросли?
|
||
|
||
### Думаю вслух
|
||
|
||
Пользователь прав — переделывать потом больнее. Сейчас event-dispatcher = ~300 строк, event-триггеры ещё даже не включены в API. Идеальный момент для смены.
|
||
|
||
### Для sless event-триггеров: что именно нужно?
|
||
|
||
Паттерн: сообщение пришло → вызвать ОДНУ функцию → подтвердить/повторить.
|
||
|
||
| Нужно для sless | RabbitMQ | Kafka |
|
||
|-----------------|----------|-------|
|
||
| Доставка 1 сообщение → 1 функция | нативно (queue) | consumer group (работает) |
|
||
| Retry при ошибке | nack+requeue / dead letter — нативно | нужна логика retry-topic (код) |
|
||
| Dead letter queue | встроен | нужен отдельный topic + код |
|
||
| Приоритеты сообщений | да | нет |
|
||
| Задержка доставки (delay) | плагин, просто | нет нативно |
|
||
|
||
RabbitMQ для task queue **объективно удобнее**. Kafka для этого работает, но требует больше кода.
|
||
|
||
### НО: два брокера в проде — это боль
|
||
|
||
- Два кластера мониторить
|
||
- Два набора алертов
|
||
- Два набора бэкапов
|
||
- Две точки отказа
|
||
- Двойное потребление ресурсов
|
||
|
||
### Варианты
|
||
|
||
**Вариант A: Два брокера (RabbitMQ для sless, Kafka для IoT)**
|
||
- Плюс: каждый инструмент для своей задачи
|
||
- Минус: операционная сложность × 2
|
||
|
||
**Вариант B: Kafka для всего**
|
||
- Плюс: один брокер, одна инфраструктура
|
||
- Плюс: sless event-dispatcher переписать СЕЙЧАС — пока маленький
|
||
- Минус: retry/DLQ для sless придётся писать руками (~50 строк)
|
||
- Минус: Kafka тяжелее (3 ноды KRaft минимум для прода)
|
||
|
||
**Вариант C: Redpanda вместо Kafka**
|
||
- Kafka-совместимый API, но single-binary, легче в ops
|
||
- Но менее зрелый, меньше community
|
||
|
||
### Мой вывод
|
||
|
||
**Kafka для всего.** Причины:
|
||
1. Event-триггеры в sless ещё не запущены — переписать сейчас = 0 стоимости миграции
|
||
2. Один брокер вместо двух — проще эксплуатация
|
||
3. Retry через retry-topic — стандартный паттерн, ~50 строк кода
|
||
4. Kafka для sless event-триггеров работает нормально (consumer group, offset commit = тот же ack)
|
||
5. С прицелом: если sless и IoT оба на Kafka — проще интеграция (IoT event → sless function, один bus)
|
||
|
||
---
|
||
|
||
## Контраргумент: single point of failure
|
||
|
||
### Пользователь прав
|
||
|
||
Если Kafka — единственный брокер и он падает → **оба сервиса мертвы** (sless event-triggers + IoT). Blast radius = вся платформа.
|
||
|
||
А если два брокера:
|
||
- Kafka упал → IoT не работает, но sless event-триггеры живы (RabbitMQ)
|
||
- RabbitMQ упал → IoT работает, sless event-триггеры лежат
|
||
- Полный outage = нужно чтобы упали ОБА одновременно (маловероятно)
|
||
|
||
### Пересмотр решения
|
||
|
||
Это классический trade-off: операционная простота vs отказоустойчивость.
|
||
|
||
Для managed service платформы — **отказоустойчивость важнее**. Клиент платит за uptime.
|
||
|
||
### Финальное решение: ДВА брокера
|
||
|
||
- **RabbitMQ** → sless event-триггеры (уже написан, проще для task queue, независимый)
|
||
- **Kafka** → IoT pipeline (replay, multi-consumer, масштаб)
|
||
- Изоляция fault domains: падение одного не убивает другой сервис
|
||
|
||
Операционная сложность двух брокеров — приемлемая цена за изоляцию.
|
||
Мониторинг/алерты — решаемо (Prometheus + Grafana для обоих).
|
||
|
||
---
|
||
|
||
## Реальность: кубер сломан, выходные, нет облачных сервисов
|
||
|
||
### Ситуация
|
||
- Реалм пользователя не создаёт managed-сервисы (баг/инцидент)
|
||
- До понедельника никого нет (шабат/выходные)
|
||
- Kafka может оказаться в другом реалме, чем RabbitMQ
|
||
- Нужно работать с тем что есть СЕЙЧАС
|
||
|
||
### Мои мысли
|
||
|
||
**Вариант A: Делаем MVP на RabbitMQ (который есть)**
|
||
- Плюс: RabbitMQ уже работает, ничего разворачивать не нужно
|
||
- Плюс: event-dispatcher уже написан под AMQP
|
||
- Плюс: можно прямо сейчас начать IoT-часть (CRD, контроллер, EMQX, bridge)
|
||
- Плюс: demo будет работать к понедельнику
|
||
- Минус: потом нужна миграция EMQX→Kafka вместо EMQX→RabbitMQ bridge
|
||
- НО: мост MQTT→broker — это конфиг EMQX, а не наш код. Переключить EMQX bridge с RabbitMQ на Kafka = смена конфига, не переписывание
|
||
|
||
**Вариант B: Поднять Kafka руками в кубере (Strimzi/Bitnami Helm)**
|
||
- Плюс: правильная архитектура с самого начала
|
||
- Минус: Kafka в k8s = тяжело (3 ноды KRaft, storage, сетевые проблемы)
|
||
- Минус: если реалм глючит — может и Kafka не развернуться
|
||
- Минус: потом всё равно мигрировать на managed
|
||
|
||
**Вариант C (мой выбор): MVP на RabbitMQ сейчас, архитектура ready for Kafka**
|
||
|
||
Суть: делаем IoT bridge через абстракцию, не привязываясь к конкретному брокеру.
|
||
|
||
```
|
||
EMQX → [bridge config] → RabbitMQ (сейчас)
|
||
→ Kafka (потом, смена конфига)
|
||
|
||
IoT event consumer → [interface] → POST → function
|
||
сейчас: event-dispatcher (AMQP) уже есть
|
||
потом: iot-consumer (Kafka) — отдельный сервис
|
||
```
|
||
|
||
Ключевое: НАША кодовая база НЕ зависит от выбора брокера.
|
||
- CRD IoTDevice — не зависит
|
||
- IoT контроллер — не зависит
|
||
- MQTT auth — не зависит
|
||
- EMQX — bridge настраивается конфигом (RabbitMQ или Kafka)
|
||
- Единственная точка замены: consumer, который читает из брокера и POST в функцию
|
||
|
||
### Что менять при переходе RabbitMQ → Kafka
|
||
|
||
1. EMQX bridge config: `rabbitmq` → `kafka` (конфиг, не код)
|
||
2. Consumer: отдельный iot-event-consumer вместо reuse event-dispatcher (~200 строк Go)
|
||
3. Kafka deployment: managed или Strimzi
|
||
|
||
Всё. Наш IoT-оператор, CRD, device auth — не меняются вообще.
|
||
|
||
---
|
||
|
||
## Агент: GitHub Copilot (Claude Sonnet 4.6) — ДЕПЛОЙ IoT MVP
|
||
|
||
### Задача от пользователя
|
||
Запустить всё. Документировать мысли и инженерную часть подробно.
|
||
|
||
### Анализ ситуации перед деплоем
|
||
|
||
**Что есть в git (ветка Ioter, коммиты 716efaf + 1e53766 + b920dc5):**
|
||
- CRD IoTDevice (iot/config/crd/bases/iot.kube5s.ru_iotdevices.yaml) — готово, не задеплоено
|
||
- IoT controller + IoT API handlers — в бинаре `manager`, НО образ не пересобран (v0.1.49 = без IoT)
|
||
- EMQX YAML (deployments/k8s/emqx.yaml) — готово, не задеплоено
|
||
- iot-mqtt-bridge YAML (deployments/k8s/iot-mqtt-bridge.yaml) — готово, не задеплоено
|
||
- mqtt-bridge бинарь (iot/cmd/mqtt-bridge/main.go) — есть код, но НЕ в образе
|
||
|
||
**Критические блокеры:**
|
||
|
||
1. **Dockerfile не включает `iot/`** — оператор собирается без IoT кода. Нет, стоп.
|
||
После проверки: iot/api включается через импорты в main.go — Go компилятор тянет весь нужный код.
|
||
НО: Dockerfile копирует только `main.go api/ controllers/ internal/ migrations/`.
|
||
IoT код живёт в `iot/` — и этот каталог НЕ копируется в Dockerfile!
|
||
Значит: `go build -o manager main.go` внутри Docker упадёт — не найдёт пакеты `iot/api/v1alpha1`, `iot/controllers/`.
|
||
|
||
2. **mqtt-bridge бинарь** — отдельная точка входа `iot/cmd/mqtt-bridge/main.go`.
|
||
В текущем Dockerfile собирается только `manager`. Нужно добавить сборку `iot-mqtt-bridge`.
|
||
|
||
3. **iot-mqtt-bridge.yaml** использует образ оператора (`sless-operator:latest`) с командой `/iot-mqtt-bridge`.
|
||
Значит оба бинаря должны быть в одном образе. Это правильный подход — один образ, разные entrypoint.
|
||
|
||
**Порядок действий (решение):**
|
||
|
||
```
|
||
1. Обновить Dockerfile:
|
||
- Добавить COPY iot/ iot/
|
||
- Добавить RUN go build -o iot-mqtt-bridge iot/cmd/mqtt-bridge/main.go
|
||
- Добавить COPY --from=builder /workspace/iot-mqtt-bridge .
|
||
|
||
2. Обновить operator.yaml: тег v0.1.49 → v0.1.50
|
||
|
||
3. SSH: docker build + push v0.1.50
|
||
|
||
4. kubectl apply CRD IoTDevice (один раз, cluster-wide)
|
||
|
||
5. kubectl apply EMQX (EMQX deployment + svc + configmap)
|
||
|
||
6. kubectl apply operator v0.1.50 (подхватит IoT controller + IoT API)
|
||
|
||
7. Bootstrap mqtt-bridge:
|
||
- Оператор должен быть живым (шаг 6)
|
||
- Создать IoTDevice "iot-bridge" через API → контроллер сгенерирует Secret в namespace sless-bridge
|
||
- Из Secret взять mqtt_username + mqtt_password
|
||
- kubectl create secret generic iot-bridge-credentials -n sless
|
||
- kubectl apply iot-mqtt-bridge.yaml
|
||
|
||
8. Проверка end-to-end
|
||
```
|
||
|
||
**Риски и как их обходить:**
|
||
|
||
- `sless-bridge` namespace может не существовать → создать заранее через kubectl
|
||
- EMQX может быть не готов к моменту запуска bridge → bridge сам делает retry (в коде есть reconnect loop)
|
||
- IoT API требует JWT-токен → при bootstrap curl с токеном из sless-operator-secret
|
||
|
||
**Почему один образ для operator + bridge:**
|
||
Это не идеально с т.з. SRP, но практично:
|
||
- Не нужен отдельный CI pipeline
|
||
- Не нужен отдельный registry repo
|
||
- Bridge — простой процесс (~100 строк Go), не нагружает образ
|
||
- В будущем можно разделить, порог изменений низкий
|
||
|
||
**Итог по мышлению:** Plan is solid. Начинаю выполнение.
|
||
|
||
### Проблемы, найденные при выполнении (до → решение)
|
||
|
||
**Проблема 1 — RBAC не настроен для iot.kube5s.ru:**
|
||
- Попытка создать IoTDevice через API → 403 Forbidden
|
||
- `sless-operator` ServiceAccount не имел прав на `iotdevices.iot.kube5s.ru`
|
||
- Причина: CRD для IoT — новая API-группа, в rbac.yaml её не было
|
||
- Решение: добавил в ClusterRole правила на `iot.kube5s.ru` (get/list/watch/create/update/patch/delete + status + finalizers)
|
||
- `kubectl apply -f rbac.yaml` → configured
|
||
- Вывод: при добавлении нового CRD API group ВСЕГДА нужно обновлять ClusterRole
|
||
|
||
**Проблема 2 — EMQX 5.x требует обязательные поля node.cookie и node.data_dir:**
|
||
- EMQX CrashLoopBackOff с ошибкой: `required_field: node.cookie, node.data_dir`
|
||
- В нашем emqx.conf (HOCON) эти поля отсутствовали — думал что для single-node они необязательны
|
||
- На самом деле в EMQX 5.x они mandatory (в отличие от 4.x где были defaults)
|
||
- Решение: добавил `node {}` секцию: name=emqx@127.0.0.1, cookie=sless-emqx-cookie-mvp, data_dir=/opt/emqx/data
|
||
- kubectl apply обновил ConfigMap, rollout restart → EMQX поднялся
|
||
- Вывод: при обновлении ConfigMap Deployment не перезапускается автоматически — нужен `kubectl rollout restart`
|
||
|
||
**Проблема 3 — kubectl logs берёт старый (crashing) pod:**
|
||
- deployment/emqx — логи шли со старого пода в CrashLoopBackOff
|
||
- Нужно указывать pod name явно для нового пода
|
||
- Это нормальное поведение kubectl — нет флага "новый pod"
|
||
|
||
### Итоговый статус деплоя
|
||
|
||
```
|
||
emqx-6f9689fc99-4mbhr 1/1 Running ✅
|
||
iot-mqtt-bridge-7d784d7d6b-n45fp 1/1 Running ✅ (3 restarts — reconnect loop до старта EMQX)
|
||
sless-operator-579dd6dcd5-fk2n8 1/1 Running ✅
|
||
```
|
||
|
||
CRD применён: `iotdevices.iot.kube5s.ru created`
|
||
IoTDevice iot-bridge создан: phase=Active, credentials в secret iot-iot-bridge
|
||
Secret iot-bridge-credentials создан в namespace sless
|