# Журнал сессии — 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 13–14.08): - Контейнер = k8s deployment `containerk8s`, namespace = UUID инстанса (`f1ffb134-7d16-45bd-8bef-69f6ec8ab33c`), контейнер в поде называется `app`, порт 4100. - Внутренний DNS: `containerk8s..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..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. - `Makefile` — `docker-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:10–08:29:03): | t (с) | Событие | |---|---| | 0.4 | CONNECTED | | 10–120 | echo 1..12 — все возвращаются ✓ | | 20.4–140.4 | PING сервера каждые 20с — доходят ✓ | | 50.5–125.5 | PONG сервера на ping клиента каждые 25с — доходят ✓ | | ~150 | ping клиента (25с-каденс) — БЕЗ pong; echo 16–17 пропали; 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..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с — обрыв окончательно локализуется на внешнем краевом шлюзе (вне кластера), как и таймауты 31–33с 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: таймауты 31–33с (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 20–25с). Доказано: станция 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: таймауты 31–33с (~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с, таймауты 31–33с, 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.md` → `legacy/` - `Dockerfile` → `legacy/Dockerfile.old` - `Makefile` → `legacy/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..svc.cluster.local`. 3. Патч EMQX через kubectl (один раз, платформа не откатывает — прецедент shared-sqs): `kubectl -n set env deployment/containerk8s EMQX_AUTHENTICATION__1__URL=http://containerk8s..svc.cluster.local:9090/internal/mqtt/auth EMQX_AUTHORIZATION__SOURCES__1__URL=http://containerk8s..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. --- ## 19. Очередь iot-telemetry создана в shared-sqs (16.08) - Создана через внешний endpoint `https://sqs.containerk8s.dev.nubes.ru`, тенант `t-fec713a719ad0b33` (креды — в SQS-репо, HANDOFF-NEW-CHAT.md). - QueueUrl: `http://us-east-1.goaws.com:4100/t-fec713a719ad0b33/iot-telemetry`, list-queues подтверждает. - Монолит использует те же креды тенанта + внутренний endpoint (дефолт вшит в образ: containerk8s.f1ffb134-…:4100). **Готово к созданию контейнеров в deck-UI (порядок):** 1. EMQX: образ `naeel/iot-emqx:latest` → узнать UUID namespace. 2. iot-service: образ `naeel/iot-service:latest` + env (см. ниже). 3. kubectl-патч auth/ACL URL в EMQX (команда в разделе 16). --- ## 20. EMQX-контейнер: ошибки deck и 502 — разбор (16.08) ### Ошибка 1: «Не указан applicationUsePort, expose содержит несколько портов» - **Факт:** в форме deck-UI ПОЛЯ порта НЕТ — платформа берёт порт из EXPOSE образа. - **Причина:** базовый `emqx/emqx:5.5.1` имеет 7 EXPOSE-портов (18083, 1883, 4370, 5369, 8083, 8084, 8883), которые невозможно убрать из дочернего Dockerfile (`docker inspect` подтверждает — порты наследуются). - **Решение:** образ пересобран от чистого `debian:bookworm-slim` с deb-пакетом EMQX 5.5.1: единственный `EXPOSE 8083`. v0.1.1, digest `sha256:7de0ec8a…01dd08f`. Проверка: `{"8083/tcp":{}}`. - **Урок:** для платформы «Простой HTTP контейнер» образ обязан иметь РОВНО один EXPOSE-порт; наследуемые порты базового образа — блокер. ### Ошибка 2: 502 Bad Gateway (домен https://exqx.containerk8s.dev.nubes.ru/) - **Диагностика:** deployment `containerk8s` в ns `2fdd9658-4995-494f-892a-d88dda7da1ee`, READY 0/1, рестарты; лог пода: `/usr/bin/emqx: line 457: ps: command not found` - **Причина:** launcher-скрипт EMQX использует `ps`, а `debian:bookworm-slim` не содержит пакета `procps`. - **Решение:** добавлен `procps` в установку; v0.1.2, digest `sha256:d3722528…debb30`, запушен (latest + v0.1.2). EXPOSE по-прежнему один. - **Действие пользователя:** redeploy контейнера EMQX в deck-UI (образ `naeel/iot-emqx:latest` — платформа подтянет новый digest по latest). ### Состояние - EMQX-контейнер пока работает на старом сломанном образе — после redeploy проверить: под Ready 1/1, лог без `ps: command not found`, wss-подключение. - Образы: iot-emqx v0.1.2 (latest), iot-service v0.1.0 (latest) — оба на Docker Hub. --- ## 21. EMQX: mqtt_path исправлен, но на платформе boot висит (16.08) **Ошибка 3 (найдена локальным запуском):** `unknown_fields path=listeners.ws.default` — поле `mqtt_path` в EMQX 5.5.1 живёт в `listeners.ws.default.websocket.mqtt_path`. Исправлено в emqx.conf → v0.1.3/0.1.4. **Локальная проверка v0.1.4 (docker run на станции):** ПРОЙДЕНА — boot ~50с, `emqx ctl listeners` OK (tcp:1883 running), wscat с subprotocol `mqtt` подключается (curl без subprotocol получает 400 от Cowboy — так и должно: fail_if_no_subprotocol). **На платформе (наш кластер, ns 2fdd9658-…):** v0.1.4 поставлен kubectl set image, под Running 1/1, НО: - нода НЕ поднимается 5+ мин: beam запущен (config сгенерирован в 09:43:36), CPU всего ~5с (почти idle), /var/log/emqx ПУСТ, `emqx ctl status/listeners` возвращают пустоту (нет даже ошибки); - множество зомби `inet_gethost ` (DNS-хелперы Erlang); - DNS в поде работает (getent OK), resolv.conf: search …svc.cluster.local, ndots:5; - память 257/512Mi, рестартов 0; - снаружи — 502 Bad Gateway (wscat: Unexpected server response: 502). - PID1 = `su - emqx -c DEBUG=0 "/usr/bin/emqx" foreground` (поведение deb-пакета), HOME=/var/lib/emqx, node name emqx@127.0.0.1, -start_epmd false, -proto_dist ekka. **Статус:** локально образ рабочий, на платформе boot висит. По договорённости с пользователем — вопрос к Sonnet (диагностика + правильный способ запуска EMQX в контейнере: без su, логи в stdout, env-конфигурация).