Files
IoT/HISTORY/2026-08-16-session-log.md
T

47 KiB
Raw Blame History

Журнал сессии — 2026-08-16 (IoT: интеграция с shared-sqs, выбор пути миграции)

Рабочая папка: /home/naeel/nubes/IoT

Правило этой сессии (задано пользователем): ВСЁ документировать в HISTORY, каждый шаг — сразу.


1. Контекст (состояние на входе)

  • Пользователь завершил handoff по shared-sqs:
    • HANDOFF-NEW-CHAT.md — в SQS-репозитории, закоммичен и запушен (5fa34c2).
    • doc/sqs-integration.md — создан в IoT-репозитории, не закоммичен (untracked).
  • 2026-08-13-migration-to-managed.md — также untracked в IoT-репо.
  • Git IoT: HEAD 368ff06 (feature/replace-kafka-with-sqs), origin отстаёт на 1 коммит (HEAD не запушен: «fix(ingress): remove cert-manager + ssl-redirect»).

2. doc/sqs-integration.md — что важно для IoT (прочитан, суть)

  • shared-sqs: прод v0.1.35, все тесты зелёные (api 22/22, sdk 15/15, FIFO/DLQ e2e), нагрузка 168 591 операция — 0 сервисных ошибок.
  • Внешний endpoint: https://sqs.containerk8s.dev.nubes.ru (регион us-east-1).
  • Внутренний endpoint (поды того же кластера Nubes, realm iot-naeel): http://containerk8s.f1ffb134-7d16-45bd-8bef-69f6ec8ab33c.svc.cluster.local:4100.
  • Проблемы ПЛАТФОРМЫ (не сервиса):
    1. Таймауты каждые 31–33с на внешнем пути (~5.5% запросов без ответа 30с). Доказано tcpdump; внутри кластера потерь нет (port-forward 24977 раундов, 0 сбоев).
    2. MSS=1448 при MTU 1400 → тела >~1400 байт виснут ~51с (PMTUD сломан). Тикет Nubes подготовлен (SQS-service/doc/thinking/nubes-ticket.md).
  • Рекомендации интеграции: AWS SDK + override endpoint; главная — внутренний адрес (обходит шлюз полностью); ретраи ≥3, read_timeout ≥30с (для внешнего пути); сообщения до ~1300 байт; VisibilityTimeout 30с+; FIFO требует MessageGroupId (dedup 5 мин); DLQ через RedrivePolicy; лимит 50 очередей на тенанта.
  • Креды детерминированы из email: tenantID t-+sha256(email)[:8], AccessKey SSAK-+sha256(email)[:12], SecretKey sha256(email+":shared-sqs:secret-key:v1").
  • Блокер прода — только тикет Nubes (для интернет-клиентов); для внутренней интеграции блокеров нет.

