# 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 |