- Добавлен HTML SPA: internal/api/ui/iot-console.html Ванильный JS + mqtt.js (CDN), без фреймворков. Страницы: вход, список устройств, credentials, эмулятор MQTT, заглушка телеметрии. - Добавлен go:embed: internal/api/console_embed.go, GET /console - Добавлен CORS middleware в router.go для http://iot.kube5s.ru - Ingress emqx-ws-ingress.yaml: /console → sless-operator:9090 - Версия образа v0.1.53, задеплоен Доступно: http://iot.kube5s.ru/console
14 KiB
Лог мышления — 2026-04-04-02
Агент: GitHub Copilot (Claude Sonnet 4.6)
Продолжение сессии от 2026-04-04 (файл 2026-04-04.md достиг 681 строки).
Задача: MQTT WebSocket через nginx-ingress — отладка 404 → 400
Контекст
Порт 1883 заблокирован NSX-T Edge firewall на уровне облака.
Временный workaround: EMQX WebSocket listener (8083) → nginx-ingress → iot.kube5s.ru/mqtt.
Что нашёл
Проблема 1 — 404: Ingress не получил ADDRESS (IP не присвоен).
- Причина: annotation
nginx.ingress.kubernetes.io/configuration-snippetзаблокирован в nginx-controller v1.12.6. По умолчаниюallow-snippet-annotations=false. - Решение: убрал
configuration-snippetиз Ingress. - Результат: Ingress получил sync (Scheduled for sync), но ADDRESS всё ещё пустой — это нормально для данного контроллера.
Проблема 2 — 400: После удаления snippet стало 400 вместо 404.
- nginx конфиг для
iot.kube5s.ruправильный:proxy_set_header Upgrade $http_upgrade— ЕСТЬ (nginx-ingress добавляет автоматически)proxy_http_version 1.1— ЕСТЬproxy_send_timeout/read_timeout 3600s— ЕСТЬproxy_pass → sless-emqx-ws-8083— ЕСТЬ
- Значит 400 приходит от EMQX, а не от nginx.
Гипотезы почему EMQX возвращает 400
EMQX WebSocket listener конфиг:
listeners.ws.default {
bind = "0.0.0.0:8083"
max_connections = 512
}
Нет явного websocket.path. По умолчанию EMQX ожидает путь /mqtt.
Но nginx из-за pathType: Prefix на /mqtt может передавать путь без trailing slash.
Вариант A: nginx передаёт запрос на /mqtt (без /) — EMQX ожидает /mqtt → должно работать.
Вариант B: EMQX в 5.5.1 ожидает ws://host/mqtt без всякого proxy path rewrite и есть баг с X-Forwarded-Proto — nginx шлёт http, а EMQX может редиректить.
Вариант C: ssl_redirect: true в nginx конфиге означает что HTTP запрос перенаправляется на HTTPS, и там 400.
Следующий шаг
Проверить $ssl_redirect = true в nginx server block для нашего хоста:
- Если да — nginx делает redirect HTTP→HTTPS
- Добавить аннотацию
nginx.ingress.kubernetes.io/ssl-redirect: "false"или тестировать через HTTPS
Правило (новое)
Если файл doc/thinking/ГГГГ-ММ-ДД.md стал большим — создавать ГГГГ-ММ-ДД-02.md, ГГГГ-ММ-ДД-03.md и т.д.
Задача: Анализ безопасности MQTT multitenancy
Что исследовал
Пользователь спросил об угрозах межтенантного проникновения.
Изучил:
emqx.confв поде —authorization { no_match = allow }— после CONNECT всё разрешеноinternal/api/handler/iot_device_handler.go—MQTTAuthвозвращает только{"result":"allow"}без ACL rules
Вывод
Auth (CONNECT) защищён:
- HTTP auth endpoint проверяет namespace+deviceId+password (constant-time compare)
- enabled=true проверяется
- Secret изолирован по namespace
ACL на pub/sub НЕТ:
no_match = allow— аутентифицированный клиент может SUBSCRIBE на любой топик- EMQX HTTP auth plugin поддерживает возврат ACL rules в ответе на auth
- Формат ответа:
{"result":"allow","acl":[{"permission":"allow","action":"all","topic":"sless/ns/+"}]} - Текущий
mqttAuthResponseсодержит толькоResult string— ACL поле отсутствует
Риски по приоритету
- Критично: User A может SUBSCRIBE
sless/#и читать все IoT данные всех пользователей - Средне: Нет rate limit на MQTT — один клиент может flood брокер
- Низко: Нет TLS на 8083 (WebSocket без шифрования) — данные видны в сети
План фикса
Добавить в mqttAuthResponse поле ACL []aclRule и возвращать из MQTTAuth:
{
"result": "allow",
"acl": [
{"permission": "allow", "action": "publish", "topic": "sless/{ns}/{deviceId}"},
{"permission": "allow", "action": "subscribe", "topic": "sless/{ns}/{deviceId}"},
{"permission": "deny", "action": "all", "topic": "#"}
]
}
Ждём подтверждения от пользователя перед реализацией.
Архитектурная дискуссия — IoT телеметрия и хранение данных
Контекст разговора
Пользователь задал вопрос: "куда пишутся данные с IoT датчиков?"
Выяснилось что сейчас данные теряются — function pod получает событие но никуда не сохраняет. Это нормально для serverless (пользователь сам решает), но для IoT платформы нужно автоматическое хранение.
Анализ сценариев использования
Реалистичные клиенты для Nubes (облачный провайдер СНГ, малый/средний бизнес):
- Мониторинг объектов (склады, серверные, торговые точки) — температура, влажность, протечка
- Умные счётчики / ЖКХ — снятие показаний без выезда
- Небольшое производство / агро — теплицы, мини-заводы
Общий паттерн для всех: датчик → данные в БД → алерт если порог → график
Решение по хранению данных
Вопрос: один большой Postgres или отдельный на каждого? Ответ: один Postgres инстанс, но отдельная DATABASE на каждого tenant.
Причины:
- Вариант со одной таблицей + tenant_id — изоляция программная, ошибка в коде = утечка
- Отдельная DATABASE — физическая изоляция, разные connection string, разные пароли
- Клиент B не может подключиться к DATABASE клиента A даже при баге в коде платформы
Структура:
Postgres инстанс
├── sless_platform DB — системные таблицы (tenants, invocations)
├── tenant_abc DB — только данные клиента A
└── tenant_def DB — только данные клиента B
Решение по доступу клиента
Вопрос: давать клиенту прямой доступ к Postgres? Ответ: нет. Только через REST API платформы.
Причины:
- Postgres внутри кластера, снаружи не торчит (security)
- Единый endpoint
iot.kube5s.ru - Легко добавить rate limit, биллинг, кеш
- Клиент не зависит от деталей реализации хранилища
API:
GET /v1/namespaces/{ns}/iot/telemetry?device=X&from=T&to=T
GET /v1/namespaces/{ns}/iot/devices/{id}/last
Решение по schema.sql
Клиент может положить schema.sql рядом с функцией. При деплое платформа выполняет его в БД tenant'а.
Это даёт низкий порог входа — клиент не шарит в Python, но может написать SQL по шаблону.
Ключевое архитектурное решение — разделение операторов
Решение: sless-operator и iot-operator — ОТДЕЛЬНЫЕ компоненты. Пока в одном кластере, но сделать так чтобы могли быть в разных.
Namespace layout:
namespace: sless — платформа sless (operator, event-dispatcher, RabbitMQ, Postgres invocations)
namespace: sless-{hash} — tenant функции (function pods)
namespace: iot — платформа IoT (iot-operator, EMQX, Postgres telemetry)
namespace: iot-{hash} — tenant IoT (IoTDevice CRDs)
Связь:
- Общий идентификатор tenant:
{hash}одинаковый в обоих namespace - MQTT событие → RabbitMQ в sless → function pod в sless-{hash}
- IoT operator НЕ импортирует пакеты sless-operator (loose coupling)
- Общение только через k8s API и RabbitMQ
Postgres:
- sless имеет свой Postgres (invocations)
- iot имеет свой Postgres (telemetry per tenant)
- Разные StatefulSet, разные PVC
Что делает пользователь
Клиент:
- Подключает устройство → данные автоматически пишутся в его
iot_telemetry - Пишет функцию которая реагирует на события
- Функция получает
DB_DSNв env var (автоматически из Secret) - Может делать SELECT/INSERT в свою БД через обычный SQL в коде функции
- Может читать телеметрию через REST API
Plan — следующие шаги (этап IoT Postgres)
- Поднять Postgres StatefulSet в namespace
iot - В iot-operator при создании IoTDevice namespace →
CREATE USER,CREATE DATABASE,CREATE TABLE iot_telemetry,CREATE TABLE iot_devices - Credentials → k8s Secret
iot-tenant-{ns}-pg - В iot-mqtt-bridge при получении MQTT сообщения → INSERT в tenant БД
- REST API endpoint для чтения телеметрии
- При деплое function → прокинуть
DB_DSNв env var из Secret - При деплое function → если есть
schema.sql→ выполнить в tenant БД
Технические решения
- Postgres:
postgres:16-alpineStatefulSet с PVC 10Gi в namespaceiot - Connection pool: pgxpool (pgx v5) per-tenant, lazy init, max 5 conn per tenant
- Таблица telemetry:
(id bigserial, device_id text, ts timestamptz default now(), payload jsonb) - Индекс:
(device_id, ts DESC)для быстрых запросов по устройству за период - Retention: пока без TTL, добавить позже через pg_partman или cron job
2026-04-04 — IoT Console UI: план и реализация
Агент: GitHub Copilot (Claude Sonnet 4.6)
Постановка задачи
Пользователь сформулировал: нужен UI для управления IoT устройствами. Причина: не все пользователи работают через Terraform/API напрямую. Нужно: создать устройство, получить credentials, прошить в устройство, проверить отправку данных.
Ключевое решение: эмулятор устройства в браузере
MQTT WebSocket уже работает: ws://iot.kube5s.ru:80/mqtt.
Браузер через mqtt.js (CDN) может подключиться как устройство напрямую.
Это значит: эмулятор — это не "симуляция", а реальная публикация MQTT сообщений.
Когда у клиента ещё нет физического устройства — он тестирует через эмулятор. Это закрывает весь цикл без необходимости устанавливать MQTT-клиент.
Архитектурные решения UI
Стек: ванильный HTML/CSS/JS + mqtt.js (CDN). Никаких фреймворков.
Где хранить: встраиваем в бинарник оператора через go:embed.
- Файл:
internal/api/ui/iot-console.html - Маршрут:
GET /console
Где доступен: http://iot.kube5s.ru/console
- Ingress добавляем path
/console→sless-operator:9090
Почему не https://sless.kube5s.ru/console:
- UI на HTTPS + MQTT WS без TLS = mixed content, браузер блокирует
- UI на HTTP + MQTT WS = нет mixed content, всё работает
- HTTP → HTTPS API вызовы разрешены (это не mixed content)
- Нужен только CORS на API стороне
CORS: заголовки Access-Control-Allow-Origin: http://iot.kube5s.ru + OPTIONS preflight
Страницы
- Вход: API адрес + MQTT брокер + namespace + токен → localStorage
- Список устройств: таблица, создать, удалить
- Устройство (3 вкладки):
- Credentials: username, password скрыт, топик, инструкция
- Эмулятор: подключиться → JSON payload → send / авто
- Телеметрия: "скоро"
Следующие шаги после UI
- Postgres StatefulSet в namespace
iot - INSERT в iot_telemetry из mqtt-bridge
- REST API для чтения телеметрии
- Заполнить вкладку "Телеметрия" в UI