Merge remote-tracking branch 'origin/master'
# Conflicts: # .gitignore
This commit is contained in:
@@ -0,0 +1,230 @@
|
|||||||
|
# IoT ↔ SQS: сводка всех находок и мыслей (handoff для нового чата/репы)
|
||||||
|
|
||||||
|
> Дата: 2026-10-05.
|
||||||
|
> Назначение: полностью самодостаточный документ — чтобы, открыв его в НОВОЙ
|
||||||
|
> репе или новом чате, можно было продолжить без чтения истории.
|
||||||
|
> Вся информация ниже взята из реального кода/доков репозитория IoT
|
||||||
|
> (`/home/naeel/nubes/IoT`), а НЕ из памяти и НЕ из чужих заметок.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. КЛЮЧЕВОЙ РАЗВОРОТ: что в какой репе
|
||||||
|
|
||||||
|
**Есть ДВА разных проекта, в ДВУХ разных репах:**
|
||||||
|
|
||||||
|
| Проект | Репа | Что делает | Состояние |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **shared-sqs** | отдельная репа (SQS-service) | SQS-совместимая очередь (AWS API, Go, форк GoAWS), multi-tenant | **ГОТОВ, работает на проде v0.1.35** (все тесты зелёные, нагрузка без ошибок) |
|
||||||
|
| **IoT** | эта репа (`/home/naeel/nubes/IoT`) | телеметрия устройств: MQTT → SQS → PostgreSQL → REST | **MVP, не production-ready** (есть подтверждённые блокеры) |
|
||||||
|
|
||||||
|
**Зависимость:** IoT **потребляет** shared-sqs как внешний сервис (очередь
|
||||||
|
телеметрии `iot-telemetry`). shared-sqs от IoT **не зависит**.
|
||||||
|
|
||||||
|
**Вывод о порядке (отвечая на вопрос «что сначала — SQS или IoT»):**
|
||||||
|
- shared-sqs фактически **уже готов и работает** раньше, чем IoT дотянет до
|
||||||
|
production. Его «доделывать» не надо для старта работ по IoT.
|
||||||
|
- Порядок работ по сути такой: **SQS уже есть (работает) → теперь доработки
|
||||||
|
IoT**, который на него опирается.
|
||||||
|
- Если планируется создавать оба «с нуля» — логический порядок архитектурно:
|
||||||
|
сначала SQS, потом IoT (т.к. IoT от него зависит).
|
||||||
|
|
||||||
|
**ВНИМАНИЕ про ветки IoT-репы (важно, легко запутаться):**
|
||||||
|
- Актуальный код IoT (монолит `iot-service`, `internal/service/`,
|
||||||
|
`internal/storage/iotpg/`) лежит на ветке **`feature/replace-kafka-with-sqs`**.
|
||||||
|
Имя ветки — ПЕРЕЖИТОК старой миграции Kafka→SQS (апрель 2026), к текущей
|
||||||
|
задаче отношения не имеет. Не путать имя с сутью: это текущая ветка IoT.
|
||||||
|
- Ветка **`master`** — СТАРАЯ, без монолита (там легаси: `internal/api/`, CRD,
|
||||||
|
k8s-манифесты, `deployments/`, `examples/`, `config/crd/`). НЕ актуальна.
|
||||||
|
- На `master` ошибочно закоммичены 3 файла документации (см. раздел 5).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что такое текущий IoT (архитектура)
|
||||||
|
|
||||||
|
**Монолит `iot-service`** — один Go-бинарник с тремя ролями в одном процессе:
|
||||||
|
1. **REST API** + MQTT auth/acl на порту `:9090` (устройства хранятся в PostgreSQL);
|
||||||
|
2. **bridge** — MQTT-подписка на EMQX → отправка в shared-SQS;
|
||||||
|
3. **consumer** — чтение из shared-SQS (long-poll) → запись в per-tenant PostgreSQL.
|
||||||
|
|
||||||
|
**Конвейер телеметрии:**
|
||||||
|
```
|
||||||
|
устройство → wss → EMQX (authn+authz напрямую в PG) → бридж → shared-SQS
|
||||||
|
→ consumer → per-tenant PostgreSQL → REST API (/v1/namespaces/{ns}/iot/telemetry)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Стек:**
|
||||||
|
- Go 1.25, gorilla/mux, paho.mqtt.golang, aws-sdk-go-v2 (SQS), lib/pq.
|
||||||
|
- EMQX 5.5.1 (образ `naeel/iot-emqx`), shared-SQS (SQS-совместимая очередь),
|
||||||
|
PostgreSQL managed (per-tenant БД).
|
||||||
|
- Модуль: `gitea.services.ngcloud.ru/Nail/IoT`.
|
||||||
|
|
||||||
|
**Ключевые файлы актуального кода (на ветке feature):**
|
||||||
|
- `cmd/iot-service/main.go` — точка входа: config → PG → SQS → API + горутины bridge/consumer.
|
||||||
|
- `internal/service/config/config.go`, `defaults.go` — env-конфигурация.
|
||||||
|
- `internal/service/api/router.go`, `middleware/auth.go` — маршруты + JWT.
|
||||||
|
- `internal/service/api/handler/*.go` — devices, mqtt_auth, telemetry, admin, sqsstats.
|
||||||
|
- `internal/service/store/*.go` — устройства в PG (`iot_devices`).
|
||||||
|
- `internal/storage/iotpg/iot_telemetry_store.go` — запись телеметрии (batch).
|
||||||
|
- `internal/service/bridge/*.go` — bridge → SQS.
|
||||||
|
- `internal/service/consumer/*.go` — SQS → PG.
|
||||||
|
- `internal/service/sqsclient/sqsclient.go` — SQS-клиент (endpoint override).
|
||||||
|
- `emqx/emqx.conf` — конфиг EMQX (auth/ACL — напрямую в PostgreSQL).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Как IoT использует shared-sqs (факты)
|
||||||
|
|
||||||
|
- Внутренний endpoint shared-sqs (тот же кластер Nubes, realm `iot-naeel`):
|
||||||
|
`http://containerk8s.f1ffb134-7d16-45bd-8bef-69f6ec8ab33c.svc.cluster.local:4100`
|
||||||
|
(вшит дефолтом в `config/defaults.go`).
|
||||||
|
- Очередь: `iot-telemetry`, регион `us-east-1`.
|
||||||
|
- Тенант IoT: `t-fec713a719ad0b33`; креды детерминированы из email:
|
||||||
|
tenantID `t-`+sha256(email)[:8], AccessKey `SSAK-`+sha256(email)[:12],
|
||||||
|
SecretKey sha256(email+":shared-sqs:secret-key:v1").
|
||||||
|
- Лимиты shared-sqs: 50 очередей на тенанта, сообщение до 256 КБ, Receive 1–10.
|
||||||
|
- В бридже: неблокирующий SQS-диспетчер (канал 10000 + 4 worker'а, ретраи 1/2/4с),
|
||||||
|
дроп при переполнении буфера и при oversize >250 КБ.
|
||||||
|
- В consumer: long-poll 20с, VisibilityTimeout 120с (в коде), backoff при
|
||||||
|
сбое PG (1с→60с), DeleteMessage после обработки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. ПОДТВЕРЖДЁННЫЕ БЛОКЕРЫ IoT (для доработок)
|
||||||
|
|
||||||
|
> Источник: два независимых анализа + ревью Opus + моя перепроверка построчно.
|
||||||
|
> Все — по КОДУ, с точными ссылками. Сгруппировано по приоритету.
|
||||||
|
|
||||||
|
### P0 — компрометация/потеря данных или доступ к чужим данным
|
||||||
|
|
||||||
|
**P0-1. Tenant-изоляция не работает.**
|
||||||
|
- `internal/service/api/middleware/auth.go` (`validateJWT`, ~стр. 62, 120):
|
||||||
|
проверяет только наличие `sub` и (необязательно) `exp`, а `sub` НИКАК не
|
||||||
|
сверяется с `{namespace}` из URL.
|
||||||
|
- Обработчики берут namespace из пути (`handler/devices.go:91`,
|
||||||
|
`handler/telemetry.go:19`) без сверки с токеном.
|
||||||
|
- Итог: **любой валидный токен читает/пишет ЛЮБОЙ namespace** — горизонтальный
|
||||||
|
доступ к чужим устройствам и телеметрии.
|
||||||
|
|
||||||
|
**P0-2. Проверка подписи JWT необязательна.**
|
||||||
|
- `auth.go` (~стр. 70): при пустом `JWT_HMAC_SECRET` подпись НЕ проверяется —
|
||||||
|
только структура токена. Токен подделывается вручную (`{"sub":"x","exp":...}`).
|
||||||
|
- `config/config.go` (~стр. 118): дефолт секрета пустой, секрет не в списке
|
||||||
|
required env.
|
||||||
|
|
||||||
|
**P0-3. Пароли устройств в открытом виде.**
|
||||||
|
- `store/open.go:21`: `mqtt_password TEXT` — хранится открытым текстом.
|
||||||
|
- `handler/devices.go:189`: пароль отдаётся повторно через `GET /devices/{name}`.
|
||||||
|
- `emqx/emqx.conf:46`: `password_hash_algorithm name=plain`.
|
||||||
|
- Компрометация БД = все пароли устройств + возможность выдать их повторно.
|
||||||
|
|
||||||
|
**P0-7. Потеря телеметрии: SQS-удаление до фактической записи.**
|
||||||
|
- `consumer/consumer.go:78`: удаляет сообщение из SQS после
|
||||||
|
`processTelemetry`==nil.
|
||||||
|
- `iotpg/iot_telemetry_store.go:356-376`: `InsertTelemetry` при неполном
|
||||||
|
батче кладёт строку В ПАМЯТЬ и возвращает nil (запись ещё не сделана).
|
||||||
|
- `iotpg/iot_telemetry_store.go:129-135` (`flushAll`): подменяет буфер на
|
||||||
|
пустой ДО записи в PG, при ошибке — только логирует.
|
||||||
|
- Итог: краш между «кладём в буфер» и «flush» → сообщение уже удалено из SQS,
|
||||||
|
в PG не попало → **безвозвратная потеря**.
|
||||||
|
|
||||||
|
**P0-8. Нет идемпотентности (дубли).**
|
||||||
|
- `iot_telemetry` таблица: `id BIGSERIAL`, без ключа дедупликации.
|
||||||
|
- `bridge/handler.go:19`: envelope без message-id.
|
||||||
|
- At-least-once SQS → **молчаливые дубли** при повторе сообщения.
|
||||||
|
|
||||||
|
### P1 — важные риски эксплуатации
|
||||||
|
|
||||||
|
**P1-4. Лишние HTTP-auth/ACL-эндпоинты.**
|
||||||
|
- `router.go:50-51`: `/internal/mqtt/auth` и `/internal/mqtt/acl` висят на
|
||||||
|
публичном порту 9090 БЕЗ аутентификации.
|
||||||
|
- Но EMQX ходит по PG-auth напрямую (`emqx/emqx.conf`, `backend=postgresql`),
|
||||||
|
эти эндпоинты — НЕиспользуемый fallback. Рекомендация: **удалить**, а не защищать.
|
||||||
|
- Риск: кто достучался до порта — перебирает пароли устройств и получает ACL.
|
||||||
|
|
||||||
|
**P1-5. `AUTH_TEST_MODE=true` на проде.**
|
||||||
|
- Был включён для диагностики DevOps (HISTORY §38); любой Bearer-токен проходит.
|
||||||
|
- Нужно выставить `false` + задать `JWT_HMAC_SECRET`.
|
||||||
|
|
||||||
|
**P1-9. `/health` не отражает состояние.**
|
||||||
|
- `router.go:72`: `healthHandler` всегда `{"status":"ok"}`, не проверяет
|
||||||
|
bridge/consumer/PG/SQS.
|
||||||
|
- `main.go`: ошибки `bridge.Run`/`consumer.Run` только логируются; горутина
|
||||||
|
завершается, а liveness остаётся зелёным.
|
||||||
|
|
||||||
|
**P1-10. Дропы в бридже без метрик.**
|
||||||
|
- `bridge/sender.go:73`: дроп при переполнении RAM-очереди (10000).
|
||||||
|
- `bridge/sender.go:112`: дроп после исчерпания ретраев.
|
||||||
|
|
||||||
|
### P2 — обвязка/гигиена
|
||||||
|
|
||||||
|
**P2-6. TLS.** `sslmode=require` (без verify-full) к PG; EMQX `verify=verify_none`
|
||||||
|
(`emqx.conf:40`).
|
||||||
|
|
||||||
|
**P2-11/P2-12. Нет тестов и CI.** `*_test.go` отсутствует; `.github/workflows`
|
||||||
|
нет (в `.github/` только правила).
|
||||||
|
|
||||||
|
**P2-13. `/metrics` заявлен, но не зарегистрирован.**
|
||||||
|
- `config/defaults.go:6`: в комментарии упомянут `/metrics`, но роут в
|
||||||
|
`router.go` НЕ создан. prometheus — только indirect dep. Rate-limiting нет.
|
||||||
|
|
||||||
|
**P2-14. Легаси не вычищено.**
|
||||||
|
- Остались `cmd/iot-operator`, `controllers/`, `internal/api/` (старый k8s-код),
|
||||||
|
`cmd/mqtt-bridge`, `cmd/sqs-consumer`; k8s/controller-runtime в `go.mod`
|
||||||
|
(~12 строк); бинарники в `bin/`, `ws-probe/ws-probe`.
|
||||||
|
|
||||||
|
**P2-15. Дрейф доков.**
|
||||||
|
- `doc/deployment-nubes-production.md` — образы `v0.1.2`; Makefile — `v0.1.13`.
|
||||||
|
- `loadtests/README.md` — visibility «30с» (в коде 120с), описывает уже
|
||||||
|
исправленную блокировку MQTT-обработчика.
|
||||||
|
|
||||||
|
**P2-16. `emqx.conf`: `cookie = "iot-emqx-cookie-mvp"` захардкожен.**
|
||||||
|
|
||||||
|
### ПЛАТФОРМА (вне нашего кода, но блокирует прод)
|
||||||
|
|
||||||
|
1. **wss-обрывы ~150с** на внешнем шлюзе Nubes (детерминированно, доказано с
|
||||||
|
3 независимых точек: станция, Vultr; внутри кластера и с ВМ — чисто).
|
||||||
|
Тикет Nubes подготовлен (`doc/thinking/nubes-devops-report-2026-08-16.md`).
|
||||||
|
2. **MSS/MTU-баг** — плавающий обрыв ответов >~15–64 КБ (баг нестабилен по
|
||||||
|
маршруту/времени). Обход в коде: offset-пагинация API, внутренний endpoint SQS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Что уже ХОРОШО (не трогать без нужды)
|
||||||
|
|
||||||
|
- Конвейер end-to-end работает и проверен на проде (HISTORY: baseline 2390/2390
|
||||||
|
без потерь/дублей, ACL-нарушения отсекаются, reconnect-storm чистый).
|
||||||
|
- Уже исправлены (по итогам ревью 2026-08-16): асинхронный SQS-диспатчер,
|
||||||
|
VisibilityTimeout 120с, backoff, graceful shutdown, batch-INSERT) — НЕ
|
||||||
|
регрессировать это.
|
||||||
|
- EMQX auth/ACL — напрямую в PostgreSQL, без зависимости от монолита.
|
||||||
|
- MQTT-reconnect с повторной подпиской (OnConnectHandler).
|
||||||
|
- Документация: деплой без kubectl, интеграция с SQS, 10 нагрузочных сценариев.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Что уже записано в этой репе (файлы документации)
|
||||||
|
|
||||||
|
- `doc/thinking/readiness-assessment-2026-10-05.md` — два анализа (5/10 и
|
||||||
|
60–65%) + моё мнение.
|
||||||
|
- `doc/thinking/remediation-plan-2026-10-05.md` — план доработок (4 трека, P0/P1/P2).
|
||||||
|
- `doc/thinking/opus-review-prompt.md` — промпт для ревью (allowlist файлов).
|
||||||
|
- **ВНИМАНИЕ:** эти 3 файла лежат в ветке `master` (коммит `a4e8fd5`, запушен),
|
||||||
|
а НЕ на актуальной `feature/replace-kafka-with-sqs`, где живёт код.
|
||||||
|
`doc/sqs-integration.md`, `doc/deployment-nubes-production.md`, `HISTORY/` —
|
||||||
|
на feature-ветке.
|
||||||
|
|
||||||
|
**Backup-ветки (сохранение состояний):**
|
||||||
|
- `backup/2026-08-17-feature-sqs` (старая точка).
|
||||||
|
- `backup/2026-10-05-feature-sqs` (сделана 2026-10-05, на `12db7d3`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Рекомендуемый порядок работ (по плану)
|
||||||
|
|
||||||
|
1. P0-1 + P0-2 — tenant-граница (связать `sub` с namespace) + обязательная подпись JWT.
|
||||||
|
2. P0-7 + P0-8 — доставка без потерь (ack только после успешного flush) + dedup (message-id).
|
||||||
|
3. P0-5 — выключить `AUTH_TEST_MODE`.
|
||||||
|
4. P0-3 + P1-4 — пароли (не отдавать повторно/хешировать) + удалить `/internal/mqtt/*`.
|
||||||
|
5. P1-9 + P1-10 — честный `/health` + метрики дропов.
|
||||||
|
6. P1-11 + P1-12 — Go-тесты + CI.
|
||||||
|
7. P2 (гигиена: легаси, доки, cookie, TLS).
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Промпт для ревью (Opus) — IoT готовность к production
|
||||||
|
|
||||||
|
Ты ревьюишь Go-проект IoT. Цель — выявить блокеры, мешающие назвать сервис
|
||||||
|
production-ready. Дай ответ МАКСИМАЛЬНО сжато, без воды: списком, короткими
|
||||||
|
строками, с точными ссылками на файл и строку. Никаких пояснений-простыней.
|
||||||
|
|
||||||
|
**Запрещено:**
|
||||||
|
- Не читай память, историю сессий, заметки, предыдущие анализы, `HISTORY/`,
|
||||||
|
`LEGACY.md`, ничего в `legacy/`. Только код и документация из списка ниже.
|
||||||
|
- Не читай старые k8s-манифесты, CRD, оператор — это легаси, не актуально.
|
||||||
|
- Не предлагай рефакторинг ради рефакторинга — только то, что блокирует прод.
|
||||||
|
|
||||||
|
**Компоненты продукта (актуально):** монолит `iot-service` (API + MQTT-bridge +
|
||||||
|
SQS-consumer в одном бинарнике) + EMQX + shared-SQS + managed PostgreSQL.
|
||||||
|
Конвейер: устройство → wss → EMQX → бридж → SQS → consumer → PG → REST API.
|
||||||
|
|
||||||
|
**Смотри ТОЛЬКО эти файлы (актуальный код):**
|
||||||
|
|
||||||
|
Конфигурация и точка входа:
|
||||||
|
- `cmd/iot-service/main.go`
|
||||||
|
- `internal/service/config/config.go`
|
||||||
|
- `internal/service/config/defaults.go`
|
||||||
|
|
||||||
|
HTTP API:
|
||||||
|
- `internal/service/api/router.go`
|
||||||
|
- `internal/service/api/middleware/auth.go`
|
||||||
|
- `internal/service/api/middleware/logging.go`
|
||||||
|
- `internal/service/api/handler/handler.go`
|
||||||
|
- `internal/service/api/handler/devices.go`
|
||||||
|
- `internal/service/api/handler/mqtt_auth.go`
|
||||||
|
- `internal/service/api/handler/telemetry.go`
|
||||||
|
- `internal/service/api/handler/admin.go`
|
||||||
|
- `internal/service/api/handler/sqsstats.go`
|
||||||
|
|
||||||
|
Хранение:
|
||||||
|
- `internal/service/store/open.go`
|
||||||
|
- `internal/service/store/device.go`
|
||||||
|
- `internal/service/store/devices.go`
|
||||||
|
- `internal/service/store/bridge.go`
|
||||||
|
- `internal/storage/iotpg/iot_telemetry_store.go`
|
||||||
|
|
||||||
|
Бридж и consumer:
|
||||||
|
- `internal/service/bridge/bridge.go`
|
||||||
|
- `internal/service/bridge/handler.go`
|
||||||
|
- `internal/service/bridge/sender.go`
|
||||||
|
- `internal/service/consumer/consumer.go`
|
||||||
|
- `internal/service/consumer/processor.go`
|
||||||
|
- `internal/service/sqsclient/sqsclient.go`
|
||||||
|
|
||||||
|
Инфраструктура:
|
||||||
|
- `Dockerfile`
|
||||||
|
- `Makefile`
|
||||||
|
- `go.mod`
|
||||||
|
- `emqx/emqx.conf`
|
||||||
|
- `loadtests/README.md` (только для понимания критериев, не весь код)
|
||||||
|
|
||||||
|
**Вопросы (ответь строго по пунктам, каждому — вердикт + файл:строка):**
|
||||||
|
|
||||||
|
1. Tenant-изоляция: как `sub` из JWT связывается с `{namespace}` в URL?
|
||||||
|
Есть ли путь доступа к чужому namespace валидным токеном?
|
||||||
|
2. Гарантии доставки: есть ли окно потери сообщения между буфером записи,
|
||||||
|
ответом об успехе и удалением из SQS? Где именно?
|
||||||
|
3. Идемпотентность: защищён ли вход от дублей при at-least-once SQS?
|
||||||
|
4. `/health`: отражает ли реальную работоспособность bridge/consumer или
|
||||||
|
всегда `ok`?
|
||||||
|
5. Auth JWT: валидация sub/exp/подписи — что при пустом `JWT_HMAC_SECRET`?
|
||||||
|
6. Пароли устройств: как хранятся, как отдаются через API.
|
||||||
|
7. `/internal/mqtt/auth` и `/internal/mqtt/acl`: нужны ли ещё, защищены ли?
|
||||||
|
8. Ошибки записи в PG: что происходит с сообщением при сбое flush-батча?
|
||||||
|
9. Есть ли тесты (`*_test.go`), CI, rate-limiting, метрики, TLS-верификация PG?
|
||||||
|
|
||||||
|
**Формат ответа:** по каждому пункту — 1 строка вердикт (OK / ПРОБЛЕМА) +
|
||||||
|
`файл:строка` + при проблеме — 1 строка суть. В конце — список
|
||||||
|
«блокеры для прода» (только критические, без ранжирования по вкусу).
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Оценка готовности IoT к production (2026-10-05)
|
||||||
|
|
||||||
|
> Три взгляда на один репозиторий: анализ A, анализ B и моё мнение.
|
||||||
|
> Все три — только чтением кода и документации, без сборки и запуска тестов
|
||||||
|
> (правила репо: команды — только через SSH на ВМ; сборку/прогоны не выполняли).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Анализ A (первый) — итог 5/10
|
||||||
|
|
||||||
|
**Вердикт:** цельный MVP, но по коду не считать production-ready для
|
||||||
|
надёжной multi-tenant эксплуатации.
|
||||||
|
|
||||||
|
### Блокеры
|
||||||
|
- **Не проверяется принадлежность namespace пользователю.** Middleware
|
||||||
|
проверяет JWT, но не передаёт `sub` обработчикам для сравнения с namespace
|
||||||
|
из URL. CRUD и выдача пароля устройства берут namespace из запроса.
|
||||||
|
Валидный токен не ограничивает пользователя его tenant-ом:
|
||||||
|
`auth.go:20`, `router.go:54`, `devices.go:179`. `exp` фактически необязателен
|
||||||
|
(`auth.go:120`).
|
||||||
|
- **Путь потери уже принятых сообщений.** Flusher заменяет буфер на пустой
|
||||||
|
ДО записи в PG, при ошибке только логирует. Consumer при этом удаляет
|
||||||
|
сообщения из SQS после добавления в буфер: `iot_telemetry_store.go:129`,
|
||||||
|
`:135`, `consumer.go:81`.
|
||||||
|
- **Health зелёный при остановившемся pipeline.** Ошибки запуска bridge/consumer
|
||||||
|
только логируются; горутина завершается, а `/health` всегда отдаёт `ok`:
|
||||||
|
`main.go:106`, `router.go:74`.
|
||||||
|
- **Доставка допускает потери и дубли.** Переполненная RAM-очередь bridge
|
||||||
|
сбрасывает сообщения; исчерпание SQS-ретраев заканчивается drop. При
|
||||||
|
at-least-once нет идемпотентного ключа против дублей: `sender.go:70`, `:112`.
|
||||||
|
|
||||||
|
### Что хорошо
|
||||||
|
Компоненты сведены в понятный единый сервис; есть MQTT-reconnect с повторной
|
||||||
|
подпиской, SQS-ретраи, graceful shutdown, разделение хранения устройств и
|
||||||
|
телеметрии, широкий набор нагрузочных сценариев.
|
||||||
|
|
||||||
|
### Чего не хватает до уверенного релиза
|
||||||
|
- Автоматических Go-тестов (`*_test.go` не найдено) и CI.
|
||||||
|
- Согласованности доков с кодом: гайд деплоя — `iot-service:v0.1.2`, Makefile —
|
||||||
|
`v0.1.13`; loadtest README описывает уже исправленную блокировку
|
||||||
|
MQTT-обработчика и visibility 30с, код задаёт 120с:
|
||||||
|
`deployment-nubes-production.md:3`, `Makefile:4`, `README.md:85`, `config.go:114`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Анализ B (второй) — итог ~60–65%
|
||||||
|
|
||||||
|
**Вердикт:** рабочий MVP / технический прототип, не продакшн-готовый сервис.
|
||||||
|
|
||||||
|
### Что хорошо
|
||||||
|
- Конвейер устройство → EMQX → мост → SQS → consumer → PG (per-tenant)
|
||||||
|
работает end-to-end; подтверждено HISTORY и нагрузочными прогонами.
|
||||||
|
- Прогоны: baseline 2390/2390 без потерь/дублей; ACL-нарушения отсекаются;
|
||||||
|
reconnect-storm прошёл чисто.
|
||||||
|
- Код небольшой (~2,4 тыс. строк в монолите), аккуратный: graceful shutdown,
|
||||||
|
асинхронный SQS-диспатчер с ретраями, batch-insert, backoff, constant-time
|
||||||
|
сравнение паролей.
|
||||||
|
- Документация подробная: деплой без kubectl, интеграция с SQS, лог ревью и
|
||||||
|
фиксов, 10 нагрузочных сценариев.
|
||||||
|
- Auth и ACL в EMQX работают напрямую через PostgreSQL, без зависимости от монолита.
|
||||||
|
|
||||||
|
### Что мешает назвать готовым
|
||||||
|
1. **Нет ни одного Go-теста и нет CI.** Только Python-нагрузочные скрипты;
|
||||||
|
в `.github/` одни правила.
|
||||||
|
2. **Изоляция тенантов слабая.** JWT проверяет только `sub` и `exp`; namespace
|
||||||
|
из URL не сверяется с токеном. Без `JWT_HMAC_SECRET` подпись не проверяется.
|
||||||
|
Любой «валидный» токен читает устройства и пароли любого namespace.
|
||||||
|
3. **Пароли устройств в БД открытым текстом** и отдаются через GET. EMQX —
|
||||||
|
`password_hash_algorithm = plain`.
|
||||||
|
4. **`AUTH_TEST_MODE=true`** (любой Bearer) был включён на проде для диагностики.
|
||||||
|
HISTORY §38 — «вернуть false»; закрытия не видно.
|
||||||
|
5. **`/internal/mqtt/auth` и `/internal/mqtt/acl`** открыты на публичном порту
|
||||||
|
без авторизации; дублируют PG-auth в EMQX и уже не нужны. ACL выдаёт права
|
||||||
|
моста любому клиенту с `clientid`, совпадающим с мостовым.
|
||||||
|
6. **Нерешённая платформенная проблема:** плавающий обрыв ответов >~15–64 КБ и
|
||||||
|
WebSocket-обрывы ~150с; стабильного воспроизведения нет, разбор с DevOps
|
||||||
|
идёт. В коде обходится пагинацией.
|
||||||
|
7. **Отложено:** whitelist тенантских БД (любой namespace создаёт базу), DLQ,
|
||||||
|
soak 24ч.
|
||||||
|
8. **Легаси не вычищено:** `cmd/iot-operator`, `controllers/`, `internal/api/`,
|
||||||
|
`cmd/mqtt-bridge`, `cmd/sqs-consumer`; в `go.mod` 12 строк k8s/controller-runtime;
|
||||||
|
бинарники в `bin/` и `ws-probe/ws-probe`. `LEGACY.md` обещает убрать после
|
||||||
|
переписывания хендлеров, а оно сделано.
|
||||||
|
9. **Дрейф конфигурации:** `cookie = "iot-emqx-cookie-mvp"` захардкожен в
|
||||||
|
`emqx.conf`; параметры EMQX задаются только через env в deck-UI.
|
||||||
|
10. **Нет TLS-верификации** к PG (`verify_none`). Нет rate limiting и метрик
|
||||||
|
(только `/health` и admin-статистика).
|
||||||
|
|
||||||
|
### Минимум для продакшна
|
||||||
|
- привязать namespace к JWT, сделать `JWT_HMAC_SECRET` обязательным;
|
||||||
|
- убедиться, что `AUTH_TEST_MODE` выключен;
|
||||||
|
- хешировать пароли устройств или не отдавать повторно;
|
||||||
|
- удалить `/internal/mqtt/*` и легаси;
|
||||||
|
- добавить тесты и CI;
|
||||||
|
- закрыть вопрос обрывов на платформе.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Моё мнение (перепроверено по коду)
|
||||||
|
|
||||||
|
Оба анализа компетентны и в главном совпадают. Расхождение 5/10 vs 60–65% —
|
||||||
|
не в фактах, а в весе, который они дают одним и тем же находкам. Спорные места
|
||||||
|
я перепроверил сам — оба правы по существу.
|
||||||
|
|
||||||
|
### Подтверждается кодом (не предположение)
|
||||||
|
1. **Изоляция тенантов слабая** — верно. `middleware/auth.go` проверяет JWT
|
||||||
|
только на `sub`+`exp`; `sub` не сверяется с `{namespace}` (claims не
|
||||||
|
передаются в обработчики, `router.go` не извлекает `sub`). `exp` необязателен
|
||||||
|
(`if claims.Exp > 0`), без `JWT_HMAC_SECRET` подпись не проверяется.
|
||||||
|
2. **Путь потери принятых сообщений** — верно. `processTelemetry` → `InsertTelemetry`
|
||||||
|
кладёт в in-memory буфер и возвращает nil, после чего consumer безусловно
|
||||||
|
удаляет сообщение из SQS. `flushAll` подменяет буфер на пустой ДО записи в PG
|
||||||
|
и при ошибке только логирует. Данные могут молча пропасть.
|
||||||
|
3. **`/health` зелёный при упавшем pipeline** — верно. `main.go` ошибки
|
||||||
|
bridge/consumer только логирует; горутина завершается, `healthHandler` всегда
|
||||||
|
отдаёт `ok`.
|
||||||
|
4. **Нет Go-тестов и CI** — верно. `*_test.go` отсутствует, `.github/` — правила.
|
||||||
|
5. **`AUTH_TEST_MODE` и пароли plaintext** — верно и задокументировано
|
||||||
|
(`password_hash_algorithm=plain`, `AUTH_TEST_MODE=true` для DevOps, §38).
|
||||||
|
6. **Дрейф доков** — верно: гайд `v0.1.2` vs Makefile `v0.1.13`; loadtest README
|
||||||
|
30с vs код 120с. Анализ A зафиксировал точнее.
|
||||||
|
|
||||||
|
### Где анализ B точнее/дополняет
|
||||||
|
- Явно оговорил, что сборку/тесты не запускал (ограничение правил — SSH).
|
||||||
|
- Глубже разобрал открытые `/internal/mqtt/*`, TLS `verify_none`, захардкоженный
|
||||||
|
`cookie`, невычищенное легаси и k8s-зависимости в `go.mod`.
|
||||||
|
|
||||||
|
### Где я бы поправил обоих
|
||||||
|
- **Проценты условны.** Оба дают один вывод «MVP/прототип, не production-ready»;
|
||||||
|
цифры — риторика, а не измерение.
|
||||||
|
- **Приоритет блокеров:** главные технические — потеря сообщений и tenant-граница.
|
||||||
|
Plaintext-пароли и unauthed `/internal/mqtt/*` — важные, но второго эшелона
|
||||||
|
(периметр платформы + EMQX уже на PG-auth напрямую снижают остроту).
|
||||||
|
|
||||||
|
### Итог
|
||||||
|
**Рабочий MVP, пригодный для демо/пилота, но не production-ready.** Главные
|
||||||
|
блокеры: слабая tenant-изоляция, гарантированная потеря сообщений при сбое
|
||||||
|
flush, «слепой» health, отсутствие Go-тестов/CI и не закрытая платформенная
|
||||||
|
проблема wss ~150с / MTU. Моя оценка — между A и B, ближе к
|
||||||
|
«функционально готов, эксплуатационно — нет».
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# План доработок IoT — до production-ready (2026-10-05)
|
||||||
|
|
||||||
|
> Основа: два независимых анализа + ревью Опуса (построчные ссылки) + моя
|
||||||
|
> перепроверка по коду. Сгруппировано в 4 трека, внутри — по приоритету.
|
||||||
|
> Приоритеты: P0 — потеря/компрометация данных или чужие данные; P1 — важные
|
||||||
|
> риски эксплуатации; P2 — обвязка/гигиена; ПЛАТФОРМА — вне нашего кода.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Трек 1. Безопасность / tenant-изоляция
|
||||||
|
|
||||||
|
### P0-1. Связать JWT `sub` с `{namespace}`
|
||||||
|
- Сейчас: `validateJWT` проверяет только наличие `sub`/`exp`/подписи, `sub`
|
||||||
|
никак не сверяется с `{namespace}` из URL (`middleware/auth.go:62`); обработчики
|
||||||
|
берут ns из пути (`handler/devices.go:91`, `handler/telemetry.go:19`).
|
||||||
|
- Что: извлекать `sub` из claims, передавать в контекст запроса, в middleware
|
||||||
|
(`/v1`) сверять с `{namespace}` → 403 при несовпадении.
|
||||||
|
- Файлы: `middleware/auth.go`, `router.go`, `handler/*.go`.
|
||||||
|
|
||||||
|
### P0-2. Сделать проверку подписи обязательной
|
||||||
|
- Сейчас: при пустом `JWT_HMAC_SECRET` подпись не проверяется — токен
|
||||||
|
подделывается (`auth.go:70`); дефолт секрета пустой, не в required
|
||||||
|
(`config.go:118`).
|
||||||
|
- Что: при `AUTH_TEST_MODE=false` требовать `JWT_HMAC_SECRET` (иначе отказ на
|
||||||
|
старте); `exp` сделать обязательным.
|
||||||
|
- Файлы: `config.go`, `auth.go`.
|
||||||
|
|
||||||
|
### P0-3. Не отдавать пароль устройства повторно + хешировать
|
||||||
|
- Сейчас: пароль в БД открытым текстом (`store/open.go:21` mqtt_password TEXT),
|
||||||
|
отдаётся через `GET /devices/{name}` (`handler/devices.go:189`); EMQX
|
||||||
|
`password_hash_algorithm name=plain` (`emqx.conf:46`).
|
||||||
|
- Что: пароль отдавать единожды при создании; в БД хранить hash; EMQX перевести
|
||||||
|
на алгоритм хэша. Миграция существующих строк.
|
||||||
|
- Файлы: `store/open.go`, `handler/devices.go`, `emqx/emqx.conf`.
|
||||||
|
|
||||||
|
### P1-4. Удалить HTTP-auth/ACL `/internal/mqtt/auth|acl`
|
||||||
|
- Сейчас: EMQX ходит по PG-auth напрямую (`emqx.conf`, `backend=postgresql`).
|
||||||
|
HTTP-эндпоинты — неиспользуемый fallback, висят на публичном порту 9090 без
|
||||||
|
аутентификации (`router.go:50-51`) → перебор паролей устройств.
|
||||||
|
- Что: удалить роуты и обработчики `MQTTAuth`/`MQTTAcl` (и зависимый код).
|
||||||
|
- Файлы: `router.go`, `handler/mqtt_auth.go`.
|
||||||
|
|
||||||
|
### P1-5. Включить `AUTH_TEST_MODE=false` на проде
|
||||||
|
- Сейчас: включён для диагностики DevOps (HISTORY §38); любой Bearer проходит.
|
||||||
|
- Что: выставить false + валидный `JWT_HMAC_SECRET`, убедиться в деплое.
|
||||||
|
|
||||||
|
### P2-6. TLS верификация PG и EMQX
|
||||||
|
- Сейчас: `sslmode=require` (без verify-full) и EMQX `verify=verify_none`
|
||||||
|
(`emqx.conf:40`) — шифрование без проверки сертификата.
|
||||||
|
- Что: `sslmode=verify-full` при наличии CA; убрать `verify_none` где возможно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Трек 2. Надёжность доставки телеметрии
|
||||||
|
|
||||||
|
### P0-7. Устранить окно потери сообщения (SQS-удаление до записи в PG)
|
||||||
|
- Сейчас: `consumer.go:78` удаляет из SQS после `processTelemetry`==nil, но
|
||||||
|
`InsertTelemetry` при неполном батче лишь кладёт в память и возвращает nil
|
||||||
|
(`iot_telemetry_store.go:356-376`); flush-ошибка неполного батча только
|
||||||
|
логируется (`iot_telemetry_store.go:133`). Краш/сбой flush = безвозвратная потеря.
|
||||||
|
- Что: либо синхронная запись (убрать «возврат nil до записи»), либо явный
|
||||||
|
acknowledge только после подтверждения flush + при ошибке возвращать сообщение
|
||||||
|
в очередь (видимость + не удалять).
|
||||||
|
- Файлы: `consumer/processor.go`, `iotpg/iot_telemetry_store.go`.
|
||||||
|
|
||||||
|
### P0-8. Идемпотентность входа (dedup)
|
||||||
|
- Сейчас: `iot_telemetry` id BIGSERIAL без ключа дедупликации; envelope без
|
||||||
|
message-id (`bridge/handler.go:19`). At-least-once SQS → молчаливые дубли.
|
||||||
|
- Что: добавить message-id в envelope, UNIQUE-индекс/ON CONFLICT DO NOTHING по
|
||||||
|
(namespace, message_id) в таблице телеметрии.
|
||||||
|
- Файлы: `bridge/handler.go`, `iotpg/iot_telemetry_store.go`, схема БД.
|
||||||
|
|
||||||
|
### P1-9. `/health` отражает реальное состояние
|
||||||
|
- Сейчас: `healthHandler` всегда `{"status":"ok"}` (`router.go:72`), не отражает
|
||||||
|
bridge/consumer/PG/SQS; liveness зелёный при упавшем consumer (`main.go:106`).
|
||||||
|
- Что: ready-проверки подсистем; `main().Run` bridge/consumer — не глотать
|
||||||
|
ошибку, а помечать неготовность; `/healthz` (liveness) отделить от `/ready`
|
||||||
|
(readiness).
|
||||||
|
- Файлы: `router.go`, `main.go`.
|
||||||
|
|
||||||
|
### P1-10. Bridge: поведение при переполнении и ретраях — осознанно
|
||||||
|
- Сейчас: дроп при переполнении RAM-очереди (`sender.go:73`) и после 3 ретраев
|
||||||
|
(`sender.go:112`) — потери без метрик.
|
||||||
|
- Что: метрики дропов; при исчерпании ретраев — не молчаливый дроп, а
|
||||||
|
лог+счётчик (или при желании DLQ в shared-sqs).
|
||||||
|
- Файлы: `bridge/sender.go`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Трек 3. Тесты, CI, метрики
|
||||||
|
|
||||||
|
### P1-11. Go-тесты
|
||||||
|
- Сейчас: `*_test.go` нет ни одного.
|
||||||
|
- Что: unit-тесты критичных мест — `validateJWT` (tenant-привязка),
|
||||||
|
`InsertTelemetry`/`flushAll` (потеря), `processor` (дубли), `devices` (CRUD +
|
||||||
|
пароль), `mqtt_auth` (constant-time). Приоритет — на путях из P0-1..P0-8.
|
||||||
|
- Файлы: `*_test.go` рядом с пакетами.
|
||||||
|
|
||||||
|
### P1-12. CI
|
||||||
|
- Сейчас: `.github/workflows` нет; в `.github/` только правила.
|
||||||
|
- Что: workflow: `go vet` + `go test ./...` + `gofmt` check; сборка образа.
|
||||||
|
- Файлы: `.github/workflows/ci.yml`.
|
||||||
|
|
||||||
|
### P2-13. `/metrics` + rate-limiting
|
||||||
|
- Сейчас: `/metrics` заявлен в комментарии (`config/defaults.go:6`), но роут не
|
||||||
|
зарегистрирован в `router.go`; prometheus — только indirect dep. Rate-limit нет.
|
||||||
|
- Что: зарегистрировать `/metrics` (если решено), добавить rate-limiting на `/v1`.
|
||||||
|
- Файлы: `router.go`, `middleware/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Трек 4. Гигиена репозитория и документов
|
||||||
|
|
||||||
|
### P2-14. Вычистить легаси
|
||||||
|
- Сейчас: `cmd/iot-operator`, `controllers/`, `internal/api/`, `cmd/mqtt-bridge`,
|
||||||
|
`cmd/sqs-consumer`; k8s/controller-runtime в `go.mod` (12 строк); бинарники в
|
||||||
|
`bin/`, `ws-probe/ws-probe`. `LEGACY.md` обещает убрать после переписывания
|
||||||
|
хендлеров — оно сделано.
|
||||||
|
- Что: удалить легаси-код (после P1-4), `go mod tidy`, убрать бинарники из дерева.
|
||||||
|
- Файлы: `go.mod`, дерево.
|
||||||
|
|
||||||
|
### P2-15. Согласовать доки с кодом (дрейф)
|
||||||
|
- Сейчас: `doc/deployment-nubes-production.md` — образы `v0.1.2`; Makefile —
|
||||||
|
`v0.1.13`; `loadtests/README.md` — visibility 30с (в коде 120с) и старая
|
||||||
|
блокировка MQTT-обработчика (уже исправлена).
|
||||||
|
- Что: привести версии и параметры в доках к фактическим.
|
||||||
|
- Файлы: `doc/deployment-nubes-production.md`, `loadtests/README.md`.
|
||||||
|
|
||||||
|
### P2-16. Emqx cookie из env
|
||||||
|
- Сейчас: `cookie = "iot-emqx-cookie-mvp"` захардкожен (`emqx.conf`).
|
||||||
|
- Что: задавать cookie через env/деплой.
|
||||||
|
- Файлы: `emqx/emqx.conf`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ПЛАТФОРМА (вне нашего кода, но блокирует прод)
|
||||||
|
|
||||||
|
1. **wss обрывы ~150с** на внешнем шлюзе (детерминированно, доказано с 3 точек).
|
||||||
|
Тикет Nubes; до фикса — устройства должны авто-переподключаться (MQTT
|
||||||
|
keepalive ≤60с).
|
||||||
|
2. **MSS/MTU-баг** — обрыв ответов >~15–64 КБ. Обход уже есть (offset-пагинация,
|
||||||
|
внутренний SQS endpoint); нужен фикс на стороне Nubes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Рекомендуемый порядок выполнения
|
||||||
|
|
||||||
|
1. P0-1, P0-2 (tenant-граница + подпись) — закрывают горизонтальный доступ.
|
||||||
|
2. P0-7, P0-8 (доставка + dedup) — закрывают потерю/дубли.
|
||||||
|
3. P0-5 (выключить AUTH_TEST_MODE) — быстро, на проде.
|
||||||
|
4. P0-3, P1-4, P1-5 (пароли + лишние эндпоинты).
|
||||||
|
5. P1-9, P1-10 (health + метрики дропов).
|
||||||
|
6. P1-11, P1-12 (тесты + CI).
|
||||||
|
7. P2-13..P2-16 (гигиена).
|
||||||
Reference in New Issue
Block a user