Files
sless/doc/decisions/log.md
T
“Naeel” 79719e078d fix: rollout restart после kaniko rebuild (operator v0.1.11)
- controllers/function_controller.go: в ensureDeployment добавить аннотацию
  kubectl.kubernetes.io/restartedAt = fn.Status.LastBuiltAt при обновлении
  существующего Deployment — принудительный rolling restart после каждой сборки
- deployments/k8s/operator.yaml: образ обновлён до v0.1.11
- doc/errors/log.md: задокументирована причина и решение
- doc/decisions/log.md: добавлено правило restartedAt + :latest
- doc/progress.md: версия обновлена до v0.1.11

Причина бага: imagePullPolicy=IfNotPresent + :latest — kubelet кешировал
старый образ, pod не рестартовал после успешного kaniko build.
2026-03-08 20:55:47 +04:00

265 lines
17 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.
# Решения и обоснования
## 2026-03-06 — Отдельная репа для сервиса
**Решение:** Serverless service в отдельной репе, не вместе с Terraform provider.
**Причина:** Разные зоны ответственности, разные релизы, потенциально разные команды.
---
## 2026-03-06 — Один бинарник для v1
**Решение:** Один Go бинарник вместо микросервисов.
**Причина:** Нагрузки изначально нет. Проще деплоить, проще отлаживать. Разделим при необходимости.
---
## 2026-03-06 — Аутентификация через облачный токен
**Решение:** Использовать Bearer token облака, без Keycloak.
**Причина:** Terraform provider уже работает с токенами облака. Keycloak — лишняя зависимость для v1.
---
## 2026-03-06 — S3 облачный, остальное в кубере
**Решение:** S3 (Ceph) использовать облачный (`ceph.tst.nubes.ru`), PostgreSQL/Redis — в кластере.
**Причина:** S3 имеет внешний доступ и уже готов. Для PostgreSQL/Redis сетевого связывания с облаком пока нет — настраивается через devops облака.
---
## 2026-03-06 — Текущий кластер для разработки
**Решение:** Использовать существующий k8s кластер (namespace `sless`), потом перенести на новый.
**Причина:** Новый кластер ещё не готов. Изоляция через namespace — безопасно для существующих сервисов.
---
## 2026-03-06 — RabbitMQ откладываем
**Решение:** В v1 только HTTP и Cron триггеры. RabbitMQ/event triggers — в v2.
**Причина:** Упрощение первой итерации.
---
## 2026-03-07 — DockerHub вместо внутреннего registry
**Решение:** Образы функций и runtime базовые образы публикуются на DockerHub (user `naeel`).
**Причина:** Namespace `registry` в кластере — это Apache NiFi Registry (NOT Docker). Отдельный Docker registry не поднят. DockerHub доступен и достаточен для разработки.
---
## 2026-03-07 — Terraform провайдер sless — отдельный модуль в той же репе
**Решение:** `terraform/provider/` — независимый Go-модуль внутри репы `sless`.
**Причина:** Удобно держать рядом. Код провайдера писался с прицелом на перенос в nubes провайдер (`/home/naeel/remote_dev/terraform`). Клиент (`internal/client/`) → `internal/core/` nubes, ресурсы → `internal/resources_gen/`.
---
## 2026-03-07 — WaitReady в Terraform провайдере при создании функции
**Решение:** После `UploadCode` провайдер ждёт `phase=Ready` (polling каждые 5 сек, таймаут 5 мин).
**Причина:** Kaniko-сборка занимает ~1 минуту. Без ожидания `terraform apply` завершился бы с `phase=Building` в state, что неверно отображало бы реальное состояние ресурса.
---
## 2026-03-07 — code_hash для детектирования изменений кода функции
**Решение:** Атрибут `code_hash` в `sless_function` — пользователь задаёт через `filemd5("./handler.zip")`. Изменение hash → провайдер перезагружает zip и запускает пересборку.
**Причина:** Terraform не отслеживает содержимое файлов автоматически. Это стандартный паттерн (аналогично `aws_lambda_function.source_code_hash`).
---
## 2026-03-07 — Scale-to-zero откладываем до v2
**Решение:** В v1 функции работают как Deployment с постоянно живым подом (always-on). Scale-to-zero — в v2 через KEDA HTTP Add-on.
**Причина:** Scale-to-zero меняет архитектуру контроллера и routing. Для MVP это несоразмерная сложность. Пользователь может управлять ресурсами вручную через `replicas = 0/1/N` (планируется в v1.1).
**v2 план:** Заменить Deployment на `HTTPScaledObject` (KEDA), минимальные реплики = 0. KEDA буферизует запросы во время cold start (~1-3 сек).
---
## 2026-03-07 — replicas как ручное управление масштабом (TODO v1.1)
**Решение:** Добавить поле `replicas *int32` в `FunctionSpec`. Пользователь задаёт через Terraform: `replicas = 0` (выключить), `replicas = 1` (включить), `replicas = N` (масштабировать).
**Причина:** Без этого функция жрёт ресурсы 24/7 даже если не нужна. Это минимальный механизм контроля потребления до реализации scale-to-zero.
---
## 2026-03-07 — PostgreSQL опционален для базового Function Hosting
**Решение:** Postgres нужен только для логов вызовов (`invocations`). Для базового деплоя функций — не нужен. Оператор работает без него (просто не пишет логи).
**Минимальные зависимости для production:** k8s кластер + S3 + Docker registry + Ingress.
## 2026-03-07 — Версионированные теги для runtime образов (не :latest)
**Решение:** Runtime базовые образы (`sless-runtime-python3.11`, `sless-runtime-nodejs20`) и образ оператора (`sless-operator`) тегируются по схеме `v<major>.<minor>.<patch>`. `:latest` не используется.
**Причина:**
- `:latest` приводит к непредсказуемому поведению: kaniko может взять старый кешированный образ, pod не перезапускается если `imagePullPolicy: IfNotPresent`.
- Версионированные теги дают явный контроль: при изменении runtime нужно обновить тег в `upload.go` → это принудительно пересобирает все функции с новым базовым образом.
- Аудит и откат: можно пинить конкретную версию runtime.
**Соглашение:**
- Runtime образы: `naeel/sless-runtime-{lang}:v{версия}` (например `v0.1.0`)
- Оператор: `naeel/sless-operator:v{версия}`
- При изменении runtime — инкрементировать минорную версию образа и обновить константу в `upload.go`
---
## 2026-03-07 — nodejs20 как второй поддерживаемый runtime
**Решение:** Добавлен nodejs20 runtime (`node:20-alpine` base, `server.js` HTTP wrapper, `exports.handle(event)`).
**Причина:** Node.js — стандарт для serverless (AWS Lambda, Vercel). Покрывает JS/TypeScript аудиторию. Паттерн идентичен python3.11: runtime image → kaniko → Deployment.
**Детали реализации:**
- `runtimes/nodejs20/server.js``http.createServer`, динамический `require(HANDLER_PATH)`
- Зависимости через `package.json``npm install --omit=dev` (аналог `requirements.txt``pip install`)
- `entrypoint` в HCL игнорируется для Node.js (всегда `handler.js` + `exports.handle`) — TODO: поддержать произвольный entrypoint в v1.1
---
## 2026-03-07 — FunctionJob CRD: одноразовые запуски функций
**Решение:** Добавлен `FunctionJob` CRD для одноразового запуска функции с произвольным JSON-событием.
**Причина:** Нужны sync-вызовы без HTTP — для батч-обработки, миграций, крон-задач через Terraform.
**Реализация:**
- `api/v1alpha1/job_types.go` — CRD: `FunctionRef`, `EventJSON`, phases: Pending/Running/Succeeded/Failed
- `controllers/functionjob_controller.go` — создаёт k8s Job, ждёт завершения, синхронизирует статус
- `internal/api/handler/jobs.go` — REST: CreateJob/GetJob/DeleteJob
- `terraform/provider/internal/resources/job_resource.go` — ресурс `sless_job`
- Настраиваемые таймауты: `build_timeout_sec` (sless_function), `wait_timeout_sec` (sless_job)
---
## 2026-03-07 — Прокси /fn/ вместо wildcard Ingress
**Проблема:** wildcard DNS `*.fn.kube5s.ru` недоступен (провайдер не позволяет).
**Решение:** HTTP-прокси внутри оператора — маршрут `GET|POST|... /fn/{namespace}/{name}` на `sless-api.kube5s.ru`.
**Реализация:**
- `internal/api/handler/invoke.go` — форвардит запрос к `http://{fn}.sless-fn-{ns}.svc.cluster.local:8080`
- `internal/api/router.go``/fn/` регистрируется **до** auth middleware, публично доступен; `/v1/` — по-прежнему с Bearer токеном (gorilla `Use()`)
- `internal/config/config.go` — новое поле `ExternalURL` (env `EXTERNAL_URL`)
- `controllers/trigger_controller.go` — если `ExternalURL` задан, `Trigger.Status.URL = ExternalURL/fn/{ns}/{name}`; иначе fallback: создаёт Ingress с поддоменом (прежнее поведение)
- `deployments/k8s/operator.yaml``EXTERNAL_URL=https://sless-api.kube5s.ru`
**URL функции:** `https://sless-api.kube5s.ru/fn/{namespace}/{name}`
**E2E:** `curl https://sless-api.kube5s.ru/fn/default/hello-node``{"message":"Hello, Naeel! (nodejs20)"}`
---
## 2026-03-08 — Lifecycle control: trigger.enabled + job.run_id
**Задача:** управление жизненным циклом ресурсов без удаления.
### trigger.enabled
**Проблема:** нет способа "заморозить" функцию без удаления Trigger/Function (
освобождение ресурсов под праздники, дебаггинг и т.д.).
**Решение:** `enabled bool` (по умолчанию `true`) в `TriggerSpec`.
- `enabled=false` → trigger_controller масштабирует Deployment функции до 0 реплик.
- Функция не принимает запросы, не потребляет CPU (pod не запущен).
- Изменение **не** пересоздаёт ресурс (нет RequiresReplace) — in-place через PATCH.
**Реализация:**
- `api/v1alpha1/trigger_types.go``Enabled bool` в TriggerSpec, `//+kubebuilder:default=true`
- `controllers/trigger_controller.go` — патчит Deployment replicas=0/1 в зависимости от Enabled
- `internal/api/handler/triggers.go``UpdateTrigger` handler (PATCH), поле `enabled` в request/response
- `internal/api/router.go``PATCH /v1/namespaces/{namespace}/triggers/{name}`
- `internal/client/client.go``TriggerUpdateRequest`, `UpdateTrigger()` метод
- `terraform/provider/internal/resources/trigger_resource.go` — атрибут `enabled` (Optional+Computed, default=true), реализован `Update` метод
### job.run_id
**Проблема:** нет способа создать FunctionJob "отложенным" — с явным контролем когда запускать.
Также нет механизма повторного запуска с сохранением структуры ресурса.
**Решение:** `run_id int64` (по умолчанию `0`) в `FunctionJobSpec`.
- `run_id=0` → FunctionJob создаётся в k8s, но k8s Job не запускается (phase=Skipped).
- `run_id>0` → запускает Job. Увеличение значения (1→2→3) = повторный запуск через пересоздание.
**Реализация:**
- `api/v1alpha1/job_types.go``RunID int64` в FunctionJobSpec, `//+kubebuilder:default=0`
- `controllers/functionjob_controller.go` — если RunID==0 → устанавливает phase=Skipped, return
- `internal/api/handler/jobs.go` — поле `run_id` в jobRequest/jobResponse
- `internal/client/client.go``RunID int64` в JobRequest/JobResponse
- `terraform/provider/internal/resources/job_resource.go` — атрибут `run_id` (RequiresReplace, default=0). Если run_id=0 → не ждёт завершения, phase=Skipped сразу в state.
**Версии:**
- operator: `naeel/sless-operator:v0.1.6`
- provider: `terra.k8c.ru/naeel/sless v0.1.4`
---
## 2026-03-08 — Переключение registry с Harbor на DockerHub
**Проблема:** Harbor (`pearlharbor.registryk8s.services.ngcloud.ru`) — внешний сервис облачного провайдера. Нестабилен: `/v2/` периодически зависает на 10+ секунд или возвращает 504. Kaniko не мог завершить push образа.
**Решение:** `REGISTRY_HOST=naeel` (DockerHub namespace). Образы функций пушатся как `naeel/sless-default-{namespace}-{name}:latest`.
**Реализация:**
- `deployments/k8s/operator.yaml` — configmap `REGISTRY_HOST: "naeel"`
- Secret `sless-registry-auth` уже содержал DockerHub credentials → дополнительных изменений не потребовалось
**Компромисс:** DockerHub — публичный registry. Образы функций пользователей публично видимы. Для production нужен приватный registry (Harbor, ECR, GCR и т.д.).
**Версии:**
- operator: оператор не пересобирался, только configmap
- commit: `b69f795`
---
## 2026-03-08 — FunctionJob polling вместо Owns watch
**Проблема:** `Owns(&batchv1.Job{})` в `SetupWithManager` не работает cross-namespace. Job создаётся в `sless-fn-{ns}`, FunctionJob — в user namespace. Watch никогда не срабатывал.
**Решение:** Убрать `Owns`. В `syncJobStatus` при Running статусе возвращать `ctrl.Result{RequeueAfter: 5 * time.Second}` — контроллер сам поллит k8s Job каждые 5 сек.
**Версии:**
- operator: `naeel/sless-operator:v0.1.10`
- commit: `461ac09`
---
## 2026-03-08 — code_hash: filesha256 вместо output_md5
**Проблема:** `hashicorp/archive v2.7.x` имеет баг: `output_md5` возвращает MD5 предыдущей версии zip. `output_sha` и `output_sha256` обновляются корректно.
**Решение:** `code_hash = filesha256("${path.module}/code/handler.js")` — хэшируется исходный файл напрямую.
**Правило проекта:** В `sless_function.code_hash` всегда использовать `filesha256(source_file)`, не `archive_file.output_md5`.
---
## 2026-03-08 — Rollout restart после kaniko build (imagePullPolicy + :latest)
**Проблема:** После успешной kaniko сборки pod не перезапускался — kubelet брал кешированный образ `:latest` (imagePullPolicy: IfNotPresent). Функция возвращала старый код.
**Решение:** В `ensureDeployment` при обновлении существующего Deployment проставляем аннотацию:
```go
existing.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = fn.Status.LastBuiltAt.Time.Format(time.RFC3339)
```
Значение привязано к `fn.Status.LastBuiltAt` → меняется при каждой сборке → Kubernetes делает rolling restart → свежий образ гарантированно пул-ится.
**Правило проекта:** При использовании `:latest` tag всегда явно проставлять `restartedAt` annotation при обновлении кода.
**Версия:** operator `naeel/sless-operator:v0.1.11`