# Архитектура системы Последнее обновление: 2026-03-18 (v0.1.33 + funcs-service v0.1.3) ## Общее описание Managed Serverless Functions Service для облачного провайдера nubes.ru. Пользователь загружает код через Terraform, сервис его собирает (kaniko) и запускает по HTTP-триггеру, расписанию (cron) или вручную через one-shot Job. ## Стек | Компонент | Технология | Где запущен | |-----------|-----------|-------------| | Operator (API + Controllers) | Go (controller-runtime) | Kubernetes, namespace `sless` | | funcs-service (глобальная консоль) | Go (net/http) | Kubernetes, namespace `sless` | | PostgreSQL | PostgreSQL 16 | Kubernetes, namespace `sless` | | S3 | Ceph (облачный) | `s3.msk-1.ngcloud.ru` | | Container Registry | DockerHub (`naeel/`) | внешний | | Builder | kaniko (k8s Job) | namespace пользователя | | Функции (HTTP) | k8s Deployment + Service | namespace пользователя | | Функции (one-shot) | k8s Job | namespace пользователя | | Функции (cron) | k8s CronJob | namespace пользователя | | Terraform Provider | Go (plugin framework v6) | localhost/CI | | nubes API | REST (облако) | `deck-api.ngcloud.ru` | > Redis и RabbitMQ — отложены до v2. ## Компонент: funcs-service Глобальный HTTP сервис — **одна копия** на весь кластер, для всех пользователей. ``` User Browser / curl └─► https://sless.kube5s.ru/funcs/ └─► 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 → alpine - `deployments/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` читает `functions/{ns}/{name}/{ts}.zip` напрямую. ## Изоляция пользователей — 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) 1. Декодирует JWT → sub 2. Вычисляет namespace через SHA256 3. Если задан nubes_endpoint — пингует nubes API (GET ) с тем же токеном - HTTP 401/403 → ошибка инициализации провайдера - Недоступен → ошибка инициализации 4. Вызывает 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 |