agnes-the-ai-analyst/docs/superpowers/specs/2026-04-21-multi-customer-deployment-spec.md
ZdenekSrotyr 4e4d2a39e6
chore(oss): isolate customer-specific deploy bits from scripts/grpn/ (#88, wave 1) (#94)
* chore(oss): isolate customer-specific deploy bits from scripts/grpn/ (#88)

Vendor-neutralization step before public release. The directory mixed
two concerns: (1) generic ops scripts referenced from mainline OSS
infrastructure (TLS rotation, auto-upgrade cron) and (2) one operator's
hackathon manual-deploy helper with hardcoded GCP project IDs, VM names,
and admin emails. Splitting them per concern.

Moved (still in OSS, just under a vendor-neutral name):
- scripts/grpn/agnes-tls-rotate.sh   → scripts/ops/agnes-tls-rotate.sh
- scripts/grpn/agnes-auto-upgrade.sh → scripts/ops/agnes-auto-upgrade.sh

Removed (belongs in private consumer infra repos, not upstream OSS):
- scripts/grpn/Makefile (hardcoded prj-grp-foundryai-dev-7c37, foundryai-development VM name, e_zsrotyr@groupon.com bootstrap email)
- scripts/grpn/README.md (GRPN hackathon deploy walkthrough)
- docs/superpowers/plans/2026-04-22-grpn-deploy-learnings.md (org-specific deploy log)

Cross-refs updated in README.md, CLAUDE.md, docs/DEPLOYMENT.md,
docker-compose.yml. CHANGELOG entry flags BREAKING (ops) for any
consumer infra repo that installs these scripts via path-based systemd
timers.

This is the first wave of #88 — the remaining leaks (test data with
prj-grp-dataview-prod-1ff9, AIAgent.FoundryAI tags in OpenMetadata test
fixtures, docstrings in connectors/openmetadata/enricher.py) will be a
separate, smaller PR.

Refs #88.

* chore(oss): comprehensive vendor-neutralization (#88 wave 2 + review fixes)

PR #94 review found that the original wave-1 grep was scoped wrong and
many leaks survived. This commit closes wave 1 properly AND folds in all
wave-2 anonymization in a single pass — easier to review than two PRs.

Wave-1 review-fix corrections:
- Caddyfile: scripts/grpn/agnes-tls-rotate.sh → scripts/ops/ (the original
  wave-1 grep filter excluded extensionless files like Caddyfile).
- CHANGELOG bullet rewritten — original wording implied an in-repo migration
  for infra/modules/customer-instance/, which is wrong (the TF module embeds
  the script inline via heredoc, never sourced from scripts/grpn/). Now
  flags downstream consumer infra repos only.
- infra/modules/customer-instance/variables.tf: Czech docstring with `grpn`
  example → English description with `acme, example` placeholders.

Wave-2 anonymization:
- Code docstrings (connectors/openmetadata/{client,transformer,enricher}.py,
  src/catalog_export.py, scripts/duckdb_manager.py): prj-grp-… →
  my-bq-project / prj-example-1234, AIAgent.FoundryAI → AIAgent.MyAgent,
  FoundryAIDataModel → AnalyticsDataModel.
- Test fixtures (4 files): same set of replacements — 157 tests still pass.
- .github/workflows/keboola-deploy.yml: "Groupon-side dev VMs" comment →
  generic "per-developer dev VMs".
- docs/auth-groups.md + scripts/debug/probe_google_groups.py:
  kids-ai-data-analysis project name → acme-internal-prod placeholder.
- 5 planning/spec docs under docs/superpowers/{plans,specs}/2026-04-21-*:
  hardcoded IPs (34.77.94.14, 34.77.102.61) → <dev-vm-ip>/<prod-vm-ip>;
  GRPN/Groupon → Acme/another-customer; prj-grp-… → prj-example-….
- scripts/switch-dev-vm.sh deleted — hackathon-era helper hardcoded to a
  specific shared dev VM. Per-developer dev VMs are the supported pattern.

Final grep `groupon|grpn|foundryai|prj-grp|groupondev|34\.77\.(94|102)\.…|kids-ai-data`
returns zero hits (excluding CHANGELOG.md historical entries).

CHANGELOG entry expanded to document both waves under one bullet, with
the BREAKING (ops) clarification about the TF module being unaffected.

Refs review of #94, closes #88.

* fix(oss): close remaining #94 review-2 findings (Czech, padak refs, CHANGELOG)

Reviewer of PR #94 round 2 caught 4 remaining items the wave-2 pass missed:

1. infra/modules/customer-instance/variables.tf had Czech descriptions on
   8 more variables. Previous review only flagged line 19; this round
   audited the rest. Translated lines 2, 28, 42-46 (heredoc), 60, 65, 71,
   78, 84 to English. Same review concern: a Terraform module that is
   the customer-facing API surface in Czech is unfit for OSS distribution.

2. infra/modules/customer-instance/outputs.tf had Czech descriptions on
   four outputs. Same fix.

3. docs/padak-security.md referenced a private repo (padak/keboola_agent_cli#206)
   in two places. Replaced with generic 'tracked upstream in the auth-CLI repo'
   per CLAUDE.md vendor-agnostic rule (no cross-refs to private repos).

4. scripts/fetch-env-from-secrets.sh:41 had a Czech comment.
   Translated.

5. CHANGELOG cosmetic: bullet said 'AIAgent.FoundryAI -> AIAgent.MyAgent'
   but the actual code uses both MyAgent (in docstrings) and Example
   (in test fixtures). Reworded to mention both targets.

Final grep across all shipping file types (.md, .py, .yml, .yaml, .sh,
Makefile, .json, .tf, .tpl, Caddyfile, .toml) for groupon|grpn|foundryai|
prj-grp|groupondev|34.77.94.14|34.77.102.61|kids-ai-data|padak/keboola_agent_cli
returns ZERO hits (excluding CHANGELOG.md). Czech-diacritic grep across
.tf/.toml/Caddyfile/Makefile/.yml returns ZERO hits.

157/157 OpenMetadata + DuckDB tests still pass.

* fix(oss): close #94 round-3 leaks (env.template, instance.yaml.example, padak typo)

Round-3 reviewer caught two MUST-FIX leaks the round-2 grep missed
(grep was scoped to extensions that did not include .template / .example
suffixes — the audit was right, the previous grep was not paranoid enough):

1. config/instance.yaml.example:114 — '(optional - Groupon-specific)' brand
   leak in a shipping config example. Replaced with '(optional)'.

2. config/.env.template:68 — stale path 'scripts/grpn/agnes-tls-rotate.sh'
   in operator-facing env-template comment. The script lives at
   scripts/ops/ now (commit 16a85cc); this comment had been pointing
   operators at a non-existent path.

3. docs/padak-security.md:188 — phrase duplication 'tracked in tracked
   upstream' from a sloppy substitution in round-2. Trivial wording fix.

Final paranoid grep across .md/.py/.yml/.yaml/.sh/Makefile/.json/.tf/.tpl/
Caddyfile/.toml/.template/.example/.env* with the full token set
(groupon|grpn|foundryai|prj-grp|groupondev|34\.77\.94\.14|34\.77\.102\.61|
kids-ai-data|padak/keboola_agent_cli) returns ZERO hits, excluding
CHANGELOG.md historical entries.

* fix(oss): #94 round-4 — QUICKSTART.md + rename padak-security.md

Devin Review caught two findings on the latest round-3 commit:

1. docs/QUICKSTART.md:67 still pointed users at the deleted
   scripts/switch-dev-vm.sh. A Quickstart user following step-by-step
   would hit a missing-file error at the final step. Replaced with the
   inline gcloud-ssh equivalent that the Removed bullet documents.

2. docs/padak-security.md filename retains the personal identifier
   'padak'. The PR fixed the body content (replaced
   padak/keboola_agent_cli#206 references with generic wording) but
   missed the filename. Renamed to docs/security-audit-2026-04.md
   (date-anchored, vendor-neutral). Updated the historical CHANGELOG
   link to point at the new path with an inline note about the rename.

* fix(oss): redact remaining hardcoded IPs from planning docs + remove default email

Devin Review caught two more leaks:
1. scripts/fetch-env-from-secrets.sh line 16 had a hardcoded
   personal-email default (zdenek.srotyr@keboola.com). Replaced with
   ':?' bash error so SEED_ADMIN_EMAIL must be explicitly set —
   safer than carrying any specific identity.
2. Planning docs still had 35.195.96.98 and 34.62.223.189 (legacy
   prod/dev IPs) that the round-1 IP-replace pattern missed (it only
   targeted 34.77.x.x). Generic regex redaction across all five
   planning docs replaces every public IP with <redacted-ip>,
   preserving private/loopback/IAP ranges.
2026-04-27 20:24:34 +02: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