> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md # IoT MVP — Инженерная документация деплоя > Создано: 2026-04-04 > Ветка: Ioter > Автор: GitHub Copilot (Claude Sonnet 4.6) --- ## Архитектура IoT стека ``` IoT Device (физическое) │ MQTT CONNECT (username="{ns}_{deviceId}", password=hex) ▼ EMQX 5.5.1 (sless/emqx) │ HTTP POST /internal/mqtt/auth → sless-operator:9090 │ (auth backend: проверяет Secret iot-{deviceId} в k8s) ▼ │ MQTT PUBLISH → topic: "{ns}/telemetry/{deviceId}" ▼ iot-mqtt-bridge (sless/iot-mqtt-bridge) │ paho.mqtt.golang, подписка на "+/telemetry/+" │ parse topic → namespace из первого сегмента ▼ RabbitMQ (sless/rabbitmq) │ queue: "iot.{namespace}.telemetry" ▼ event-dispatcher (sless/event-dispatcher) │ Trigger type=event, queue=iot.{namespace}.telemetry ▼ Serverless Function (пользовательский handler) ``` --- ## Компоненты ### 1. CRD IoTDevice **Расположение:** `iot/api/v1alpha1/device_types.go` **API group:** `iot.kube5s.ru/v1alpha1` **Манифест:** `iot/config/crd/bases/iot.kube5s.ru_iotdevices.yaml` Поля Spec: | Поле | Тип | Обязательное | Описание | |------|-----|--------------|----------| | `deviceId` | string | да | Идентификатор устройства. Pattern: `^[a-z0-9][a-z0-9-]*[a-z0-9]$` | | `enabled` | bool | нет | Активно ли устройство (default: true) | | `metadata` | map[string]string | нет | Произвольные метаданные (модель, локация) | Поля Status: | Поле | Описание | |------|----------| | `phase` | `Active` / `Disabled` / `Pending` / `Error` | | `mqttUsername` | `{namespace}_{deviceId}` | | `secretName` | Имя k8s Secret с credentials | | `topicPrefix` | `{namespace}/` | | `message` | Сообщение об ошибке если phase=Error | ### 2. IoT Controller **Файл:** `iot/controllers/iotdevice_controller.go` **Логика Reconcile:** ``` IoTDevice CREATE/UPDATE 1. Добавить finalizer "iot.kube5s.ru/device-cleanup" 2. Если Secret iot-{deviceId} не существует: - Сгенерировать пароль: crypto/rand 32 bytes → hex (64 символа) - OwnerReference → Secret удаляется каскадно при удалении IoTDevice - Secret keys: mqtt-username, mqtt-password, device-id 3. Обновить Status: phase=Active, mqttUsername, secretName, topicPrefix 4. Если enabled=false → phase=Disabled IoTDevice DELETE 1. Проверить finalizer 2. Secret удаляется каскадно (OwnerReference) 3. Убрать finalizer → k8s завершает удаление ``` ### 3. IoT REST API **Файл:** `internal/api/handler/iot_device_handler.go` | Endpoint | Auth | Описание | |----------|------|----------| | `POST /internal/mqtt/auth` | Нет (internal) | MQTT auth backend для EMQX | | `POST /v1/namespaces/{ns}/iot/devices` | JWT | Создать IoTDevice | | `GET /v1/namespaces/{ns}/iot/devices` | JWT | Список (без паролей) | | `GET /v1/namespaces/{ns}/iot/devices/{name}` | JWT | Получить (включая mqtt_password из Secret) | | `DELETE /v1/namespaces/{ns}/iot/devices/{name}` | JWT | Удалить | | `PATCH /v1/namespaces/{ns}/iot/devices/{name}` | JWT | Обновить enabled | **MQTT Auth endpoint:** - Всегда HTTP 200 (EMQX игнорирует non-200) - Парсит `username` → `{namespace}_{deviceId}` (разделитель первый `_`) - Ищет k8s Secret `iot-{deviceId}` в namespace - `crypto/subtle.ConstantTimeCompare` для защиты от timing attack ### 4. EMQX 5.5.1 **Манифест:** `deployments/k8s/emqx.yaml` **Конфиг:** HOCON `emqx.conf`, монтируется как ConfigMap volume **Критически важные поля (без них EMQX 5.x не стартует):** ```hocon node { name = "emqx@127.0.0.1" # Обязательно для single-node cookie = "..." # Erlang cluster cookie (любая строка для single-node) data_dir = "/opt/emqx/data" # Директория данных Mnesia } ``` > ⚠️ EMQX 5.x: поля `node.cookie` и `node.data_dir` — **обязательные** (mandatory), > в отличие от 4.x где были значения по умолчанию. > При обновлении ConfigMap нужен `kubectl rollout restart` — Deployment не перезапускается автоматически. **Auth backend:** ```hocon authentication = [{ mechanism = password_based backend = http method = post url = "http://sless-operator.sless.svc:9090/internal/mqtt/auth" }] ``` ### 5. iot-mqtt-bridge **Код:** `iot/cmd/mqtt-bridge/main.go` **Манифест:** `deployments/k8s/iot-mqtt-bridge.yaml` **Образ:** тот же что и оператор (`sless-operator:v0.1.50`), бинарь `/iot-mqtt-bridge` **Логика:** 1. Подключиться к EMQX как MQTT клиент (credentials из Secret `iot-bridge-credentials`) 2. Подписаться на `+/telemetry/+` (все namespace, все устройства) 3. При получении: извлечь namespace из topic[0], publish в RabbitMQ `iot.{namespace}.telemetry` 4. Reconnect loop при обрыве соединения **Envelope в RabbitMQ:** ```json { "namespace": "sless-user123", "device_id": "sensor-01", "topic": "sless-user123/telemetry/sensor-01", "payload": "", "received_at": "2026-04-04T07:19:30Z" } ``` ### 6. Terraform Provider **Файл:** `terraform/provider/internal/resources/iot_device_resource.go` **Ресурс:** `sless_iot_device` **Версия провайдера:** `0.1.2` ```hcl resource "sless_iot_device" "temperature_sensor" { name = "temp-sensor-01" device_id = "temp-sensor-01" enabled = true metadata = { model = "DHT22" location = "Warehouse A" } } output "mqtt_password" { value = sless_iot_device.temperature_sensor.mqtt_password sensitive = true } ``` --- ## Процедура первого деплоя ### Предварительные условия - Кластер с namespace `sless` - sless-operator запущен (или будет запущен в шаге 3) - RabbitMQ доступен в кластере ### Шаги **1. Применить CRD (один раз, cluster-wide)** ```bash kubectl apply -f iot/config/crd/bases/iot.kube5s.ru_iotdevices.yaml ``` **2. Обновить RBAC (добавить права на iot.kube5s.ru)** ```bash kubectl apply -f deployments/k8s/rbac.yaml ``` **3. Применить EMQX** ```bash kubectl apply -f deployments/k8s/emqx.yaml kubectl rollout status deployment/emqx -n sless ``` **4. Применить оператор (с IoT поддержкой)** ```bash kubectl apply -f deployments/k8s/operator.yaml kubectl rollout status deployment/sless-operator -n sless ``` **5. Bootstrap credentials для mqtt-bridge** Создать системное IoTDevice устройство для bridge: ```bash TOKEN=$(kubectl get secret sless-operator-secret -n sless \ -o jsonpath="{.data.SLESS_API_TOKEN}" | base64 -d) # Создать IoTDevice curl -X POST https://sless.kube5s.ru/v1/namespaces/sless/iot/devices \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"iot-bridge","device_id":"iot-bridge","enabled":true}' # Подождать 5с пока контроллер создаст Secret sleep 5 # Получить credentials CREDS=$(curl -s https://sless.kube5s.ru/v1/namespaces/sless/iot/devices/iot-bridge \ -H "Authorization: Bearer $TOKEN") MQTT_USER=$(echo $CREDS | jq -r .mqtt_username) MQTT_PASS=$(echo $CREDS | jq -r .mqtt_password) # Создать Secret для bridge Deployment kubectl create secret generic iot-bridge-credentials -n sless \ --from-literal=MQTT_USERNAME="$MQTT_USER" \ --from-literal=MQTT_PASSWORD="$MQTT_PASS" ``` **6. Применить mqtt-bridge** ```bash kubectl apply -f deployments/k8s/iot-mqtt-bridge.yaml kubectl rollout status deployment/iot-mqtt-bridge -n sless ``` ### Ожидаемый результат ``` emqx-xxx 1/1 Running iot-mqtt-bridge-xxx 1/1 Running sless-operator-xxx 1/1 Running ``` --- ## Известные ошибки и решения ### EMQX CrashLoopBackOff: required_field node.cookie/node.data_dir **Симптом:** `escript: exception throw: {emqx_conf_schema, [{kind=>validation_error, path=>"node.cookie", reason=>required_field}]}` **Причина:** EMQX 5.x требует явного задания `node { cookie, data_dir }` в конфиге. **Решение:** Добавить в `emqx.conf`: ```hocon node { name = "emqx@127.0.0.1" cookie = "your-cookie-string" data_dir = "/opt/emqx/data" } ``` После `kubectl apply` — сделать `kubectl rollout restart deployment/emqx -n sless`. --- ### RBAC forbidden: iotdevices.iot.kube5s.ru **Симптом:** `{"error":"iotdevices.iot.kube5s.ru is forbidden: User \"system:serviceaccount:sless:sless-operator\" cannot create resource"}` **Причина:** ClusterRole `sless-operator` не включает API group `iot.kube5s.ru`. **Решение:** Добавить в `deployments/k8s/rbac.yaml` и применить: ```yaml - apiGroups: ["iot.kube5s.ru"] resources: ["iotdevices"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["iot.kube5s.ru"] resources: ["iotdevices/status"] verbs: ["get", "update", "patch"] - apiGroups: ["iot.kube5s.ru"] resources: ["iotdevices/finalizers"] verbs: ["update"] ``` --- ### mqtt-bridge: multiple restarts при старте **Симптом:** `iot-mqtt-bridge RESTARTS=3` **Причина:** bridge пытается подключиться к EMQX который ещё не готов. Нормальное поведение. **Решение:** Bridge имеет reconnect loop — после старта EMQX подключение восстанавливается автоматически. Ничего делать не нужно. --- ## Версии образов | Версия | Дата | Изменения | |--------|------|-----------| | v0.1.50 | 2026-04-04 | IoT controller + IoT API + iot-mqtt-bridge бинарь | | v0.1.49 | ранее | До IoT | --- ## Актуальный деплой (2026-04-12) > Кластер: iot-naeel (новый). Kafka удалён. Postgres — managed. SQS вместо Kafka. ### Кластер iot-naeel - API: `https://185.247.187.149:6443` - Ingress IP: `185.247.187.151` - DNS: `iot.kube5s.ru → 185.247.187.147` - kubeconfig: `~/.kube/config` на ВМ (токен с TTL 24ч, обновляется через auth.k8s.ngcloud.ru) ### Namespace sless — содержимое | Ресурс | Имя | Описание | |--------|-----|----------| | Deployment | emqx | MQTT-брокер EMQX 5.5.1 | | Deployment | iot-operator | Controller-manager + REST API v0.2.0 | | Deployment | iot-mqtt-bridge | MQTT → shared-SQS bridge v0.2.0 | | Deployment | iot-sqs-consumer | shared-SQS → Postgres consumer v0.2.0 | | Service | emqx | :1883 (MQTT), :8083 (WS), :18083 (dashboard) | | Service | emqx-ws | :8083 (для Ingress) | | Service | iot-operator | :9090 (REST API) | | Ingress | emqx-mqtt-websocket | iot.kube5s.ru → /mqtt (WS), /console (UI) | | Secret | iot-sqs-credentials | SQS_ENDPOINT, SQS_ACCESS_KEY, SQS_SECRET_KEY | | Secret | iot-postgres-secret | POSTGRES_USER, POSTGRES_PASSWORD, IOT_PG_DSN | | Secret | iot-bridge-credentials | MQTT_USERNAME, MQTT_PASSWORD | | CRD | iotdevices.iot.kube5s.ru | IoTDevice v1alpha1 | | ServiceAccount | iot-operator | + ClusterRole + ClusterRoleBinding | ### Процедура деплоя (актуальная) ```bash # 0. SSH на ВМ (все команды оттуда) ssh -i ~/.ssh/id_ed25519 naeel@5.172.178.213 # 1. Сборка и push образа cd /home/naeel/terra/IoT docker build -t naeel/iot-operator:v0.2.0 -t naeel/iot-operator:latest . docker push naeel/iot-operator:v0.2.0 docker push naeel/iot-operator:latest # 2. CRD (один раз) kubectl apply -f config/crd/bases/iot.kube5s.ru_iotdevices.yaml # 3. Postgres Secret (содержит DSN managed PG) kubectl apply -f deployments/k8s/iot-postgres.yaml # 4. EMQX kubectl apply -f deployments/k8s/emqx.yaml kubectl apply -f deployments/k8s/emqx-ws-ingress.yaml # 5. Operator kubectl apply -f deployments/k8s/iot-operator.yaml # 6. SQS credentials (уже создан через kubectl create secret) # kubectl get secret iot-sqs-credentials -n sless # 7. MQTT bridge credentials (уже создан) # kubectl get secret iot-bridge-credentials -n sless # 8. Bridge + Consumer kubectl apply -f deployments/k8s/iot-mqtt-bridge.yaml kubectl apply -f deployments/k8s/iot-sqs-consumer.yaml # 9. Проверка kubectl get pods -n sless kubectl logs -n sless deployment/iot-operator --tail=10 kubectl logs -n sless deployment/iot-mqtt-bridge --tail=10 kubectl logs -n sless deployment/iot-sqs-consumer --tail=10 ``` ### Managed PostgreSQL - Namespace: `dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5` - Pod: `postgresqlk8s-0` - Host: `postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local` - User: `super`, DB: `sqsdb`, PG 17 - Тот же инстанс что использует shared-SQS для billing - Credentials: см. `/home/naeel/terra/SQS-service/secrets/iot_pg.md` ### shared-SQS (очередь вместо Kafka) - Namespace: `shared-sqs` - Endpoint: `https://qu.kube5s.ru` - Tenant ID: `t-96afe7e9f781f6ca` (iot-service) - Queue: `iot-telemetry` - Access Key: хранится в Secret `iot-sqs-credentials` namespace `sless` - Admin token: хранится в Secret `shared-sqs-admin` namespace `shared-sqs` ### Версии образов (актуальные) | Версия | Дата | Registry | Изменения | |--------|------|----------|-----------| | v0.2.0 | 2026-04-12 | Docker Hub naeel/iot-operator | Kafka→SQS, managed PG, новый кластер | | v0.1.50 | 2026-04-04 | pearlharbor (Harbor) | IoT controller + API + mqtt-bridge (Kafka) |