diff --git a/docs/superpowers/plans/2026-07-30-processing-engine-k3s-scheduling.md b/docs/superpowers/plans/2026-07-30-processing-engine-k3s-scheduling.md new file mode 100644 index 0000000..2708922 --- /dev/null +++ b/docs/superpowers/plans/2026-07-30-processing-engine-k3s-scheduling.md @@ -0,0 +1,499 @@ +# workflows-engine через k3s (планирование Job'ов) — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Добавить в контур `workflows-engine` как сервис docker-compose, который через kubeconfig-контекст создаёт Job'ы в k3s; Job-поды резолвят `postgres`/`minio` из compose-сети через k8s DNS-мост. + +**Architecture:** engine — обычный compose-контейнер (до `workflow_db` ходит по compose-DNS), в k3s ходит out-of-cluster (`KUBE_CONFIG`+`KUBE_ADDR`). В namespace `processing` k3s заводятся Service+Endpoints `postgres`/`minio` на статические IP compose-контейнеров, чтобы Job-поды достукивались до БД и S3. Поднимаем только `k3s-server`. + +**Tech Stack:** Docker Compose, k3s (rancher/k3s v1.36), kubectl, Ansible (роль `sarex_stack`), Go-сервис workflows-engine (конфиг через env, cleanenv). + +**Спека:** `docs/superpowers/specs/2026-07-30-processing-engine-k3s-scheduling-design.md` + +## Global Constraints + +- Целевой хост — RedOS 8 (SELinux → volume-флаги `:z`/`:ro,z`), деплой из WSL через ansible роль `sarex_stack`. +- Все образы параметризуются `${VAR:-default}`; секреты — через `.env`, генерятся ролью. +- Только `k3s-server` (без `k3s-worker`). Downstream `ENABLE_*` engine выключены, кроме `ENABLE_S3_STORAGE`. +- Статические IP в compose: `postgres` = `172.28.0.10`, `minio` = `172.28.0.11`, subnet `172.28.0.0/16`. +- Имена в k8s = compose-имена: `postgres`, `minio`, namespace `processing`. +- Образ engine: `${SAREX_WORKFLOWS_ENGINE_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/workflows-endigne_prod:075fc0}`. +- Проверки инфраструктуры — не pytest: валидация `docker compose config -q`, `kubectl --dry-run`, коннект-чек по логам. Каждая задача завершается коммитом. + +## File Structure + +- `iac/docker-compose.yaml` (Modify) — top-level `networks.default` IPAM; static IP у `postgres`/`minio`; сервисы `engine`, `processing-k8s-init`. +- `iac/.env.example` (Modify) — `SAREX_WORKFLOWS_ENGINE_IMAGE`. +- `iac/k3s/manifests/processing/namespace.yaml` (Create) — namespace `processing`. +- `iac/k3s/manifests/processing/bridge-postgres.yaml` (Create) — Service+Endpoints `postgres`. +- `iac/k3s/manifests/processing/bridge-minio.yaml` (Create) — Service+Endpoints `minio`. +- `iac/backend/yc-s3-service-account.json` → нет; S3 SA кладём в `iac/engine/yc-s3-service-account.json` (Create, плейсхолдер на `http://minio:9000`). +- `iac/aero/roles/sarex_stack/tasks/main.yml` (Modify) — копирование `k3s/manifests` и `engine/`. +- `iac/aero/roles/sarex_stack/defaults/main.yml` (Modify) — `sarex_services` += `k3s-server`, `processing-k8s-init`, `engine`. +- `iac/aero/pyproject.toml` (Modify) — poe-таск проверки k8s-моста (опц.). + +--- + +### Task 1: Параметр образа engine + S3 SA-плейсхолдер + +**Files:** +- Modify: `iac/.env.example` +- Create: `iac/engine/yc-s3-service-account.json` + +**Interfaces:** +- Produces: env-переменная `SAREX_WORKFLOWS_ENGINE_IMAGE`; файл SA `iac/engine/yc-s3-service-account.json`, монтируемый в engine на `/etc/sarex/yc-s3/yc-s3-service-account.json`. + +- [ ] **Step 1: Добавить переменную образа в `.env.example`** + +В секцию образов `iac/.env.example` добавить строку: + +```dotenv +# workflows-engine (processing) — оркестратор Job'ов в k3s +SAREX_WORKFLOWS_ENGINE_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/workflows-endigne_prod:075fc0 +``` + +- [ ] **Step 2: Создать плейсхолдер S3 service-account JSON** + +`iac/engine/yc-s3-service-account.json` (endpoint на прямой MinIO; app-креды подставит рантайм/ansible — тут плейсхолдер для планирования): + +```json +{ + "host": "http://minio:9000", + "bucket": "sarex-media-storage", + "access_key_id": "sarex-app", + "secret_access_key": "sarex-app-secret", + "region": "us-east-1", + "verify": false +} +``` + +- [ ] **Step 3: Проверка — файл валидный JSON** + +Run: `python -c "import json;json.load(open('iac/engine/yc-s3-service-account.json'));print('ok')"` +Expected: `ok` + +- [ ] **Step 4: Commit** + +```bash +git add iac/.env.example iac/engine/yc-s3-service-account.json +git commit -m "feat(engine): образ workflows-engine + S3 SA-плейсхолдер" +``` + +--- + +### Task 2: Compose-сеть со статическими IP для postgres/minio + +**Files:** +- Modify: `iac/docker-compose.yaml` + +**Interfaces:** +- Produces: `postgres` доступен по `172.28.0.10`, `minio` по `172.28.0.11` в сети `default` (subnet `172.28.0.0/16`). На эти IP ссылаются k8s-Endpoints (Task 4). + +- [ ] **Step 1: Добавить top-level `networks` c IPAM** + +В конец `iac/docker-compose.yaml` (рядом с `volumes:`) добавить: + +```yaml +networks: + default: + ipam: + config: + - subnet: 172.28.0.0/16 +``` + +- [ ] **Step 2: Пин статического IP у `postgres`** + +В сервис `postgres` добавить блок `networks`: + +```yaml + networks: + default: + ipv4_address: 172.28.0.10 +``` + +- [ ] **Step 3: Пин статического IP у `minio`** + +В сервис `minio` добавить: + +```yaml + networks: + default: + ipv4_address: 172.28.0.11 +``` + +- [ ] **Step 4: Проверка — конфиг валиден и IP на месте** + +Run (в `iac/`): `docker compose --env-file .env.example config | grep -A3 -E "ipv4_address|subnet"` +Expected: видно `subnet: 172.28.0.0/16`, `ipv4_address: 172.28.0.10` и `172.28.0.11`. +(Если docker недоступен локально — выполнить на сервере в `deploy_dir` после копирования.) + +- [ ] **Step 5: Commit** + +```bash +git add iac/docker-compose.yaml +git commit -m "feat(net): статические IP postgres/minio для k8s-моста" +``` + +--- + +### Task 3: k8s-манифесты DNS-моста (namespace + Service/Endpoints) + +**Files:** +- Create: `iac/k3s/manifests/processing/namespace.yaml` +- Create: `iac/k3s/manifests/processing/bridge-postgres.yaml` +- Create: `iac/k3s/manifests/processing/bridge-minio.yaml` + +**Interfaces:** +- Consumes: статические IP из Task 2 (`172.28.0.10`, `172.28.0.11`). +- Produces: в namespace `processing` резолвятся имена `postgres:5432` и `minio:9000` (→ compose-контейнеры). + +- [ ] **Step 1: namespace** + +`iac/k3s/manifests/processing/namespace.yaml`: + +```yaml +apiVersion: v1 +kind: Namespace +metadata: + name: processing +``` + +- [ ] **Step 2: мост postgres (Service без селектора + Endpoints)** + +`iac/k3s/manifests/processing/bridge-postgres.yaml`: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: processing +spec: + ports: + - name: pg + port: 5432 + targetPort: 5432 + protocol: TCP +--- +apiVersion: v1 +kind: Endpoints +metadata: + name: postgres + namespace: processing +subsets: + - addresses: + - ip: 172.28.0.10 + ports: + - name: pg + port: 5432 + protocol: TCP +``` + +- [ ] **Step 3: мост minio** + +`iac/k3s/manifests/processing/bridge-minio.yaml`: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: minio + namespace: processing +spec: + ports: + - name: s3 + port: 9000 + targetPort: 9000 + protocol: TCP +--- +apiVersion: v1 +kind: Endpoints +metadata: + name: minio + namespace: processing +subsets: + - addresses: + - ip: 172.28.0.11 + ports: + - name: s3 + port: 9000 + protocol: TCP +``` + +- [ ] **Step 4: Проверка — YAML валиден (клиентский dry-run)** + +Run (при наличии kubectl; иначе отложить на сервер): +`kubectl apply --dry-run=client -f iac/k3s/manifests/processing/` +Expected: `namespace/processing created (dry run)`, `service/postgres ...`, `endpoints/postgres ...`, `service/minio ...`, `endpoints/minio ...`. +Fallback без kubectl: `python -c "import glob,yaml; [list(yaml.safe_load_all(open(f))) for f in glob.glob('iac/k3s/manifests/processing/*.yaml')]; print('ok')"` → `ok`. + +- [ ] **Step 5: Commit** + +```bash +git add iac/k3s/manifests/processing/ +git commit -m "feat(k3s): DNS-мост postgres/minio в namespace processing" +``` + +--- + +### Task 4: Сервис `processing-k8s-init` (применение манифестов) + +**Files:** +- Modify: `iac/docker-compose.yaml` + +**Interfaces:** +- Consumes: манифесты `./k3s/manifests/processing/` (Task 3), kubeconfig `./k3s/kubeconfig.yaml` (пишется `k3s-server`). +- Produces: applied namespace + мост в k3s; сервис завершается успешно (barrier для engine). + +- [ ] **Step 1: Добавить сервис-хелпер (переиспользуем образ k3s — в нём есть kubectl)** + +В `iac/docker-compose.yaml` (после `k3s-worker`/до `gitea` или рядом с processing-сервисами) добавить: + +```yaml + # Одноразовый провижининг k3s: namespace processing + DNS-мост postgres/minio. + # Образ k3s содержит kubectl; server/tls перекрываем на k3s-server:6443. + processing-k8s-init: + image: ${K3S_IMAGE:-rancher/k3s:v1.36.2-k3s1} + container_name: processing-k8s-init + restart: "no" + depends_on: + k3s-server: + condition: service_healthy + entrypoint: ["/bin/sh", "-ec"] + command: + - | + kubectl --kubeconfig /kube/config \ + --server https://k3s-server:6443 --insecure-skip-tls-verify \ + apply -f /manifests/ + volumes: + - ./k3s/kubeconfig.yaml:/kube/config:ro,z + - ./k3s/manifests/processing:/manifests:ro,z +``` + +- [ ] **Step 2: Проверка — конфиг валиден** + +Run (в `iac/`): `docker compose --env-file .env.example config --services | grep processing-k8s-init` +Expected: `processing-k8s-init`. + +- [ ] **Step 3: Commit** + +```bash +git add iac/docker-compose.yaml +git commit -m "feat(k3s): processing-k8s-init — применение DNS-моста через kubectl" +``` + +--- + +### Task 5: Сервис `engine` в docker-compose + +**Files:** +- Modify: `iac/docker-compose.yaml` + +**Interfaces:** +- Consumes: `k3s-server` (kube-API), `processing-k8s-init` (мост applied), `postgres`/`postgres-init` (`workflow_db`), kubeconfig `./k3s/kubeconfig.yaml`, S3 SA `./engine/yc-s3-service-account.json` (Task 1). +- Produces: работающий оркестратор, создающий Job'ы в namespace `processing`. + +- [ ] **Step 1: Добавить сервис `engine`** + +В `iac/docker-compose.yaml` рядом с processing-сервисами: + +```yaml + # workflows-engine — оркестратор: читает workflow_db, создаёт Job'ы в k3s. + # Ходит в k3s out-of-cluster (KUBE_CONFIG+KUBE_ADDR). K8s-исполнитель; AMQP выкл. + engine: + image: ${SAREX_WORKFLOWS_ENGINE_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/workflows-endigne_prod:075fc0} + container_name: engine + restart: unless-stopped + entrypoint: ["/engine"] + environment: + APP_NAME: workflows-engine + LOG_LEVEL: info + ENVIRONMENT: contour + # исполнители + ENABLE_KUBERNETES_EXECUTOR: "1" + ENABLE_AMQP_EXECUTOR: "0" + COUNT_RUNNING_WORKERS: "1" + COUNT_CANCELING_WORKERS: "1" + COUNT_HANDLE_JOB_WORKERS: "1" + WORKFLOW_PRIORITY: low + MAX_WORKFLOWS_LIMIT: "5" + # доступ в k3s (out-of-cluster) + KUBE_CONFIG: /kube/config + KUBE_CONTEXT: default + KUBE_ADDR: https://k3s-server:6443 + JOBS_NAMESPACE: processing + # свой доступ к workflow_db (compose-DNS) + POSTGRES_ADDRESS: postgres + POSTGRES_PORT: "5432" + POSTGRES_DB: ${SAREX_PROCESSING_DB:-workflow_db} + POSTGRES_USER: ${SAREX_PROCESSING_DB_USER:-processing} + POSTGRES_PASSWORD: ${SAREX_PROCESSING_DB_PASSWORD:-processing-secret} + POSTGRES_POOL_SIZE: "20" + POSTGRES_SSL_USE: "0" + # хранилища: только S3, напрямую в minio + ENABLE_S3_STORAGE: "1" + S3_SERVICE_ACCOUNT: /etc/sarex/yc-s3/yc-s3-service-account.json + # дефолты планирования подов + DEFAULT_IMAGE_PULL_POLICY: IfNotPresent + DEFAULT_CPU_REQUESTS: 100m + DEFAULT_MEMORY_REQUESTS: 64Mi + volumes: + - ./k3s/kubeconfig.yaml:/kube/config:ro,z + - ./engine/yc-s3-service-account.json:/etc/sarex/yc-s3/yc-s3-service-account.json:ro,z + depends_on: + postgres: + condition: service_healthy + postgres-init: + condition: service_completed_successfully + k3s-server: + condition: service_healthy + processing-k8s-init: + condition: service_completed_successfully +``` + +- [ ] **Step 2: Проверка — конфиг рендерится, образ и зависимости на месте** + +Run (в `iac/`): `docker compose --env-file .env.example config | grep -A2 "container_name: engine"` +Expected: сервис `engine` с образом `workflows-endigne_prod`. +Run: `docker compose --env-file .env.example config | grep -E "KUBE_ADDR|JOBS_NAMESPACE"` +Expected: `KUBE_ADDR: https://k3s-server:6443`, `JOBS_NAMESPACE: processing`. + +- [ ] **Step 3: Commit** + +```bash +git add iac/docker-compose.yaml +git commit -m "feat(engine): сервис workflows-engine (k8s-исполнитель через kubeconfig)" +``` + +--- + +### Task 6: Интеграция в Ansible-роль + +**Files:** +- Modify: `iac/aero/roles/sarex_stack/defaults/main.yml` +- Modify: `iac/aero/roles/sarex_stack/tasks/main.yml` + +**Interfaces:** +- Consumes: файлы Task 1-5 в репозитории `iac/`. +- Produces: `poe stack`/`install` копирует манифесты и `engine/`, поднимает `k3s-server`, `processing-k8s-init`, `engine`. + +- [ ] **Step 1: Добавить сервисы в `sarex_services`** + +В `iac/aero/roles/sarex_stack/defaults/main.yml`, список `sarex_services`, добавить (порядок не критичен — `depends_on` рулит; но добавим осмысленно): + +```yaml + - k3s-server + - processing-k8s-init +``` +и после `celery` (или рядом с processing) добавить: +```yaml + - engine +``` + +- [ ] **Step 2: Копировать каталог `k3s/manifests` на хост** + +В `iac/aero/roles/sarex_stack/tasks/main.yml`, рядом с копированием nginx/backend, добавить задачу: + +```yaml +- name: Скопировать k3s-манифесты (DNS-мост processing) + ansible.builtin.copy: + src: "{{ deploy_src_root }}/k3s/manifests/" + dest: "{{ deploy_dir }}/k3s/manifests/" + mode: "0644" +``` + +- [ ] **Step 3: Копировать каталог `engine/` (S3 SA) на хост** + +Там же добавить: + +```yaml +- name: Скопировать конфиги engine (S3 service-account) + ansible.builtin.copy: + src: "{{ deploy_src_root }}/engine/" + dest: "{{ deploy_dir }}/engine/" + mode: "0644" +``` + +- [ ] **Step 4: Проверка — YAML роли валиден и сервисы в списке** + +Run (в `iac/aero/`): `uv run python -c "import yaml;d=yaml.safe_load(open('roles/sarex_stack/defaults/main.yml'));assert {'k3s-server','processing-k8s-init','engine'} <= set(d['sarex_services']);print('services ok')"` +Expected: `services ok`. +Run: `uv run python -c "import yaml;list(yaml.safe_load_all(open('roles/sarex_stack/tasks/main.yml')));print('tasks ok')"` +Expected: `tasks ok`. + +- [ ] **Step 5: Commit** + +```bash +git add iac/aero/roles/sarex_stack/defaults/main.yml iac/aero/roles/sarex_stack/tasks/main.yml +git commit -m "feat(ansible): деплой engine + k3s DNS-моста в роли sarex_stack" +``` + +--- + +### Task 7: Развёртывание и проверка планирования на сервере + +**Files:** (нет изменений кода — деплой и верификация) + +**Interfaces:** +- Consumes: всё выше, ветка `aero`, доступ к хосту через ansible из WSL. + +- [ ] **Step 1: Задеплоить файлы и поднять стек** + +Run (в `iac/aero/`, WSL): `uv run poe stack` +(копирует compose/манифесты/.env, `docker compose up -d` включая `k3s-server`, `processing-k8s-init`, `engine`.) +Expected: playbook завершается без ошибок; `changed`. + +- [ ] **Step 2: Проверить, что k3s-server поднялся** + +Run: `uv run poe ps` (или ad-hoc `docker compose ps`) +Expected: `k3s-server` в статусе `Up (healthy)`; `processing-k8s-init` — `Exited (0)`; `engine` — `Up`. + +- [ ] **Step 3: Проверить DNS-мост в k3s** + +Run (ad-hoc на хосте): `docker exec k3s-server kubectl -n processing get svc,endpoints` +Expected: Service `postgres`, `minio` и Endpoints с адресами `172.28.0.10:5432`, `172.28.0.11:9000`. + +- [ ] **Step 4: Проверить коннект engine к workflow_db и kube-API** + +Run: `uv run poe logs engine --tail 120` +Expected: в логах — успешное подключение к Postgres и к kube-API, нет паник (`failed to parse`, `connection refused`, `panic`). +Если engine падает на старте — сверить с риском №2 спеки (downstream `ENABLE_*`) и №1 (`KUBE_ADDR` vs правка `server:` в kubeconfig). + +- [ ] **Step 5: Проверить резолв имён из тестового пода в k3s** + +Run: +```bash +docker exec k3s-server kubectl -n processing run netcheck --rm -it --restart=Never \ + --image=busybox:1.36 -- sh -c "nslookup postgres; nc -z -w3 postgres 5432 && echo PG_OK; nc -z -w3 minio 9000 && echo S3_OK" +``` +Expected: `postgres` резолвится в ClusterIP, `PG_OK`, `S3_OK` (под достучался до compose-postgres/minio через мост). + +- [ ] **Step 6: Проверить планирование Job'а** + +Инициировать workflow (через processing-api/фронт или тестовую запись в `workflow_db`), затем: +Run: `docker exec k3s-server kubectl -n processing get jobs,pods` +Expected: появляется Job-объект, под запланирован на ноду (статус `Running`/`ContainerCreating`/`ImagePullBackOff` — любой из них подтверждает планирование; успешный прогон конвертации — вне рамок). + +- [ ] **Step 7: Зафиксировать результат** + +Никаких изменений кода; при необходимости — записать в память проекта факт готовности планирования и оставшиеся follow-up (валидный S3 SA, downstream-сервисы, образы задач). + +--- + +## Self-Review + +**1. Spec coverage:** +- Компонент A (engine compose) → Task 1, 5. ✓ +- Компонент B (только k3s-server) → Task 6 (`sarex_services`), Task 7 (проверка). ✓ +- Компонент C (DNS-мост postgres/minio) → Task 3, 4. ✓ +- Сеть/статические IP → Task 2. ✓ +- Ansible-интеграция → Task 6. ✓ +- Порядок провижининга → `depends_on` (Task 4, 5) + Task 7. ✓ +- Риски (KUBE_ADDR, стартовые проверки, S3 SA, образ задачи, cgroup/SELinux) → Task 7 Step 4/6 отсылают к рискам спеки. ✓ +- Проверка результата (svc/endpoints, логи, jobs) → Task 7. ✓ + +**2. Placeholder scan:** S3 SA — намеренный плейсхолдер (задокументирован в спеке как приемлемый для планирования), не «TODO в требовании». Остальное — конкретный контент. ✓ + +**3. Type consistency:** имена согласованы сквозняком — `172.28.0.10`/`172.28.0.11`, namespace `processing`, Service/Endpoints `postgres`/`minio`, env `KUBE_ADDR=https://k3s-server:6443`, `JOBS_NAMESPACE=processing`, образ `workflows-endigne_prod:075fc0`. ✓ + +**Открытый нюанс для исполнителя:** если engine НЕ принимает `KUBE_ADDR` как override (риск №1), fallback — в `processing-k8s-init`/отдельном шаге sed-правкой заменить `server: https://127.0.0.1:6443` на `https://k3s-server:6443` в `./k3s/kubeconfig.yaml` и снять `KUBE_ADDR`, оставив `KUBE_CONFIG`+`KUBE_CONTEXT`.