Files
IoT/doc/deployment.md
T
Naeel 1a94241c62 feat: перенос IoT managed service из sless в отдельную репу
Компоненты:
- iot-operator: controller-manager (IoTDevice CRD) + REST API (порт 9090)
- mqtt-bridge: MQTT (EMQX) → Kafka bridge
- kafka-consumer: Kafka → Postgres pipeline

Модуль: gitea.services.ngcloud.ru/Nail/IoT
Все 3 бинарника собираются, import paths адаптированы.
2026-04-12 14:29:43 +03:00

10 KiB
Raw Blame History

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 не стартует):

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:

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:

{
  "namespace": "sless-user123",
  "device_id": "sensor-01",
  "topic": "sless-user123/telemetry/sensor-01",
  "payload": "<base64 of raw MQTT 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

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)

kubectl apply -f iot/config/crd/bases/iot.kube5s.ru_iotdevices.yaml

2. Обновить RBAC (добавить права на iot.kube5s.ru)

kubectl apply -f deployments/k8s/rbac.yaml

3. Применить EMQX

kubectl apply -f deployments/k8s/emqx.yaml
kubectl rollout status deployment/emqx -n sless

4. Применить оператор (с IoT поддержкой)

kubectl apply -f deployments/k8s/operator.yaml
kubectl rollout status deployment/sless-operator -n sless

5. Bootstrap credentials для mqtt-bridge

Создать системное IoTDevice устройство для bridge:

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

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:

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 и применить:

- 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