F-08 FastAPI skeleton — middleware + manifest auto-reg + production deploy
Research prereqs (mandatory per F-06 lesson)
Прочитано перед implementation:
- AB §1: FastAPI 0.115+, Pydantic v2, SQLAlchemy 2.0 async, modular monolith
- AG §1-2: manifest-driven module system, 5 single sources of truth
- N-security §I1+I2: RLS + FORCE +
OrgScopedSession, token redactor - N-security §S3: JWT alg allowlist (PyJWT >= 2.10, NO
none/HS256 confusion) - Y RBAC §1.2: 4 system roles (owner/admin/operator/viewer) + permission level matrix
- W pentest §P01+P03+P04: alg=none, cross-tenant SELECT, cross-tenant INSERT
Никаких research skips — все основные patterns взяты прямо из docs.
Что было сделано
apps/api/src/razmakh_api/модулей:settings.py— pydantic-settingslogging_config.py— structlog JSON для Loki + token redaction processorauth/jwt.py— PyJWT decode/encode, explicitalgorithms=[HS256]allowlistauth/permissions.py— Permission registry +require_permissiondecoratormiddleware/rls.py— JWT decode →request.state.jwt_claims+ correlation IDshealth/router.py—/health,/health/db,/health/etl,/health/infisical,/health/aggregatemetrics/router.py— Prometheus custom REGISTRY + HTTP middlewaremodules/registry.py— manifest auto-discovery → permissions + routesmain.py— FastAPI app factory + lifespan (startup/shutdown)
- Tests: 32 unit + integration (test_jwt_auth, test_rls_middleware, test_metrics, test_manifest_registration, test_health, test_cross_tenant_probe)
- Dockerfile multi-stage (uv 0.8 + python:3.13-slim, 384MB final)
- VPS deploy: docker-compose api service (root user → cat secrets → su razmakh → uvicorn)
- Caddy:
razmakh.ru/api/*→api:8000 - Prometheus razmakh-api job uncommented + reloaded
- k6 pool leak test: 100 RPS × 5min, 30001 reqs, 0 failures, p95=3.52ms
Главные wins
- Mypy strict + ruff clean — 32 файла, no ignores сверх pre-existing
- JWT alg=none blocked: test_alg_none_rejected pass (PyJWT 2.12)
- Cross-tenant probe pass: 3 tests (SELECT isolation + INSERT WITH CHECK + no-context default-deny)
- Pool leak test pass: 30001 reqs, p95=3.52ms, 8 active connections под cap (pool=20+overflow=10)
- Prometheus scrape works:
up{job="razmakh-api"}=1через docker network
Issues encountered
Issue 1: uv 0.5.11 workspace install — Audited 0ms (empty venv)
Symptom: первый Docker build — venv с только _virtualenv.pth, uvicorn missing. uv sync --frozen --no-dev repeated → “Audited in 0.00ms” (cached “ничего не сделано” — но deps не installed).
Cause: старая uv 0.5.11 не правильно обрабатывает workspace без явного --all-packages flag. Root pyproject.toml у нас “пустой aggregator” с [tool.uv.workspace], без dependencies. uv 0.5 решает “ничего не надо install” → пустой venv.
Fix:
- Upgrade
ghcr.io/astral-sh/uv:0.5.11→:0.8.4 - Add
--all-packagesflag кuv sync --frozen --no-dev
После fix — Built razmakh-core / razmakh-api → Prepared 51 packages → Installed 51 packages в 65ms.
Issue 2: Docker secrets permission denied for non-root user
Symptom: container exit с cat: /run/secrets/postgres_password: Permission denied когда USER=razmakh (UID 1001).
Cause: Docker secrets mounted с permissions 0400 root:root через tmpfs — immutable, не подлежат chmod в running container. Same pattern уже у postgres-exporter container (см. docker-compose.yml: user: "0:0").
Fix:
- В docker-compose
apiservice:user: "0:0" - Entrypoint wrapper: cat secrets → export env →
su razmakh→ exec uvicorn
Issue 3: asyncpg ищет ~/.postgresql/postgresql.key при connect
Symptom: /health/db returns 500 Internal Server Error. Logs: PermissionError: [Errno 13] Permission denied: '/root/.postgresql/postgresql.key'.
Cause: asyncpg default SSL key lookup идёт в $HOME/.postgresql/postgresql.key. После su razmakh -p (preserve env) HOME оставался /root (export’нут root shell). Файл /root/.postgresql отсутствует, но stat падает с EACCES потому что /root 700 root:root → razmakh user не может даже check existence.
Fix:
- В entrypoint:
export HOME=/tmpпередsu - В
sucommand:su -p -s /bin/sh razmakh -c "HOME=/tmp python -m uvicorn ..."(защита от inherited HOME)
После — /health/db → 200, latency 1.6ms через TCP (postgres container).
Issue 4: Dockerfile CMD using sh -c "exec uvicorn ..." — uvicorn not in PATH
Symptom: container exit с sh: 1: exec: uvicorn: not found.
Cause: ENV PATH с /opt/venv/bin set в Dockerfile, но sh -c запускается из base shell environment где PATH inheritance работает не как ожидалось — некоторые ENV vars не propagate в child shell argv.
Fix: явно вызывать /opt/venv/bin/python -m uvicorn (absolute path) вместо bare uvicorn.
Что меняется в process
- Перед добавлением dependency:
uv lock+git diff uv.lock— убедиться, что lockfile содержит новые deps. Иначеuv sync --frozenв Docker → старые deps. - При первой Docker сборке: всегда smoke test container локально (
docker run --rm -p 18000:8000) перед push на VPS. Это caught 4 issues выше за 15 минут вместо часа debugging на VPS. - Asyncpg + non-root containers: всегда
HOME=/tmpилиHOME=/var/lib/<app>(writable). Default$HOMElookup в asyncpg SSL detection — gotcha.
Tracking — speedup metric
- Plan estimate: 1-2 дня (8-16h)
- Actual wall-clock: ~3.5h (включая 4 Docker issues + Caddy/Prometheus reload)
- Speedup: ~3-5x (consistent с F-06 estimate-recalibration baseline 5-8x; на нижней границе из-за infra debugging time)
Production verification (post-deploy)
curl https://razmakh.ru/api/health# {"status":"ok","service":"razmakh-api","version":"0.1.0","timestamp":"..."}
curl https://razmakh.ru/api/health/aggregate# {"status":"ok","components":{"db":{"status":"ok","latency_ms":1.6},"infisical":{"status":"unknown"},"etl":{"status":"ok"}}}
# Prometheus scrape verifieddocker exec razmakh-prometheus wget -O- "http://localhost:9090/api/v1/query?query=up{job=\"razmakh-api\"}"# {"status":"success","data":{"resultType":"vector","result":[{"metric":{...job:"razmakh-api"},"value":[...,"1"]}]}}Deferred to next iterations
- /health/etl real freshness query: требует SECURITY DEFINER function или dedicated health-check DB role (NOT razmakh_app — он NOBYPASSRLS, не видит cross-org data). Phase 1.5.
- JWT KMS via Infisical: Phase 1.5 — Phase 1 simple HS256 shared secret.
- CORS_ORIGINS=https://razmakh.ru — пока web placeholder, но настройка уже работает (set в env).
- Permission templates table (Y RBAC §3): Phase 1.5 — Phase 1 = 4 system roles hardcoded.
- Module routes auto-include: hello_world в Phase 0 пока без routes (.routes.py не existed). Когда первый real module появится —
register_module_routesсам подцепит. - Image size optimization: 384MB (target <250MB). Future: distroless base, slim asyncpg, exclude .pyc.