- architecture/overview.md: добавлена актуальная схема (SQS, managed PG) - deployment.md: добавлена процедура деплоя в iot-naeel - progress.md: добавлен блок 2026-04-12 (Kafka→SQS, деплой) - api/endpoints.md: добавлен актуальный формат /iot-admin/stats - thinking/2026-04-12.md: дописан ход реализации и деплоя - copilot-instructions.md: добавлено правило «документацию не стирать» Старая документация (Kafka, RabbitMQ) сохранена как база знаний.
405 lines
14 KiB
Markdown
405 lines
14 KiB
Markdown
# 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": "<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`
|
||
|
||
```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) |
|