Přeskočit obsah

Per-app kontejner — architektonická specifikace

Status: draft (rozhodnuto 2026-05-14) Verze spec: 0.1 Související: backend-deploy.md, app-distribution.md, server-infrastructure.md

1. Rozhodnutí

Posun od dynamického mountu Python modulů do hlavního backendu (backend/app/dynamic_mounts.py) k izolovaným kontejnerům pro každou aplikaci. Hlavní backend zůstává centrálním auth/org/catalog/storage hubem; aplikace běží jako samostatné HTTP služby za API gateway.

Motivace:

  • Izolace — pád / paměťový leak / nekonečná smyčka v jedné app nezpůsobí výpadek ostatních ani auth.
  • Polyglot — apps lze psát v jiných jazycích (Go, Node, Rust), ne jen Python. Vendor si vybere stack.
  • Nezávislé nasazení — deploy app vendora nevyžaduje rebuild celého backendu.
  • Škálování — busy app dostane víc replik, klidná zůstává v 1 instanci.

Trade-off, který přijímáme:

  • Síťová režie mezi službami (~ms latence navíc).
  • Auth musí cestovat přes každý request (vyřešeno shared middleware).
  • DB connections × N apps místo 1 sdíleného poolu (řešení: PgBouncer).
  • Komplexnější deploy (víc kontejnerů, registry, gateway routing).

