Files
IoT/doc/deployment.md
T
Naeel d4f7c5abc8 doc: обновлена документация — дополнения к архитектуре, деплою, прогрессу
- 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) сохранена как база знаний.
2026-04-12 16:16:46 +03:00

405 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) |