iac/docs/superpowers/plans/2026-07-30-processing-engine-k3s-scheduling.md

500 lines
21 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.

# 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`.