agnes-the-ai-analyst/docs/archive/superpowers/specs/2026-04-21-multi-customer-deployment-spec.md
ZdenekSrotyr a48524509a
docs: consolidate and de-clutter the documentation tree (#306)
CLAUDE.md rewritten (708 -> ~320 lines): four overlapping release
sections collapsed to one, stale v1->v35 schema history dropped (it
lives in CHANGELOG), marketplace endpoint internals and verbose
process sections moved out or tightened.

New focused docs:
- docs/RELEASING.md - release process, deploy workflows, CI quirks
  (RELEASE_TEMPLATE.md folded in as an appendix)
- docs/marketplace.md - marketplace ingestion + re-serving internals
- docs/README.md - documentation index by audience, linked from
  README.md and CLAUDE.md

Archived under docs/archive/: docs/superpowers/ (52 historical
planning artifacts), HACKATHON.md, pd-ps-comments.md,
security-audit-2026-04.md, future/NOTIFICATIONS.md.

Removed the docs/auto-install.md stub. Fixed dangling links in
connectors/jira/README.md and dev_docs/README.md, repointed
code/doc references to archived paths.
2026-05-14 18:54:22 +00:00

18 KiB
Raw Blame History

Multi-Customer Deployment — Design Spec

Datum: 2026-04-21 Status: Návrh k implementaci Autor: Zdeněk Šrotýř + Claude (sparring)

1. Cíl

Zavést production-grade nasazení Agnes, které:

  1. Nechává upstream repo public (žádné zákaznické info tam).
  2. Umožňuje N zákazníků paralelně, každý v izolovaném prostoru.
  3. Je anonymizované — jeden zákazník nevidí existenci ani identitu ostatních.
  4. auto-deploy s rozumnými gates — feature branch push → dev VM aktualizace do minut; merge do main → prod s review gate.
  5. Podporuje branch-aware dev environments — víc vývojářů paralelně, každý na své branchi, bez interference.
  6. Škáluje O(1) na zákazníka — přidání another-customer vedle Keboola znamená jen klonování šablony, ne změnu upstream.

2. Model — Pure Self-Deploy

2.1 Role

Strana Co dělá
Keboola jako upstream Udržuje app kód, buildí & pushuje Docker image na GHCR, udržuje TF modul, udržuje infra template
Zákazník (vč. Keboola-as-customer) Vlastní GCP projekt, vlastní privátní infra repo, vlastní CI/CD, spravuje svoje VMs, nese náklady

Keboola jako upstream nemá žádný přístup k zákaznickým GCP projektům. Zákazník zodpovídá za svoje nasazení.

Keboola interní produkční Agnes instance je speciální případ zákazníka — Keboola IT vlastní internal-prod GCP projekt a spravuje tam svou Agnes stejně jako to bude dělat another-customer ve svém GCP.

2.2 Budoucí rozšíření (out of scope pro tuto vlnu)

  • AWS podpora: TF modul je dnes GCP-specific. Jakmile přijde první AWS zákazník, přidáme paralelní modul modules/customer-instance-aws/.
  • Managed service: Keboola bude nabízet "nasadíme vám to za vás" — znamená přidat Keboola jako operator role s IAM delegací do zákazníkova GCP. Design v tomhle specu je kompatibilní, jen vyžaduje extra vrstvu IAM bindings.

3. Repo architektura

3.1 Počet a typ repozitářů

keboola/agnes-the-ai-analyst        PUBLIC       App + TF modul + dokumentace
keboola/agnes-infra-template        PUBLIC       Skeleton pro privátní infra repo (template)
keboola/agnes-infra-keboola         PRIVATE      Keboola-as-customer deployment
{acme}/agnes-infra                  PRIVATE      Nový zákazník — v jejich GitHub org, klonováno z template

Počet: 2 upstream + N per-customer. Upstream repa jsou stabilní, per-customer vznikají při onboarding.

3.2 Obsah keboola/agnes-the-ai-analyst (public)

agnes-the-ai-analyst/
├── app/ src/ connectors/ cli/        # produkt
├── Dockerfile docker-compose.yml
├── .github/workflows/
│   └── release.yml                   # build + push do GHCR; tagy: :dev, :stable, :dev-branch-xyz
├── infra/
│   ├── modules/
│   │   └── customer-instance/        # versioned: tag infra-v1.0, v1.1, ...
│   │       ├── main.tf
│   │       ├── variables.tf
│   │       └── outputs.tf
│   └── examples/
│       └── minimal/                  # quickstart pro OSS self-hoster
└── docs/
    ├── DEPLOYMENT.md                 # pro self-host (compose, bez Terraform)
    ├── ONBOARDING.md                 # pro managed (cesta k TF + template)
    └── architecture.md

TF modul customer-instance je verzován samostatně semver (infra-v1.x), odlišeně od app image (CalVer YYYY.MM.N).

3.3 Obsah keboola/agnes-infra-template (public template)

agnes-infra-template/
├── terraform/
│   ├── main.tf                       # module { source = "github.com/keboola/agnes-the-ai-analyst//infra/modules/customer-instance?ref=infra-v1.0" }
│   ├── variables.tf
│   ├── backend.tf                    # gcs by default, komentář jak přepnout na s3/remote
│   ├── terraform.tfvars.example
│   └── .gitignore                    # terraform.tfvars, *.tfstate
├── .github/workflows/
│   ├── plan.yml                      # PR → terraform plan
│   └── apply.yml                     # main → terraform apply
├── config/
│   └── instance.yaml.example
├── bootstrap.sh                      # jednorázový setup GCP: SA, API enable, bucket, secrets
└── README.md                         # step-by-step onboarding

Zákazník (nebo Keboola při onboardingu) použije gh repo create --template keboola/agnes-infra-template → přijde privátní repo s hotovou strukturou.

3.4 Obsah per-customer privátního repa (např. keboola/agnes-infra-keboola)

Přesně ta samá struktura jako template, jen s konkrétními hodnotami v terraform.tfvars:

# keboola/agnes-infra-keboola/terraform/terraform.tfvars
# (gitignored, nebo lokálně v Secret Manageru — viz §6)

gcp_project_id  = "internal-prod"
region          = "europe-west1"
zone            = "europe-west1-b"

prod_instance = {
  name         = "agnes-prod"
  machine_type = "e2-small"
  image_tag    = "stable"              # floating | "stable-2026.04.N" (pinned)
  upgrade_mode = "auto"                # auto (watchtower) | pinned (Renovate)
  tls_mode     = "caddy"               # caddy | gcp-lb | cloudflare | none
  domain       = ""                    # prázdné = jen IP
}

dev_instances = [
  { name = "agnes-dev-default", image_tag = "dev" },
  # přidávat další dev VMs per branch/developer
]

seed_admin_email = "zdenek.srotyr@keboola.com"

# Keboola-specific
data_source        = "keboola"
keboola_stack_url  = "https://connection.us-east4.gcp.keboola.com/"
keboola_token_secret_id = "keboola-storage-token"   # reference do Secret Manageru

4. Release model

4.1 Image tagging v GHCR

Public repo CI (release.yml) buildí a pushuje do ghcr.io/keboola/agnes-the-ai-analyst při každém push:

Trigger Tagy které vzniknou
Push main :stable, :stable-YYYY.MM.N, :sha-xxxxxxx
Push feature/xyz :dev, :dev-feature-xyz, :sha-xxxxxxx
Push release/1.2.x :release-1.2.x, :release-1.2.x-YYYY.MM.N

:dev a :stable jsou floating tagy — posouvají se při každém pushe. Verzované tagy jsou neměnné.

4.2 Visibility obrazu

ghcr.io/keboola/agnes-the-ai-analyst je public image. Zákaznické VMs pullují bez credentials.

Důvod: kód je veřejný, obraz nesmí obsahovat nic, co veřejný kód neobsahuje. Secrets jdou do .env na VM, ne do image.

4.3 Smoke test

Po push main a tagování :stable-N, CI spustí smoke test: docker compose up + curl /api/health + auth + query. PASS → :stable floating se posune. FAIL → build dostane :deprecated-N label, :stable se nehne, GitHub issue s logy.

4.4 CalVer + smoke test = kontinuální release

Žádné manuální release rozhodnutí. Každý merge do main = release (pokud smoke test projde). Číslování YYYY.MM.N = rok.měsíc.sekvence.

5. Branch-aware dev environments

5.1 Motivace

Víc vývojářů paralelně potřebuje víc dev environmentů bez interference. „Floating :dev" je nedostatečné — poslední push přepíše ostatní.

5.2 Mechanismus

Každý feature branch push → samostatný tag :dev-{branch-slug} navíc k floating :dev.

V privátním infra repu zákazník vyjmenuje dev VMs s pinned tagem:

dev_instances = [
  { name = "agnes-dev",          image_tag = "dev" },                         # floating (demo / reviewers)
  { name = "agnes-alice-feat1",  image_tag = "dev-feature-alice-dashboard" }, # Alice má svou
  { name = "agnes-bob-pr142",    image_tag = "dev-pr-142" },                  # Bob pinned na PR
]

5.3 Lifecycle dev VM

1. Někdo otevře PR v privátním infra repu:
   +   { name = "agnes-carol", image_tag = "dev-feature-carol-new-auth" }
2. CI plan.yml komentuje v PR: „vytvoří se VM agnes-carol (e2-small, europe-west1-b)"
3. Merge → apply.yml spustí terraform apply
4. VM up za ~2 min
5. Watchtower na VM polluje :dev-feature-carol-new-auth každých 5 min
6. Každý push na feature/carol/new-auth → nový image → watchtower pullne → VM má aktuální verzi
7. Až Carol dokončí feature (merge do main), smaže řádek v tfvars → terraform apply → VM destroy

Žádný nový SA, žádný nový GitHub environment, žádná infra operace navíc. Jen editace seznamu v tfvars.

5.4 Ephemeral preview environments (budoucnost)

V pozdější fázi zvážit automatizaci: PR otevřen → GHA vytvoří per-PR VM; PR zavřen → destroy. Aktuálně explicitní flow přes tfvars stačí.

6. Prod upgrade model

6.1 Dva režimy (per-instance volitelné)

Režim Jak Pro koho
auto Watchtower na VM polluje :stable (floating), pullne + restart, když se objeví nový digest Default — rychlost, low-touch
pinned image_tag = "stable-2026.04.7" v tfvars. Renovate polluje GHCR, otevírá PR s bump. Ops schválí → merge → apply Regulovaní zákazníci, audit trail

6.2 Gate pro auto režim

Jedinou ochranou před rozbitým :stable je CI smoke test před posunutím floating tagu. Pokud projde tam, prod auto-upgradne. Doporučení: mít i u Keboola instance monitoring + alert na /api/health degraded status, aby případný skluz smoke testu nezůstal dlouho bez povšimnutí.

6.3 Rollback

Rollback = změnit image_tag na předchozí verzi a docker compose up -d. Zjednodušená forma:

  • Auto režim: rychle přepnout watchtower na specifický tag; pak investigate
  • Pinned režim: PR revert, apply

7. Security model

7.1 Authentication mezi komponenty

Kdo → kde Jak se přihlásí
Public CI → GHCR push ${{ secrets.GITHUB_TOKEN }} (built-in)
VM → GHCR pull Public image, bez auth
Privátní CI → GCP SA JSON key v GCP_SA_KEY secret (Fáze 1); WIF (Fáze follow-up)
CI na zákaznickém GCP → Secret Manager SA má roles/secretmanager.admin
App na VM → Secret Manager VM má dedikovaný SA s roles/secretmanager.secretAccessor
App na VM → Keboola Storage Token z Secret Manageru

7.2 Deploy SA — scope per zákazník

SA agnes-deploy@<gcp-project> dostane jen tyto role:

roles/compute.instanceAdmin.v1     # create/update/delete VMs
roles/compute.securityAdmin        # firewall rules
roles/compute.networkAdmin         # static IP
roles/iam.serviceAccountUser       # attach VM SA k instancím
roles/secretmanager.admin          # vytvořit/rotovat secrets
roles/storage.admin                # tfstate bucket

Žádný owner, žádný editor. Blast radius pro leak SA key = přepis VMs v tomhle projektu. Nic mimo projekt, nic dat.

7.3 GitHub environmenty

environments:
  dev:
    # žádná protection
    secrets:
      GCP_SA_KEY: <same key>
  prod:
    protection_rules:
      required_reviewers: [@keboola-ops-team]
      wait_timer: 5m
      deployment_branches: main
    secrets:
      GCP_SA_KEY: <same key>

Oba environmenty sdílí ten samý SA key (jeden GCP, jedna identita). Rozdíl je jen v protection rules — kdo smí pushnout kam.

7.4 VM hardening

  • OS Login místo per-user SSH klíčů (follow-up)
  • Dedikovaný VM SA s minimem práv (jen read z Secret Manageru, nic dalšího)
  • Ephemeral disk strategy: boot disk = produkt (stateless), /data = persistent disk (stateful, snapshoty)
  • Žádný token v startup-script metadatě — všechny secrets teprve při boot z Secret Manageru

7.5 Rotace tajemství

Tajemství Kde žije Jak se rotuje
Keboola Storage token Secret Manager v zákaznickém GCP Keboola UI → nová verze v SM → app restart
JWT_SECRET_KEY Secret Manager, generováno TF terraform apply s -replace=google_secret_manager_secret_version.jwt
SA JSON key GitHub secret Vygenerovat nový klíč, paste do GH secret, smazat starý klíč v GCP
User passwords Argon2 hash v DuckDB users User-facing flow (reset endpoint, admin CLI)

8. Onboarding nového zákazníka

8.1 Kroky (cílový čas: < 1 hod)

1. Zákazník (nebo Keboola ops za něj) založí GCP projekt + billing
2. Někdo s owner rolí v projektu spustí bootstrap.sh:
   - Enable APIs (compute, iam, secretmanager, storage, iamcredentials)
   - Vytvoří SA agnes-deploy s rolemi
   - Vygeneruje SA key (předá ownerovi)
   - Vytvoří gs://agnes-{project}-tfstate
3. Zákazník (nebo Keboola ops) klonuje template:
   gh repo create {org}/agnes-infra --template keboola/agnes-infra-template --private
4. V novém repu:
   - Nastaví GH secret GCP_SA_KEY (paste z kroku 2)
   - Upraví terraform.tfvars na jejich hodnoty
   - Vytvoří initial commit + push
5. Nastaví Secret Manager tajemství (Keboola token atd.)
6. První PR s tfvars → plan → merge → apply
7. DNS — zákazník si později nastaví CNAME na IP (nebo zůstane na IP)
8. Admin user — bootstrap endpoint POST /auth/bootstrap nebo admin CLI
9. Smoke test: login, sync, query

8.2 Co je vidět komu

Role Vidí
Každý na internetu Public repo agnes-the-ai-analyst, jeho issues, PRs, image na GHCR
Keboola ops tým Výše + privátní template repo + infra-keboola repo
Zákazník (acme) Výše public + svůj vlastní infra-acme repo ve svém org
Nikdo Ostatní zákazníky kromě jejich vlastního

9. Tok změn

9.1 Change v app kódu (nejčastější)

1. Vývojář: push feature branch v public repu
2. Public CI: build :dev-feature-xyz (a :dev floating)
3. Watchtower na každé VM s image_tag = "dev": pullne do 5 min
   Watchtower na VM s image_tag = "dev-feature-xyz": pullne taky
4. Dev review
5. Merge do main
6. Public CI: build :stable-YYYY.MM.N (a :stable floating)
7. Smoke test CI: PASS → :stable se posune
8. Prod VMs:
   - auto režim: watchtower pullne do 5 min
   - pinned režim: Renovate otevře PR v privátním repu

9.2 Change v infra (VM size, dev VM list, nová disk)

1. Ops otevře PR v privátním infra repu
2. CI plan.yml: terraform plan → komentář v PR
3. Review + merge
4. CI apply.yml:
   - pro dev změny: environment "dev" → apply bez gatu
   - pro prod změny: environment "prod" → required reviewer → apply
5. Po apply: smoke test přes curl /api/health

9.3 Change v TF modulu

1. Maintainer otevře PR v public repu do infra/modules/customer-instance/
2. CI validuje modul proti examples/
3. Merge → auto git tag infra-v1.1.0
4. Renovate v každém privátním infra repu:
   → otevře PR "bump source ref to infra-v1.1.0"
5. Každý zákazník schvaluje samostatně → terraform plan → apply

10. Provozní aspekty

10.1 Monitoring a alerting (doporučení, ne v první vlně)

  • Cloud Monitoring dashboard per-customer
  • Alert na /api/health status != "healthy" déle než 5 min
  • Alert na VM CPU > 80 % déle než 30 min
  • Log-based metric: sync failures, auth failures, HTTP 5xx rate
  • Integrace se Slack/email přes Alerting policy

10.2 Backup

  • Snapshoty /data persistent disku denně, retention 30 dní (TF google_compute_resource_policy)
  • system.duckdb obsahuje users/permissions — při schema migraci snapshot kopie (již existuje jako *.pre-migrate)

10.3 Disaster recovery

  • Recreation VM z nuly = terraform apply (~5 min) + restore /data ze snapshotu (~5 min)
  • Total loss zákazníka = destroy GCP projektu; recreate ze snapshotu + tfstate

10.4 Cost per customer (orientačně)

Položka $/měs
Prod VM e2-small + 30GB SSD ~$15
Dev VM e2-small + 30GB SSD ~$15
Persistent disk (50 GB) ~$2
Static IP (×2 — prod, dev) ~$5
Snapshots (daily, 30d retention) ~$2
Secret Manager ~$0 (pod freetier)
Celkem base ~$40/měs

Škáluje lineárně s počtem dev VMs.

11. Principy / Non-goals

  • Public upstream zůstává public. Nic, co zákazníka identifikuje, tam není.
  • Zákazník má plnou kontrolu svého nasazení. Včetně rozhodnutí, zda upgradovat.
  • Žádná centrální Keboola ops infra. Žádný sdílený GCP projekt, žádný sdílený state.
  • Není to multi-tenant v jednom deploymentu. Jeden docker compose up = jeden zákazník.
  • Keboola není SaaS hostér (aspoň ne teď). Pokud zákazník chce managed, je to ručně poskytnutá služba, ne produkt.
  • Žádný cross-customer routing. Žádný sdílený load balancer, žádný sdílený DNS.

12. Rozhodnutí a otázky

Všechny designové otázky, které vznikly během brainstormingu, jsou vyřešené. Odkazy zde pro trasovatelnost:

Otázka Rozhodnutí
Managed vs self-deploy A) Pure self-deploy (mění se v Fázi 2+ pokud bude potřeba)
Centrální ops repo Ne — 1 public + 1 template + N per-customer
TF state lokace gs:// v zákaznickém GCP (default); flex na S3/TFC v template
Template repo název keboola/agnes-infra-template
CI auth SA JSON key v GH secret (Fáze 1); WIF (follow-up)
Image visibility Public na GHCR
Prod upgrade režim Per-instance volba auto/pinned, default auto
TLS Caddy default, flex na gcp-lb/cloudflare
DNS Zákazník si řeší sám, default jen IP
GCP projekt pro Keboola internal-prod zůstává
Dev VM model Seznam dev_instances v tfvars, per-položka image_tag
ZdenekSrotyr/tmp_oss Smazat po Fázi 1

13. Glosář

Zkratka Význam
GHCR GitHub Container Registry — ghcr.io
WIF Workload Identity Federation — GCP mechanismus auth CI bez static key
SA Service Account (GCP)
TF Terraform
OIDC OpenID Connect — auth protokol, GitHub vydává OIDC tokeny pro GHA
CalVer Calendar Versioning — YYYY.MM.N
PD Persistent Disk (GCP)

14. Follow-up iterace

Mimo scope této první vlny, ale plánováno:

  • WIF místo SA JSON key (bezpečnost)
  • OS Login (odstranění osobních SSH klíčů)
  • Monitoring + alerting (Cloud Monitoring, Slack integration)
  • Automatické snapshoty + restore procedura
  • Ephemeral PR preview environments
  • AWS podpora (paralelní TF modul)
  • Plugin API pro proprietární customer extensions (viz issue #8)
  • Managed service varianta (Keboola hostuje za zákazníka)

15. Reference

  • Předchozí spec: docs/superpowers/specs/2026-04-09-multi-instance-deployment-design.md (CalVer release model)
  • Issue: keboola/agnes-the-ai-analyst#8 — plugin API for private customer extensions