3. Как сейчас развёртывается IoT (факты из репозитория)

  • Код полностью на Go 1.25, модуль gitea.services.ngcloud.ru/Nail/IoT.
  • Один образ Docker Hub naeel/iot-operator:v0.2.6 (distroless), три бинарника: /iot-operator, /mqtt-bridge, /sqs-consumer.
  • Собственный k8s кластер iot-naeel, namespace sless, API 185.247.187.149:6443.
  • Деплой с ВМ: ssh naeel@5.172.178.213, каталог /home/naeel/terra/IoT, kubectl apply -f deployments/k8s/ (kubeconfig на ВМ, токен ~24ч).
  • Компоненты:
    • iot-operator — controller-manager (CRD IoTDevice) + REST API :9090;
    • iot-mqtt-bridge — EMQX → SQS SendMessage;
    • iot-sqs-consumer — SQS → Postgres (long poll 20с, delete после записи);
    • EMQX 5.5.1 — MQTT-брокер (HTTP auth → operator).
  • Секреты (ns sless): iot-bridge-credentials, iot-sqs-credentials (SQS_ENDPOINT=https://qu.kube5s.ru — старый внешний адрес, устарел), iot-postgres-secret (managed PG, ...svc.cluster.local:5432/sqsdb).
  • Ingress: устройства по wss://iot.kube5s.ru/mqtt.
  • Миграция на managed Node.js (план от 13.08) — НЕ выполнена; Node.js-кода в репо нет.

4. Обсуждение: HTTP-контейнер vs Managed Node.js (вывод этой сессии)

  • Вывод AI: HTTP-контейнер (как у shared-sqs) лучше: Go сохраняется целиком, Dockerfile готов, схема доказана shared-sqs, внутренняя сеть без таймаутов. Managed Node.js = полный rewrite (aedes, Express, @aws-sdk/client-sqs, pg) — запасной.
  • Открытый риск контейнера: поддержка WebSocket upgrade на платформенном ingress не проверена (устройства ходят по wss).
  • Устройства в любом варианте ходят через внешний шлюз Nubes — проблемы шлюза (5.5% таймаутов, MSS) остаются риском для wss в обоих вариантах.

5. Чтение SQS-репозитория (/home/naeel/nubes/SQS-service) — выяснено

  • План Node.js фактически отменён. doc/2026-08-13-migration-to-nubes.md: «Код не меняется вообще. Только инфраструктура».
  • shared-sqs мигрировал в «Простой HTTP контейнер» (nubes_http) через deck-UI, realm iot-naeel. Домен sqs.containerk8s.dev.nubes.ru, /health OK.
  • Технические факты платформы (из HISTORY 1314.08):
    • Контейнер = k8s deployment containerk8s, namespace = UUID инстанса (f1ffb134-7d16-45bd-8bef-69f6ec8ab33c), контейнер в поде называется app, порт 4100.
    • Внутренний DNS: containerk8s.<uuid-inst>.svc.cluster.local:4100.
    • Внешний: *.containerk8s.dev.nubes.ru, ingress/TLS встроены в контейнер.
    • Образ — ТОЛЬКО публичный Docker Hub (Gitea/GHCR приватные платформа не тянет).
    • Обновление образа — уникальным тегом (latest кэшируется зеркалом mirror.k8s.ngcloud.ru); через kubectl set image deployment/containerk8s app=... (правки через kubectl не откатываются — ownerReferences нет).
    • Env vars — в deck-UI; Managed Redis — redisk8s.<uuid>.svc.cluster.local:6379.
    • JWT-валидация: https://lk-api-gateway.ngcloud.ru/api/v1/svc (deck-api*.ngcloud.ru — ЛЕГАСИ, не использовать).
    • Long-running работает: SQS long-poll 20с; HTTP-соединение через ingress держится ~150с (не режется).
  • WebSocket в SQS-истории не упоминается вообще — поддержка wss на платформе не проверялась. Единственный реальный риск контейнерного пути для IoT.

6. Вывод и следующий шаг

  • Путь миграции IoT (кандидат): «Простой HTTP контейнер» в realm iot-naeel, Go без переписывания; убрать operator/CRD → таблица iot_devices в Postgres; bridge и consumer остаются Go.
  • Первая проверка перед стартом: WebSocket upgrade (wss) через платформенный ingress — тестовый контейнер с WS-эндпоинтом + wscat.
  • ЖДЁМ решения пользователя по пути миграции.

7. Создан ws-probe — тестовый контейнер для проверки wss

Команда пользователя: «делай» (создать пробник + залить образ на Docker Hub; для запуска в deck-UI нужен путь до образа).

Что сделано (все шаги):

  1. Создан каталог ws-probe/ в IoT-репозитории:
    • main.go — Go echo-сервер на gorilla/websocket: GET /health (JSON status+version), GET /ws (echo + ping каждые 20с, read-deadline 60с, сбрасывается pong'ами — мёртвые соединения закрываются), GET / (HTML). Без кредов, без внешних зависимостей.
    • Dockerfile — multi-stage golang:1.25 → distroless/static:nonroot, порт 8080 (меняется env PORT), версия через ldflags.
    • Makefiledocker-build/docker-push, VERSION=v0.1.0, IMAGE=naeel/iot-ws-probe.
    • go.mod/go.sum — отдельный модуль (gorilla/websocket v1.5.3), чтобы не тащить зависимость в основной go.mod IoT.
  2. .gitignore — добавлен ws-probe/ws-probe (сборочный бинарник).
  3. Локальные проверки: go vet OK; smoke: /health{"status":"ok","version":"v0.1.0"}; WS-апгрейд → HTTP/1.1 101 Switching Protocols (Sec-WebSocket-Accept корректный).
  4. Docker: логин в Docker Hub под naeel активен (проверен docker info).
  5. Образ собран и запушен (публичный):
    • naeel/iot-ws-probe:v0.1.0:latest),
    • digest sha256:dc029be3e79fb37f15ce23477096e340ab181b96c0132dc2e443f341e36f0688,
    • размер 10.9MB.
  6. Коммиты: 27cb40e (ws-probe), 2a7ecd2 (HISTORY). Пуш НЕ делался (не просили).

Инструкция для deck-UI («Простой HTTP контейнер»):

  • Образ: naeel/iot-ws-probe:latest — стабильный путь БЕЗ уникального тега. ПРАВИЛО (от пользователя): в deck-UI путь к образу фиксируется при создании и НЕ редактируется — с тегом :v0.1.0 мы не смогли бы поменять версию. Обновление версии — новым пушем в latest + redeploy. ⚠️ Оговорка: зеркало платформы кэширует latest (случай shared-sqs) — если redeploy подтянет старый digest, запасной путь: kubectl set image deployment/containerk8s app=naeel/iot-ws-probe:<новый-уникальный-тег>.
  • Env vars: не нужны (порт 8080 — дефолт; при необходимости PORT).
  • Никаких кредов не требуется.

Критерии проверки wss после деплоя (будут применены):

  • curl -i -H "Connection: Upgrade" -H "Upgrade: websocket" ... → 101;
  • wscat -c wss://<домен>/ws → подключение, echo, соединение живёт 5–10 мин;
  • переподключения стабильны; ping/pong проходят.

Следующий шаг: пользователь создаёт контейнер в deck-UI с этим образом — далее тест wss по критериям выше.


8. Контейнер задеплоен в deck-UI — тест wss (16.08, ~07:26 GMT+03)

Контейнер: https://testiot.containerk8s.dev.nubes.ru/ (создан пользователем в deck-UI, образ naeel/iot-ws-probe:latest).

Результаты проверок (первые):

  1. GET /health{"status":"ok","version":"v0.1.0"} — под стянул наш образ.
  2. GET / → HTML-страница ws-probe (200).
  3. WebSocket upgrade через платформенный ingress → HTTP/1.1 101 Switching Protocols, Sec-WebSocket-Accept корректный, HSTS на месте. (В выводе curl первая строка HTTP/1.1 200 Connection established — локальный CONNECT-прокси машины, ответ 101 пришёл от платформы.)
  4. wscat -c wss://.../ws -x "probe-echo-1" → echo вернулось, exit 0. Двусторонний обмен работает.

КЛЮЧЕВОЙ ВЫВОД: платформа Nubes («Простой HTTP контейнер») пропускает WebSocket upgrade — wss для устройств возможен, путь контейнера подтверждён.

Длительный тест стабильности (запущен 08:26:10 по date = 07:26 GMT+03):

  • скрипт /tmp/ws-stability-test.py: соединение 10 мин, echo каждые 10с, ping/pong обе стороны (клиент ping_interval=25, сервер ping 20с), лог с таймстемпами. Результат будет дописан ниже.

РЕЗУЛЬТАТ длительного теста (16.08, обрыв на 173.1с) — ПРОБЛЕМА

Соединение оборвалось на 173.1с (08:29:03 по date = 07:29 GMT+03). Хронология (полный лог выше в терминале, 08:26:1008:29:03):

t (с) Событие
0.4 CONNECTED
10120 echo 1..12 — все возвращаются ✓
20.4140.4 PING сервера каждые 20с — доходят ✓
50.5125.5 PONG сервера на ping клиента каждые 25с — доходят ✓
~150 ping клиента (25с-каденс) — БЕЗ pong; echo 1617 пропали; PING сервера 160.4 не пришёл
170.1 клиент: ERROR ping/pong timed out
173.1 CLOSED; итог sent=17, echoed=15

Анализ: первые 140с — идеально (двусторонний обмен, ping/pong обе стороны). Деградация началась ~145–155с, обрыв ~150–173с. Похоже на платформенный лимит жизни соединения на внешнем пути ~150с (перекликается с квирком shared-sqs: «GET /ui через ingress не закрывает соединение ~150с»).

Что это значит: если лимит подтвердится — wss-устройства будут отваливаться каждые ~2.5–3 мин независимо от MQTT keepalive (обрыв при активном трафике, не по idle). Миграция на контейнер НЕ отменяется, но перед стартом нужно выяснить и снять лимит (тикет Nubes).

Следующие диагностические шаги (предложены, ждут «делай»):

  1. Повторить тест 3 раза — воспроизводимость и точное время обрыва.
  2. Логи пода пробника на платформе (kubectl logs на ВМ) — что видит сервер в момент обрыва (read error / ping error).
  3. Тест WS изнутри кластера на внутренний containerk8s.<uuid>.svc.cluster.local:8080 — если держится >10 мин: обрыв на внешнем шлюзе/ingress; если тоже рвётся: проблема ближе к поду.
  4. Держать обычное HTTP-соединение через ingress >150с — сравнение с WS.
  5. По результатам — тикет в Nubes (лимит соединений на ingress/шлюзе).

9. Диагностика обрыва (16.08, команда «делай тесты»)

9.1 Инфраструктура пробника на платформе (через ВМ, kubeconfig живой)

  • Namespace пробника: 2d9b96ff-0a50-41e5-a5d5-1cd9e5621555, deployment containerk8s, под containerk8s-5dc7dcbfcf-d4ch6 (IP 172.16.2.227), service containerk8s ClusterIP 10.106.45.135:8080.
  • Рядом стандартные: pod vmagent (метрики), ns shared-sqs f1ffb134-... (2d9h), старый чужой ns df36c8af-... (30d) — не трогали.
  • Ingress платформы: ns ingress, shturval-ingress-controller-controller (2 пода: 172.16.0.73 / 172.16.3.149), LB 185.247.187.151 (kube-vip). DNS testiot.containerk8s.dev.nubes.ru → 185.247.187.151.
  • В логах пробника источник WS-подключений = IP ingress-подов (172.16.0.73 / 172.16.3.149) — соединения терминируются на nginx.

9.2 Серверная сторона обрыва (логи пода)

  • Тест 1: сервер получал echo каждые 10с до 04:28:40 UTC, затем ПАУЗА ~24с, в 04:29:04 пришли СРАЗУ ДВА сообщения (echo-15 задержан на ~14с, echo-16 на ~4с) и сразу ws read error: close 1000 (normal) — закрыл КЛИЕНТ после своего ping-timeout. Серверных ошибок нет.
  • Вывод: сервер и под ни при чём — соединение застряло между клиентом и nginx.

9.3 Nginx (ingress) — чисто

  • Access-логи обоих подов: /ws → 101, request_time = 173.851с / 173.850с (полная жизнь соединения), статус 101, ошибок нет. Nginx НЕ закрывает соединение и не видит аномалий.
  • X-Forwarded-For показывает клиентов как 192.168.255.10 / 192.168.255.71 — перед LB есть краевой шлюз/NAT (внешний путь Nubes).
  • В error-логах за окно обрыва — только фоновые SSL-сканеры и посторонние POST /graphql — к нашему обрыву отношения не имеют.

9.4 Воспроизводимость (внешний путь, 3 прогона)

  • RUN1: обрыв ровно на 173.1с (ERROR ping timeout 170.1с, CLOSED 173.1с, sent=17, echoed=15) — идентично тесту 1. Детерминированно.
  • RUN2: запущен, ожидание (ожидаемый обрыв ~08:38:39 по date).
  • Прогоны идут на РАЗНЫЕ ingress-поды — обрыв не зависит от пода.

9.5 Внутренний путь (port-forward с ВМ) — ИДЁТ

  • kubectl -n 2d9b96ff-... port-forward svc/containerk8s 18080:8080 (ВМ), клиент node+ws (модуль ws установлен на ВМ в /tmp/wstest).
  • Тест 600с, старт 07:34:55 MSK. На 155с — ЗДОРОВ: PING сервера каждые 20с, PONG клиента каждые 25с (с 25-й секунды!), ECHO ok[12] на 120с.
  • Различие с внешним путём видно уже по pong-каденсу: внутри pong приходит с 25с, снаружи первый pong только на 50с — внешний шлюз «съедает» что-то уже на 25-й секунде.
  • Если внутренний тест доживёт 600с — обрыв окончательно локализуется на внешнем краевом шлюзе (вне кластера), как и таймауты 3133с shared-sqs.

9.6 Предварительный вывод

  • Контейнер, под, сервис, nginx — здоровы. Детерминированный обрыв внешних WS-соединений на ~150–173с происходит на краевом шлюзе Nubes.
  • Для IoT это значит: wss-устройства будут переподключаться каждые ~2.5–3 мин, пока шлюз не починят (тикет Nubes, объединить с таймаутами 31–33с и MSS).

9.7 Финальные результаты всех тестов

Тест Путь Клиент Результат
Тест 1 станция → внешний wss python обрыв 173.1с
RUN1 станция → внешний wss python обрыв ровно 173.1с
RUN2 станция → внешний wss python обрыв 163.4с (stall на ~150с)
RUN3 станция → внешний wss python обрыв 163.7с (stall на ~150с)
VM-ext ВМ → внешний wss node+ws чисто 300.2с, 29/29 echo
VM-int ВМ → port-forward → под node+ws чисто 600.1с, 59/59 echo
echo.org станция → публичный WS-echo python чисто 200с+ (тест 300с)
proxy станция → прокси 172.17.192.1:10808 → wss python обрыв 163.9с (как прямой)

Выводы:

  1. Под/сервис/nginx платформы — полностью здоровы (VM-ext 300с чисто, VM-int 600с чисто; nginx request_time = полное время жизни, ошибок нет).
  2. Обрыв воспроизводится ТОЛЬКО с рабочей станции, причём и напрямую, и через локальный прокси (172.17.192.1:10808) — 5/5 обрывов, точка сбоя ~150с.
  3. Публичный WS-echo (echo.websocket.org) с той же станции — ЧИСТО. Значит сеть станции в целом здорова; страдает именно маршрут станция → Nubes (185.247.187.151).
  4. ВМ (другой маршрут до платформы, p50 6–8мс по данным shared-sqs) — чисто.
  5. ⚠️ Нюанс для вывода «шлюз Nubes»: станция и ВМ ходят разными маршрутами. Если ВМ обходит внешний шлюз Nubes (короткий путь/пиринг) — то «таймер ~150с на внешнем шлюзе Nubes» подтверждается. Если ВМ проходит тот же шлюз — тогда проблема в маршруте станции (ISP/прокси-сеть). Точно различить может только тест с третьей независимой точки входа (другой ISP, мобильная сеть).
  6. Взаимосвязь с shared-sqs: таймауты 3133с (HTTP) и MSS 1448 наблюдались с той же станции — вероятно, тот же проблемный маршрут/шлюз. Наши данные добавляют к картине «~150с на долгоживущие соединения».

9.8 Следующие шаги (предложены, ждут «делай»)

  1. Тест wss с третьей независимой точки (другой ISP/мобильная сеть) — решает вопрос «шлюз Nubes vs ISP станции».
  2. HTTP-цикл с той же станции на /health (запрос каждые 10с, 5 мин) — связать с таймаутами 31–33с shared-sqs.
  3. Тикет в Nubes: wss-соединения с интернета рвутся на ~150с (детерминированно),
    • таймауты 31–33с и MSS 1448 (данные shared-sqs).
  4. При проектировании IoT: устройствам нужен MQTT keepalive ≤60с + автореконнект, а до фикса шлюза — считать интернет-подключения устройств нестабильными (~2.5–3 мин жизни).

9.9 Тест с третьей точки (Vultr, 95.179.252.111) — ГИПОТЕЗА ПОДТВЕРЖДЕНА

Доступ: root@95.179.252.111, ключ ~/.ssh/vultr_openssh (на ВМ-машине). Python 3.10, websocket-client 1.9.0 установлен. Тот же скрипт, 300с.

Тест Результат
Vultr → testiot wss обрыв 163.3с (stall ~150с) — ИДЕНТИЧНО станции
Vultr → echo.websocket.org чисто (175с+, pong 150.1с)

ИТОГОВЫЙ ВЫВОД (доказано с трёх точек):

  • Обрыв долгоживущих WS-соединений на ~150с — на краевом шлюзе платформы Nubes (внешний вход 185.247.187.151), а не в поде/ingress и не в ISP клиента: два независимых интернет-маршрута (станция, Vultr) дают идентичный обрыв, контрольные публичные WS-echo с обеих точек чисты.
  • ВМ 5.172.178.213 ходит коротким маршрутом (p50 6–8мс) и шлюза не касается — поэтому чиста.
  • Внутри платформы (port-forward) всё идеально (600с, 59/59).
  • Для IoT: wss-устройства из интернета будут отваливаться каждые ~2.5–3 мин. Это блокер для прода; тикет Nubes обязателен (объединить с таймаутами 31–33с и MSS 1448 из shared-sqs — тот же шлюз).

9.10 Рекомендации для тикета Nubes (готовы данные)

  • Симптом: WS-соединение (wss через edge) детерминированно рвётся на ~150с при активном трафике (echo каждые 10с, ping/pong 2025с). Доказано: станция 5/5, Vultr 1/1; nginx внутри видит полную жизнь соединения (request_time 173.85с), под чист; изнутри кластера и с ВМ — чисто.
  • Воспроизведение: wscat -c wss://testiot.containerk8s.dev.nubes.ru/ws (или python websocket-client, ping 25с / echo 10с) — обрыв на 150–173с.
  • Связь с shared-sqs: таймауты 3133с (~5.5% HTTP-запросов), MSS=1448 при MTU 1400 — вероятно, тот же внешний шлюз.

10. Консультация Sonnet (промпт → ответ) — критическая оценка

Что сделано: составлен промпт для Sonnet (жёсткий список файлов: HISTORY 2026-08-16, migration-to-managed, sqs-integration, architecture/current-v0.2.3, deployment-v0.2.3; 9 вопросов; формат «Ответ/Рекомендация/Риски», без кода). Ответ получен, оценён ниже.

Ключевые рекомендации Sonnet (приняты):

  • Компоновка: EMQX-контейнер + Go-монолит (API+bridge+consumer в одном бинарнике).
  • EMQX оставить (не mochi-mqtt), если настроится на платформе.
  • SQS в середине потока СОХРАНИТЬ (буфер при падении PG, admin-stats).
  • Auth: перенос Secrets→таблица iot_devices ДО переключения; закрыть authTestMode и ADMIN_STATS_TOKEN до прода.
  • 3 фазы миграции; откат — DNS обратно; критерий готовности переключения устройств: wss ≥600с с 3 независимых интернет-точек.
  • Вопросы в техподдержку Nubes: кастомный домен, лимит ~150с, таймауты 3133с, MSS, subprotocol mqtt, multi-port, PVC, latest-pull, лимиты.

ПОПРАВКИ к ответу Sonnet (важно):

  1. PG НЕ меняется. Managed PostgreSQL уже используется текущим consumer'ом (IOT_PG_DSN → postgresqlk8s…svc.cluster.local). Утверждение Sonnet «новые данные пишутся в новую PG» — ОШИБКА. Телеметрию переносить не нужно вовсе; переносятся только CRD-объекты (устройства+пароли).
  2. Платформенные контейнеры живут в ТОМ ЖЕ кластере, что и старый деплой IoT: ноды iot-naeel-*, ingress shturval — один кластер. Значит старый вход iot.kube5s.ru (185.247.187.147), скорее всего, идёт через ТОТ ЖЕ краевой шлюз — вариант Sonnet «EMQX на старом кластере как точка входа» проблему не обходит (и его сомнение в обратную сторону неверно). Проверяемо тестом: держать wss к wss://iot.kube5s.ru/mqtt 5+ мин — если stall ~150с есть и там, текущие устройства уже страдают.
  3. Multi-port у Sonnet заявлен как факт в п.1 («1883 внутренний для bridge»), но в п.9.6 сам же помечен НЕИЗВЕСТНО — противоречие. Вероятный реальный сценарий: один порт на контейнер → EMQX наружу только 8083 (wss), bridge к EMQX тоже по wss внутри платформы.
  4. Конфиг EMQX через env — не блокер. EMQX 5 поддерживает env vars (EMQX_*); плюс можно собрать свой образ на базе emqx/emqx с вшитым emqx.conf (публичный Docker Hub) — снимает ограничение полностью.
  5. /metrics уже есть в текущем операторе (порт 8080, controller-runtime) — переносится в монолит, а не «отсутствует» (ошибка Sonnet).
  6. bcrypt для паролей — ок для НОВЫХ устройств, но для переноса старых нужен скрипт: прочитать все Secrets iot-{deviceId} в ns sless и вставить в таблицу с сохранением работоспособности (хэшировать при вставке).
  7. Дубли при at-least-once: нужен dedup-ключ в InsertTelemetry (например уникальный индекс по (namespace, device_id, received_at) или message-id) — сейчас отсутствует.

Следующий шаг (предложен, ждёт «делай»):

  • Тест wss к СТАРОМУ прод-домену wss://iot.kube5s.ru/mqtt (5 мин, станция + Vultr) — определить, страдают ли текущие устройства от того же шлюза.

11. Статус старого деплоя IoT — «зомби» (проверено 16.08)

ЛЕГАСИ — НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ при принятии решений. Раздел оставлен только как фиксация факта. Старый IoT для миграции значения не имеет.

Проверка по вопросу «старый прод жив?» (факты):

  1. ns sless жив (125d). Все 4 deployment'а Running: emqx, iot-mqtt-bridge, iot-operator, iot-sqs-consumer (поды 117–125 дней без рестартов).
  2. Внешний вход мёртв: iot.kube5s.ru не резолвится ни с ВМ (getent пусто), ни со станции (getent exit 2); curl → 000/exit 35. DNS-запись снята — устройства через него не ходят.
  3. Конвейер телеметрии мёртв с 14.08: секрет iot-sqs-credentials в sless содержит старый SQS_ENDPOINT=https://qu.kube5s.ru (удалён 14.08 вместе со старым shared-sqs). Логи consumer (16.08, прямо сейчас): каждые 5с ERROR SQS ReceiveMessage failed ... lookup qu.kube5s.ru: no such host. Bridge подключён к EMQX, но сообщений нет (нет устройств).

Вывод: старый IoT — «зомби»: поды формально Running, но внешнего входа нет (DNS снят), а SQS-звено указывает на удалённый сервис. Работающих устройств на старом кластере нет → риск миграции «не сломать прод-устройства» минимален, переносить по сути нечего (только CRD-объекты/пароли при желании).

Отмена теста: wss-тест к старому домену бессмыслен — точки входа нет. Старый деплой можно выключать после переключения (или по команде пользователя — сейчас НЕ трогаем).


12. Пометка ЛЕГАСИ всего, что относится к старому IoT (16.08)

Команда пользователя: «УБЕРИ ВСЁ, что касается старого IoT; стирать пока не надо; ЧЁТКО пропиши везде, что это ЛЕГАСИ и НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ».

Сделано:

  1. В шапку каждого файла старого IoT добавлена пометка « ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT ... НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ»:
    • 15 документов в doc/ (кроме doc/sqs-integration.md — актуален);
    • 2026-08-13-migration-to-managed.md (старый Node.js-план — отменён);
    • все 8 манифестов deployments/k8s/;
    • config/crd/bases/iot.kube5s.ru_iotdevices.yaml;
    • cmd/iot-operator/main.go, controllers/iotdevice_controller.go, api/v1alpha1/device_types.go, api/v1alpha1/groupversion_info.go;
    • Makefile (старые k8s-цели);
    • examples/ (README.md, main.tf, handler.py).
  2. Создан корневой LEGACY.md — реестр: что легаси, что актуально.
  3. Раздел 11 этого журнала помечен «НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ».
  4. Удалений НЕ производилось.

Актуальные файлы (НЕ легаси): HISTORY/, doc/sqs-integration.md, ws-probe/, Go-код cmd/mqtt-bridge/, cmd/sqs-consumer/, internal/ (переиспользуется в монолите).


13. Аудит вмешательства старого IoT + план Фазы 1 (16.08)

Команда пользователя: «НЕ ДОПУСКАТЬ, чтобы старые данные и файлы от прежнего IoT вмешивались в проект. ПРОВЕРЬ.»

Результаты аудита (факты):

  1. Код: k8s-зависимости есть ТОЛЬКО в internal/api/handler/:
    • handler.go — controller-runtime client, runtime;
    • iot_device_handler.go — corev1, apimachinery, metav1, CRD-типы iotv1alpha1 (CRUD устройств через K8s + MQTT-auth через Secrets);
    • iot_admin_stats_handler.go — corev1, controller-runtime. Всё остальное ЧИСТО: router.go, middleware/, UI-embeds, iotpg/, cmd/mqtt-bridge/, cmd/sqs-consumer/.
  2. go.mod: тяжёлые k8s-депы (k8s.io/api, apimachinery, client-go v0.26.0, controller-runtime v0.14.1 + ~40 indirect) — держатся только из-за этих трёх файлов. После переписывания хендлеров — go mod tidy их уберёт.
  3. Dockerfile (корень) — собирал 3 старых бинарника; НЕ был помечен. → помечен ЛЕГАСИ (новый монолит получит собственный Dockerfile).
  4. CI: в .github/ только правила (copilot-instructions, pravila) — старых пайплайнов нет. bin/ в git не отслеживается.
  5. Данные: старый shared-sqs (qu.kube5s.ru) удалён полностью — старых очередей в новом shared-sqs нет. В managed PG остались старые тестовые tenant-DB (изолированы; очистка — только по отдельной команде). Старые k8s Secrets новым кодом не читаются.

Правило для монолита (зафиксировано):

  • Новый код НЕ импортирует controllers/, api/v1alpha1/, k8s-клиент.
  • Хендлеры устройств переписываются на PG (iot_devices), MQTT-auth — SQL.
  • LEGACY.md дополнен разделом аудита.

План Фазы 1 (согласован в обсуждении, ждёт «делай»):

  1. Новый cmd/iot-service/main.go + структура по пакетам (config, api, bridge, consumer, storage/iotpg) — много мелких файлов, без god-файлов.
  2. Хендлеры устройств/админки — на PG вместо K8s; authTestMode=false.
  3. Bridge+consumer — переиспользуются как есть (MQTT-подписка, SQS long-poll).
  4. Новый Dockerfile (один бинарник), образ naeel/iot-service (имя — на подтверждении пользователя), env-дефолты вшиты, секреты — в deck.
  5. Сборка → пуш latest → пользователь создаёт контейнер в deck-UI.

14. Перенос старых файлов в legacy/ (16.08, команда «делай»)

Всё старое физически вынесено из активного дерева (git mv, история сохранена):

  • deployments/legacy/deployments/
  • config/legacy/config/
  • examples/legacy/examples/
  • старые доки doc/ (api, architecture, decisions, thinking, deployment*, iot-mvp-plan, progress, run-and-test, testing-v0.2.6, errors, infrastructure) → legacy/doc/
  • 2026-08-13-migration-to-managed.mdlegacy/
  • Dockerfilelegacy/Dockerfile.old
  • Makefilelegacy/Makefile.old

В корне осталось активное: cmd/ (bridge, consumer; оператор — до Фазы 1), internal/, doc/sqs-integration.md, HISTORY/, ws-probe/, go.mod, LEGACY.md.

Проверка: go build ./... — OK (компиляция не сломана, Go-код не переносился).


15. Фаза 1: монолит iot-service (16.08, команда «делай»)

Структура кода (много мелких файлов, без god-файлов):

  • cmd/iot-service/main.go — точка входа: config → PG → SQS-клиент → API + горутины bridge/consumer; graceful shutdown.
  • internal/service/config/ — env-конфигурация: config.go (Load + проверка секретов), defaults.go (все дефолты).
  • internal/service/store/ — устройства в PG (таблица iot_devices): open.go (схема), device.go (модель+ошибки), devices.go (CRUD + auth-lookup).
  • internal/service/sqsclient/ — общий SQS-клиент (endpoint override, внутренний адрес shared-sqs).
  • internal/service/api/ — router.go (маршруты + CORS + /health), middleware/ (auth.go — authTestMode из env, logging.go), handler/ (devices, mqtt_auth, telemetry, admin, sqsstats), admin_embed/console_embed + ui/ (HTML скопирован).
  • internal/service/bridge/ — bridge.go (подключение к EMQX, подписка +/telemetry/+), handler.go (envelope → SQS).
  • internal/service/consumer/ — consumer.go (long-poll, VisibilityTimeout), processor.go (envelope → iotpg).

Ключевые решения:

  1. Устройства — таблица iot_devices в PG (PRIMARY KEY namespace+name, UNIQUE namespace+device_id); пароль генерится crypto/rand 32B hex при создании; MQTT-auth — SQL + constant-time compare; last_connected обновляется.
  2. k8s-зависимостей в новом коде НЕТ: старые хендлеры не тронуты, старый оператор компилируется. k8s-депы в go.mod остаются до удаления легаси.
  3. Admin-stats: PG (iotpg.GetAdminStats) + SQS (атрибуты очереди) + runtime (version/uptime) — вместо статусов k8s-подов.
  4. Env: дефолты вшиты в Dockerfile (порт 9090, внутренний SQS endpoint f1ffb134...:4100, очередь iot-telemetry, регион us-east-1, long-poll 20с, visibility 30с, clientID iot-bridge, MQTT 8083//mqtt, AUTH_TEST_MODE=false). Секреты обязательны: IOT_PG_DSN, SQS_ACCESS_KEY, SQS_SECRET_KEY, MQTT_USERNAME, MQTT_PASSWORD (+ ADMIN_STATS_TOKEN для статистики).
  5. CORS "*" (консоль на домене платформы), /health и /healthz отдают {"status":"ok","version":...}.

Проверки: go build ./... OK; go vet OK; gofmt чистый; go test ./... OK; smoke: без env бинарник падает с перечнем недостающих переменных.

Образ: naeel/iot-service:v0.1.0 (+latest), digest sha256:808b537fcbd1ca23e2c389a735162370645ed8188fec7ef0d6e09327b78cb568, 15.2MB, запушен на Docker Hub. Коммит 9d08473.

Следующий шаг: пользователь создаёт контейнер в deck-UI (образ naeel/iot-service:latest, секреты в env). Затем — EMQX-контейнер и проверки.


16. EMQX-образ для платформы + порядок деплоя контейнеров (16.08)

Создан emqx/ (коммит b5333b1): Dockerfile на базе emqx/emqx:5.5.1 с вшитым emqx.conf:

  • node.name/cookie/data_dir (обязательные поля EMQX 5);
  • HTTP auth/ACL → монолит (/internal/mqtt/auth, /internal/mqtt/acl);
  • listeners: ws 8083 (mqtt_path=/mqtt) — единственный порт контейнера, tcp 1883 и dashboard 18083 — для диагностики, наружу не экспонируются;
  • auth/ACL URL переопределяются env: EMQX_AUTHENTICATION__1__URL, EMQX_AUTHORIZATION__SOURCES__1__URL (в образе — заглушки).

Образ: naeel/iot-emqx:latest (+v0.1.0), digest sha256:6c8a12188e673fdd243cd926187c9207cb2837dfe617997e6478c99a7bc1bc84, 476MB.

Порядок деплоя (env после создания не меняются):

  1. EMQX-контейнер: образ naeel/iot-emqx:latest, порт 8083. Узнать UUID namespace EMQX (kubectl get ns / из deck).
  2. iot-service-контейнер: образ naeel/iot-service:latest, env:
    • секреты: IOT_PG_DSN, SQS_ACCESS_KEY, SQS_SECRET_KEY, MQTT_USERNAME, MQTT_PASSWORD, ADMIN_STATS_TOKEN;
    • MQTT_HOST = containerk8s.<uuid-EMQX>.svc.cluster.local.
  3. Патч EMQX через kubectl (один раз, платформа не откатывает — прецедент shared-sqs): kubectl -n <ns-emqx> set env deployment/containerk8s EMQX_AUTHENTICATION__1__URL=http://containerk8s.<ns-iot>.svc.cluster.local:9090/internal/mqtt/auth EMQX_AUTHORIZATION__SOURCES__1__URL=http://containerk8s.<ns-iot>.svc.cluster.local:9090/internal/mqtt/acl
  4. ДО создания iot-service: креды тенанта IoT в shared-sqs (консоль /ui → Credentials) + очередь iot-telemetry.

17. ПРОВЕРКА PostgreSQL (16.08, команда «проверь ПГ») — старый PG МЁРТВ

ДИСЦИПЛИНА (напоминание пользователя): всё от старого IoT НЕ УЧИТЫВАТЬ — ни старые DSN, ни старые креды, ни старые БД. Ниже — только фиксация факта проверки. Для нового проекта используются ТОЛЬКО новые ресурсы.

Поправка к прошлым утверждениям: «PG уже работает, тот же, что у старого consumer» — НЕВЕРНО. Проверено фактически:

  1. Namespace dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5 (из старого DSN) — НЕ СУЩЕСТВУЕТ (NotFound). Старый DSN postgresqlk8s-master.dc5db45d-… не резолвится — мёртв.
  2. Живой managed PG есть — другой инстанс:
    • ns 509145c3-ba71-4687-a084-d9dcbc88d3d2 (125d),
    • под postgresqlk8s-0 (3/3 Running, 19d), Zalando postgres-operator,
    • svc postgresqlk8s-master ClusterIP 10.104.214.32:5432,
    • секреты кредов: postgres / standby / superuser / testuser (Zalando).
  3. Проверка подключения (port-forward 15432 → master):
    • pg_isready → accepting connections;
    • старые креды IoT (super/<старый пароль>) → FATAL password authentication failed;
    • суперюзер инстанса (superuser из секрета) → OK;
    • БД инстанса: autotest, postgres, template0, template1, testdb. sqsdb ОТСУТСТВУЕТ (старых баз IoT нет).
  4. Вывод: для монолита нужен НОВЫЙ DSN на инстанс 509145c3-…: postgresql://superuser:<пароль>@postgresqlk8s-master.509145c3-….svc.cluster.local:5432/sqsdb
    • предварительно создать БД sqsdb (owner superuser). iotpg сам создаст tenant_credentials и per-tenant БД.
  5. Осталось на ВМ: port-forward PG (процесс на 15432) — остановить по команде (pkill -f "port-forward.*15432").

18. PG подготовлен для монолита (16.08)

Факты из консоли платформы (пользователь): инстанс postgresqlk8s, instanceUid 509145c3-ba71-4687-a084-d9dcbc88d3d2, internalMaster postgresqlk8s-master.509145c3-….svc.cluster.local. Пользователи superuser/testuser — роль ddl_user (createdb, createrole), НЕ настоящие PG-суперюзеры (поэтому CREATE DATABASE ... OWNER требует GRANT роли — ошибка «must be able to SET ROLE» в первом прогоне).

Создано (новые ресурсы, к старому IoT отношения не имеют):

  1. Роль iot_service (LOGIN, сгенерированный пароль).
  2. БД iotdb (OWNER iot_service) — через GRANT→CREATE→REVOKE.
  3. Проверка подключения под iot_service → OK (user=iot_service db=iotdb).

DSN для deck-UI (пароль НЕ хранится в git — только в чате/деке): postgresql://iot_service:<пароль>@postgresqlk8s-master.509145c3-ba71-4687-a084-d9dcbc88d3d2.svc.cluster.local:5432/iotdb?sslmode=disable

iotpg при старте сам создаст tenant_credentials и per-tenant БД (как и раньше), store сам создаст таблицу iot_devices.