11 KiB
Архитектура системы
Последнее обновление: 2026-04-04 (IoT telemetry storage architecture decision)
Общее описание
Managed Serverless Functions Service + IoT Platform для облачного провайдера nubes.ru. Два независимых компонента: sless (serverless) и iot (IoT), каждый со своим оператором. Пользователь загружает код через Terraform, сервис его собирает (kaniko) и запускает по HTTP-триггеру, расписанию (cron), вручную или по событию от IoT устройства.
Namespace Layout
namespace: sless — платформа serverless
(sless-operator, event-dispatcher, RabbitMQ, Postgres invocations)
namespace: sless-{hash} — tenant функции (function pods каждого клиента)
namespace: iot — платформа IoT
(iot-operator, EMQX, Postgres telemetry)
namespace: iot-{hash} — tenant IoT ресурсы (IoTDevice CRDs)
Стек
| Компонент | Технология | Где запущен |
|---|---|---|
| sless-operator (API + Controllers) | Go (controller-runtime) | namespace sless |
| iot-operator (API + Controllers) | Go (controller-runtime) | namespace iot |
| PostgreSQL (invocations) | PostgreSQL 16 | namespace sless |
| PostgreSQL (telemetry) | PostgreSQL 16 | namespace iot |
| EMQX | EMQX 5.5.1 | namespace iot |
| RabbitMQ | RabbitMQ 3 | namespace sless |
| event-dispatcher | Go | namespace sless |
| iot-mqtt-bridge | Go | namespace iot |
| S3 | Ceph (облачный) | s3.msk-1.ngcloud.ru |
| Container Registry | PearlHarbor (Nubes) | внешний |
| Builder | kaniko (k8s Job) | namespace пользователя |
| Функции (HTTP) | k8s Deployment + Service | namespace sless-{hash} |
| Функции (one-shot) | k8s Job | namespace sless-{hash} |
| Функции (cron) | k8s CronJob | namespace sless-{hash} |
| Terraform Provider | Go (plugin framework v6) | localhost/CI |
| nubes API | REST (облако) | deck-api.ngcloud.ru |
IoT Data Flow
IoT устройство
↓ ws://iot.kube5s.ru:80/mqtt (WebSocket, пока 1883 закрыт)
EMQX (namespace iot)
↓ ACL: каждое устройство видит только свои топики {ns}/{deviceId}/#
iot-mqtt-bridge
↓
RabbitMQ (namespace sless)
↓
event-dispatcher
↓ параллельно:
1. INSERT INTO tenant_{ns}.iot_telemetry ← автоматически
2. Вызов serverless function (если настроена)
Изоляция данных
- MQTT: ACL по username → топики только своего устройства
- Postgres: отдельная DATABASE per tenant, разные credentials
- k8s: отдельный namespace per tenant
Связь sless ↔ iot
- Общий идентификатор tenant:
{hash}в именах namespace - Коммуникация через RabbitMQ endpoint (не через Go пакеты)
- Loose coupling — могут быть в разных кластерах
Глобальный HTTP сервис — одна копия на весь кластер, для всех пользователей.
User Browser / curl
└─► https://sless.kube5s.ru/funcs/<namespace>
└─► nginx Ingress (sless-funcs-ingress)
└─► sless-funcs-service:8090 (namespace sless)
└─► http://sless-operator.sless.svc.cluster.local:9090/v1/...
Файлы:
services/funcs/main.go— логикаservices/funcs/Dockerfile— multi-stage Go → alpinedeployments/k8s/funcs-service.yaml— Deployment + Service + Ingress
Env vars сервиса:
| Переменная | Значение |
|---|---|
SLESS_OPERATOR_URL |
http://sless-operator.sless.svc.cluster.local:9090 |
SLESS_EXTERNAL_URL |
https://sless.kube5s.ru |
SLESS_EXCLUDE |
event-writer,event-monitor,event-cleaner |
SLESS_SERVICE_TOKEN |
JWT токен (задаётся через kubectl set env, не в git) |
Хранение кода функций в S3
Terraform source_dir (локально)
└─► zip → POST /upload → builder.PrepareContext()
├─► functions/{ns}/{name}/{ts}.zip ← ИСХОДНЫЙ КОД пользователя
└─► contexts/{ns}/{name}/{ts}.tar.gz ← BUILD CONTEXT для kaniko
└─► Function CRD: spec.s3Key = "contexts/..."
└─► контроллер → kaniko Job → Docker image
Для web-консоли: GET /source читает contexts/{ns}/{name}/{ts}.tar.gz (из Function.Spec.S3Key), распаковывает tar.gz, фильтрует Dockerfile, возвращает пользовательские файлы.
Изоляция пользователей — Namespace per user
Каждый пользователь облака получает отдельный k8s namespace.
JWT токен (Bearer)
└─► JWT.sub (строка "0199e325-1cdf-7cda-9319-e5302a85e291")
└─► SHA256(sub) → первые 8 байт → hex → "sless-{16 hex символов}"
└─► namespace = "sless-cdd874dfa31ba6ca"
- Namespace детерминирован: один sub → всегда один namespace.
- sub не раскрывается в имени namespace (SHA256 необратим).
- Длина 22 символа — укладывается в лимит k8s (63).
Кто создаёт namespace: Terraform провайдер при Configure() вызывает POST /v1/namespaces/{ns}/ensure один раз, до любых ресурсных операций. Resource-хендлеры (Function, Trigger, Job) namespace не создают — это не их ответственность.
Аутентификация
Оператор (REST API)
- Bearer JWT в заголовке Authorization
- Проверяется структура JWT (3 части), наличие sub claim, срок действия exp
- Подпись не проверяется — trusted perimeter (оператор за Ingress)
Terraform Provider (при Configure)
- Декодирует JWT → sub
- Вычисляет namespace через SHA256
- Если задан nubes_endpoint — пингует nubes API (GET <nubes_endpoint>) с тем же токеном
- HTTP 401/403 → ошибка инициализации провайдера
- Недоступен → ошибка инициализации
- Вызывает POST /v1/namespaces/{ns}/ensure (создаёт namespace если нет)
Схема вызова
Пользователь (curl / браузер)
|
v GET|POST|... /fn/{namespace}/{name}/*
sless-api Ingress -> Operator /fn/ прокси
|
v HTTP forward -> http://{name}.{namespace}.svc.cluster.local:8080
k8s Service -> Deployment/Pod функции
terraform apply
|
v provider Configure()
1. JWT -> sub -> namespace
2. PingNubesAPI (валидация токена)
3. POST /v1/namespaces/{ns}/ensure <- создаёт k8s namespace
|
+-> POST /v1/namespaces/{ns}/functions <- создаёт Function CRD
| +-> POST /upload (zip) <- загружает код -> S3 -> kaniko Job
| +-> polling phase=Ready
|
+-> POST /v1/namespaces/{ns}/triggers <- создаёт Trigger CRD
| +-> controller: Deployment + Service + (CronJob для cron)
|
+-> POST /v1/namespaces/{ns}/jobs <- создаёт FunctionJob CRD
+-> controller: k8s Job -> result в status
Структура кода
sless/
|-- main.go точка входа: k8s manager + REST API сервер (goroutine)
|-- internal/
| |-- api/
| | |-- router.go gorilla/mux: /fn/ (публичный), /v1/ (auth + middleware)
| | |-- handler/
| | | |-- handler.go Handler struct + helpers (writeJSON, namespace(), pathVar())
| | | |-- namespace.go EnsureNamespace (POST /v1/namespaces/{ns}/ensure)
| | | |-- functions.go CRUD Function
| | | |-- triggers.go CRUD Trigger
| | | |-- jobs.go CRUD FunctionJob
| | | |-- upload.go zip -> Dockerfile -> tar.gz -> S3 -> CRD patch
| | | |-- invoke.go прокси /fn/{ns}/{name} -> in-cluster DNS
| | | +-- invocations.go 501 stub (реализация отложена)
| | +-- middleware/
| | |-- auth.go JWT validation (struct + sub + exp, подпись не проверяется)
| | +-- logging.go slog request logger
| |-- builder/
| | |-- builder.go kaniko Job lifecycle (Build, JobStatus, Cleanup)
| | +-- context.go PrepareContext: zip+runtime → tar.gz+Dockerfile для kaniko
| |-- config/config.go Load() из env vars
| +-- storage/
| |-- postgres/store.go SaveInvocation, ListInvocations, RunMigrations
| +-- s3/client.go Upload, Download, UploadContext (tar.gz для kaniko)
|-- controllers/
| |-- function_controller.go Reconcile: Pending->Building->Ready/Failed + Deployment
| |-- trigger_controller.go Reconcile: Service+Ingress (http) / CronJob (cron)
| +-- functionjob_controller.go Reconcile: k8s Job -> Succeeded/Failed + output capture
|-- api/v1alpha1/
| |-- function_types.go Function CRD
| |-- trigger_types.go Trigger CRD
| +-- job_types.go FunctionJob CRD
|-- deployments/k8s/
| |-- operator.yaml Deployment + Service + Ingress
| +-- rbac.yaml ClusterRole + ClusterRoleBinding + ServiceAccount
|-- terraform/provider/ независимый Go-модуль
| +-- internal/
| |-- client/client.go SubFromJWT, NamespaceFromSub, PingNubesAPI + CRUD
| |-- provider/provider.go Configure(): JWT->NS->ping->EnsureNamespace
| +-- resources/
| |-- function_resource.go sless_function
| |-- trigger_resource.go sless_trigger
| +-- job_resource.go sless_job
+-- runtimes/
|-- python3.11/ server.py + Dockerfile -> naeel/sless-runtime-python3.11:v0.1.1
+-- nodejs20/ server.js + Dockerfile -> naeel/sless-runtime-nodejs20:v0.1.2
Kubernetes кластер
Сейчас используется существующий кластер (временный). Планируется переезд на новый кластер — манифесты переносятся без изменений.
Ноды:
- wheel-control-plane-fm9sr — control-plane
- wheel-workers-tv4qr-r45xs — worker
- wheel-workers-tv4qr-x8xw7 — worker
Ingress: nginx, external IP 5.172.178.182 API endpoint: https://sless-api.kube5s.ru
Версии в production
| Артефакт | Тег/Версия |
|---|---|
| naeel/sless-operator | v0.1.22 |
| terra.k8c.ru/naeel/sless провайдер | v0.1.13 |
| naeel/sless-runtime-python3.11 | v0.1.1 |
| naeel/sless-runtime-nodejs20 | v0.1.2 |