2. Současný stav (před změnou)

                    ┌────────────────────────────────────┐
                    │   bare uvicorn :8000 (avaxdev)     │
                    │                                    │
                    │   FastAPI app                      │
                    │   ├── /auth/* /org/* /catalog/*    │
                    │   ├── /storage/* /admin/*          │
                    │   └── dynamic_mounts.py:           │
                    │       ├── /legal/*  ← module       │
                    │       │   avax_legal_backend.legal │
                    │       └── (další modules)          │
                    └────────────────────────────────────┘
  • dynamic_mounts.py importuje modul Python avax_legal_backend.legal a mountuje router/admin_router/sync_router do hlavní FastAPI app
  • Sdílená DB session, sdílený Redis, sdílený JWT decode
  • Vše v jednom procesu

Limity: 1 bad app → restart celého backendu; Python-only; sdílený paměťový prostor.

3. Cílový stav

                  ┌──────────────────────────┐
                  │  vm-gateway (Nginx)      │
                  │  api.avaxis.cz           │
                  └──┬───────────────────┬───┘
                     │                   │
                     │ /auth/* /org/*    │ /apps/<slug>/*
                     │ /catalog/*        │
                     │ /storage/*        │
                     ▼                   ▼
            ┌────────────────┐  ┌──────────────────┐
            │  core-api      │  │  apps-gateway    │
            │  (FastAPI)     │  │  (router)        │
            │  :8000         │  │  :8100           │
            └────────┬───────┘  └─────┬────────────┘
                     │                │
                     │                ├─► app-legal :8101
                     │                │   avax-legal/backend
                     │                │
                     │                ├─► app-mzdy  :8102
                     │                │   (až bude)
                     │                │
                     │                └─► app-xxx   :8103
            ┌────────────────┐
            │  PostgreSQL    │   sdílená DB
            │  PgBouncer     │   pool sdílený přes pgbouncer
            │  :5432 / :6432 │
            └────────────────┘

Komponenty

Komponenta Účel Tech
vm-gateway / Nginx TLS termination, prefix routing Nginx (existující)
core-api Auth, org, catalog, storage, admin FastAPI (= dnešní backend, mínus dynamic_mounts)
apps-gateway Router /apps/<slug>/* → kontejner FastAPI tenké proxy NEBO Traefik label-based discovery
app-<slug> Samostatný kontejner per app Vendor volba (FastAPI/Express/Go/…)
PostgreSQL + PgBouncer Sdílená DB s connection poolem PG 16 + PgBouncer
Container registry Image hosting Gitea Container Registry (git.avaxis.cz)

Routing pravidla

  • /auth/*, /org/*, /catalog/*, /storage/*, /admin/*core-api
  • /apps/<slug>/*apps-gatewayapp-<slug> kontejner
  • Cesta <slug> se v requestu zachovává: /apps/legal/items/123 → kontejner dostane /items/123 (gateway strip prefix)

4. Health endpoint — povinná konvence

Každá app backend povinně exposuje:

GET /health
→ 200 OK
{
  "slug": "legal",
  "version": "0.3.2",
  "status": "ok",
  "checks": {
    "db": "ok",
    "core_api": "ok"
  }
}
  • 200 = vše funkční, request je obsluhovatelný
  • 503 = degraded, jeden z checks"fail" (ale kontejner stále běží, jen není připraven obsluhovat)
  • Nikdy timeout > 5s — health musí být rychlý
  • Žádná auth na /health (gateway ho volá interně)

Apps-gateway volá /health periodicky (každých 10s). Při 503 → marker degraded (uživatelské requesty stále routovány, jen alert do monitoringu). Při 3 neúspěšných pingach → marker down, requesty vracejí 503 bez zavolání app.

Liveness vs Readiness: - /health = readiness (může obsluhovat?) - /health/live = liveness (proces běží?) — volitelné, default = vrací 200 dokud proces dýchá

5. Auth propagace

Tok JWT

  1. Klient (launcher / web) volá /apps/legal/items s Authorization: Bearer <jwt>
  2. Gateway request projde rovnou do app kontejneru (token nepáruje)
  3. App kontejner spustí shared middlewareavax-auth package
  4. Middleware validuje JWT (HS256/RS256), naplní request.state.user s {id, company_id, system_role, email}
  5. Endpoint v app si přečte request.state.user

Shared middleware — avax-auth

Python balíček packages/avax-auth/ (v hlavním repu, instaluje se přes pip git+url). Node a Go varianty přijdou v pozdější fázi (viz Timeline).

Stav 2026-05-14: v0.1.0 alpha, 24 testů projde. Funguje proti reálným HS256 tokenům z core-api.

# main.py — jednou při startupu
from avax_auth import AvaxAuthConfig, configure

configure(AvaxAuthConfig(
    algorithm="HS256",            # nebo RS256 v produkci
    secret=os.environ["JWT_SECRET"],
    # public_key=... / jwks_url=...
))

# Endpoint
from typing import Annotated
from fastapi import Depends
from avax_auth import AvaxUser, require_user

@app.get("/items")
def list_items(user: Annotated[AvaxUser, Depends(require_user)]):
    return {"items": ..., "for_user": user.id, "company": user.company_id}

Public API balíčku: - AvaxUser — dataclass s id, company_id, system_role, tfa_verified, raw_token, claims - require_user — FastAPI Depends, validuje Bearer token - require_role(*roles) — factory pro role-based gating - require_super_admin — shortcut - validate_token(token) — low-level (bez FastAPI) - AvaxAuthError, TokenExpired, InvalidToken, WrongTokenType, Forbidden — výjimky

JWT veřejný klíč (pro RS256) se distribuuje: - Z JWKS endpointu GET /auth/.well-known/jwks.json ✅ implementováno 2026-05-14 — cache 1h, automatický refresh při neznámém kid - Statický PEM přes env AVAX_JWT_PUBLIC_KEY (multi-line PEM) — alternativa pro situace bez sítě k core-api

Access/refresh tokeny jsou od 2026-05-14 podepsány RS256 s kid headerem (RFC 7638 JWK Thumbprint). HS256 fallback v decode_token() je transitional — odstranit ~7 dní po deployi (až vyprší všechny refresh tokens).

Detailní README: packages/avax-auth/README.md.

Tokeny mezi službami (machine-to-machine)

App backend někdy potřebuje volat core-api (např. pro GET /storage/token). Použije: - User-on-behalf-of token — propaguje JWT uživatele, který request inicioval (preferováno) - Service token — vystavený přes POST /auth/service-token s app slugem + secret (jen pro background jobs)

6. DB strategie

Per-app schema v sdílené DB

Doporučeno na začátek. Každá app má vlastní PostgreSQL schema (legal, mzdy, …) ve sdílené DB avaxis.

CREATE SCHEMA legal;
GRANT USAGE ON SCHEMA legal TO avax_app_legal;
GRANT ALL ON ALL TABLES IN SCHEMA legal TO avax_app_legal;
  • App má vlastního DB usera avax_app_<slug> s přístupem jen do svého schématu
  • core-api má usera avaxis s přístupem všude
  • Alembic migrace per-app, version_table_schema=<slug>, include_schemas=True

Proč ne per-app DATABASE: - 1 PostgreSQL instance handles 100+ schemas snadno - Jedna záloha = jeden pg_dump (současný setup) - Cross-app queries (analytika, support) jsou možné

Kdy posunout na per-app DATABASE: pokud app vyžaduje custom extension nebo má tisíce tabulek.

PgBouncer

Power tool pro snížení connection load. Apps connect na PgBouncer (:6432), ten drží pool na real PG (:5432). Bez něj 20 apps × 10 connections = 200 connections — pro malou PG je to limit.

[databases]
avaxis = host=192.168.1.55 port=5432 dbname=avaxis_dev

[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25

7. S3 přístup z app

Nezměnen oproti dnešku — app nikdy nemá přímé S3 credentials. Vyžádá si STS token přes GET /storage/token?dir_id=<id>&scope=private v core-api. Token má 15-min platnost, scoped na konkrétní prefix.

Pro app-specifické bucky: - app-<slug> — distribution (vendor pushuje přes avax-publish, app nečte) - backup-<ico> — data firmy (app čte/píše přes STS token)

8. Deploy pipeline

Vývoj (avaxdev)

cd /home/avax/<slug>
docker compose up -d            # spustí app na local portu
curl http://localhost:81xx/health

Apps-gateway na avaxdev má apps.yml config s mapováním <slug> → http://localhost:81xx. Update vyžaduje reload gateway (systemctl reload avax-apps-gateway).

Produkce

  1. Vendor pushne tag v1.0.0 do Gitea repa app
  2. Gitea Actions:
  3. Build Docker image
  4. Push do git.avaxis.cz/<vendor>/<slug>:1.0.0
  5. Vystaví signed manifest s SHA256 do app-avax-<slug>/manifests/1.0.0.json
  6. Avaxis CI:
  7. Pull image
  8. Run security scan
  9. Deploy do staging cluster (<slug>-staging)
  10. Smoke test (health + ~5 testovacích requestů)
  11. Pokud OK → manual approval → promote do prod
  12. Prod deploy:
  13. docker pull na vm-api
  14. docker compose up -d <slug> (rolling, kontejner se vymění)
  15. Apps-gateway se reloadne (nebo discovery sám detekuje)

Versioning kontejnerů

Stejné jako binárky: - alpha — interní (Avaxis QA) - beta — vybrané firmy (per-firma routing) - stable — všichni

Per-firma routing v gateway: header X-Company-ID: <uuid> → cluster volby (stable / beta / alpha).

9. Migrace existujících apps

  1. Vyhodit _BOOTSTRAP_MODULES referenci v dynamic_mounts.py
  2. V /home/avax/avax-legal doplnit Dockerfile, docker-compose.yml, health endpoint
  3. Build image, registrovat v apps-gateway configu
  4. Smoke test: curl /apps/legal/health
  5. Klient (avax-legal-client) beze změny — volá /legal/* přes launcher → gateway → app container
  6. Routing: /legal/*/apps/legal/* (gateway prefix rewrite)
  7. NEBO klient začne volat /apps/legal/* přímo

Postup pro nové apps

Po POST /admin/app-management/create: 1. Vytvořit Gitea repo (existující ✓) 2. Push code skeleton z tools/avax-app-template/ (TODO) 3. Push backend skeleton s Dockerfile + health endpoint + Alembic init (TODO) 4. Vytvořit DB schema <slug> + user avax_app_<slug> s heslem (TODO) 5. Registrovat v apps-gateway (TODO) 6. CI v Gitea automaticky postaví image (TODO)

10. Otevřené otázky

  • Apps-gateway: vlastní FastAPI proxy nebo Traefik? Traefik má label-based discovery (kontejner se přidá → gateway ho vidí), ale je to další komponenta. FastAPI proxy je jednoduchá, ale potřebuje config push.
  • JWKS endpoint — vystavit z core-api, nebo statický PEM v configu apps? JWKS umožní rotaci klíčů bez restartu, statický PEM je jednodušší.
  • Cross-app komunikace — má app volat jinou app přímo (/apps/<other>/...) nebo přes core-api? Nedoporučuji přímo (coupling).
  • Container registry — Gitea Container Registry funguje, ale je to nová věc. Alternativa: vlastní Harbor / Docker Hub. Začít s Gitea, posunout pokud bude omezení.
  • App secrets — kde drží app vlastní credentials (např. OpenAI key pro avax-legal)? Sealed secrets (Bitnami)? Docker secrets? Hashicorp Vault? Začít s Docker secrets per service.
  • Metrics endpoint/metrics (Prometheus) povinný? Doporučuji ano (vm-monitor scrape), ale dáme volitelný v MVP.
  • Rate limiting per app — kde? Gateway level (per /apps//* prefix). Limity per role uživatele.
  • Migrace Avax Legal — kdo to udělá a kdy? Vendor (qwen) sám nebo Avaxis tým?

11. Timeline

Fáze Co Cílový termín
Phase 0 — Spec Tento dokument + diskuze 2026-05-14 ✓
Phase 1a — avax-auth Python Sdílený JWT validation package 2026-05-14 ✓ (v0.1.0)
Phase 1b — RS256 + JWKS Migrace access/refresh tokenů na RS256 + JWKS endpoint 2026-05-14 ✓
Phase 1c — Skeleton push POST /admin/app-management/create pushuje template do avax-apps/<slug>-app 2026-05-14 ✓
Phase 1d — Foundation apps-gateway + PgBouncer TBD
Phase 2 — Pilot Migrate avax-legal do kontejneru TBD
Phase 3 — Tooling Skeleton push do nových Gitea repos s Dockerfile + Alembic TBD
Phase 4 — CI/CD Gitea Actions build + deploy pipeline TBD
Phase 5 — Production vm-api hosting kontejnerů, monitoring, alerting TBD
Phase 6 — Polyglot avax-auth Node + Go packages, příklady non-Python app TBD

12. Rizika a mitigace

Riziko Pravděpodobnost Dopad Mitigace
PgBouncer connection storm při app restart střední vysoký default_pool_size: 25, exponential backoff v app
JWT decode v každém app overhead nízká nízký RS256 + cached public key (1h TTL)
Gateway single point of failure nízká vysoký 2 instance gateway za Nginx load balancer
App push do registry selhání střední střední CI retry 3x + fallback do /var/cache/images
Apps gateway config drift střední střední Config v gitu, deploy přes ansible/CI
App vrátí 500 → user nevidí proč vysoká nízký Strukturovaný log do stdout, agreguje Loki

Před migrací odpovědět:

  1. Jakou DB schema má avax_legal_backend.legal dnes? Tabulky?
  2. Volá legal jiné moduly přímo (Python imports), nebo jen přes core API?
  3. Závisí na Redis (Pub/Sub, cache)?
  4. Background úlohy (Celery)?
  5. Má vlastní credentials k externímu API (např. legislativní DB)?
  6. Velikost příchozího trafficu (požadavků za hodinu)?