Internal Documentation internal
TalkIDE internal documentation

Status: Accepted Datum: 2026-05-08 Oblast: Stopa B.5 / Kubernetes orchestrace

Context

Stopa B.5 navazuje na B.4

ADR-017 zavedl AppDeployer — per-app Deployment + ClusterIP Service v namespace tenant-{tenantSlug}. Service je ale dostupná pouze interně v clusteru. Stopa B.5 přidává Ingress s TLS, díky čemuž je user-app dostupná z veřejného internetu.

Dosud po úspěšném deployi Kai obdržel 200 OK s deploymentName a serviceName, ale URL pro uživatele neexistovala. B.5 uzavírá tuto mezeru: pipeline se prodlouží o IngressProvisioner krok a uživatel dostane funkční HTTPS URL.

Architektura DEV/PROD prostředí

TalkIDE provozuje dva paralelní životní cykly per projekt:

ProstředíURL patternTriggerDB
DEV (Preview){projectUuid}.talkide.appKaždý deploy (Mara commit)Per-deploy vlastní DB
PROD (Published){slug}.talkide.appExplicitní Publish trigger (B.7)Persistent vlastní DB

Oba env běží paralelně v namespace tenant-{tenantSlug} se svými vlastními K8s resources. projectUuid je persistent per projekt (generuje se při Create Project, neregeneruje při redeploy) — zajišťuje stabilní “share preview link” URL pro sdílení WIP buildu.

B.5 se zaměřuje výhradně na DEV prostředí. PROD env (Publish flow) přidá Stopa B.7 spolu s DRAFT→PUBLISHED state machine.

Caller: Kai (Mara’s DevOps agent)

Kai je AI agent (Anthropic Claude Agent SDK) běžící jako Node.js sidecar v stejném BE podu. Po úspěšném B.4 deploy Kai rozšíří volání internal endpointu o projectUuid. BE provede AppDeployer → IngressProvisioner jako jednu atomickou operaci.

Kai nesmí nést žádný interní auth token (riziko leaku do Anthropic logs při tool-call serializaci) — internal endpoint zůstává localhost-only jako v B.4 (ADR-017, sekce 4).

Deployment topology (zděděno z ADR-014)

Prostředítalkide.k8s.enabledWired bean
local (výchozí)false (matchIfMissing)NoopIngressProvisioner
cloud (prod pod)trueK8sIngressProvisioner

Decision

1. Konfigurační pattern (Variant B — konzistentní s ADR-014/015/016/017)

IngressProvisioner  (Kotlin interface — v package features/deployment/)
  fun provisionIngress(tenantId: Long, slug: String, env: String, projectUuid: String): IngressProvisioningResult

K8sIngressProvisioner  (@ConditionalOnProperty(name = ["talkide.k8s.enabled"], havingValue = "true"))
  - inject: KubernetesClient (fabric8)
  - inject: TenantRepository (lookup tenantSlug → namespace name)
  - implementace: get-or-create|patch Ingress s TLS

NoopIngressProvisioner  (@ConditionalOnProperty(name = ["talkide.k8s.enabled"], havingValue = "false", matchIfMissing = true))
  - log volání, vrátí stub IngressProvisioningResult

@ConditionalOnProperty(matchIfMissing=true) na Noop je deterministický fallback — konzistentní s erratou z ADR-014 (@ConditionalOnMissingBean není spolehlivý v test context loaderu při Spring Boot 3.4.4).

2. Package: features/deployment/

IngressProvisioner patří do features/deployment/ — stejný package jako AppDeployer (B.4). Ingress je přímé pokračování deployment lifecycle: bez Ingress není user-app veřejně dostupná. Oddělení do hypotetického features/k8s/ingress/ by rozbilo koherenci deployment pipeline a komplikovalo orientaci pro nového developera.

3. Jeden Ingress per app (ne shared Ingress)

Každá user-app dostane vlastní Ingress resource:

  • Jméno: app-{slug}-{env} (example: app-demo-dev)
  • Namespace: tenant-{tenantSlug}

Odmítnutý přístup — shared Ingress v platformním namespace talkide s dynamickými rules — by vyžadoval centrální write přístup a riskoval race condition při souběžných deployích. Výhody per-app Ingress:

  • Izolace: smazání projektu = kubectl delete ingress app-{slug}-dev v namespace, žádná modifikace sdíleného resource
  • Paralelní DEV + PROD: každé prostředí má vlastní Ingress bez konfliktu rules
  • Čitelnost: kubectl get ingress -n tenant-acme zobrazí přesně apps v tomto tenantu
  • RBAC: namespace-scoped oprávnění; BE pod nepotřebuje cluster-wide write na Ingress

4. DEV hostname: {projectUuid}.talkide.app

UUID je persistent per projekt — generuje se při Create Project jako UUID v6 (time-ordered, lepší pro DB indexing než v4), ukládá se do sloupce projects.uuid. Neregeneruje se při redeploy.

  • Forma v URL: lowercase, dashes — 01959e8c-6c2e-7fd5-a0e0-abc123456789.talkide.app
  • Wildcard cert pokrytí: *.talkide.app matchuje jeden level subdomény → UUID hostname je validně pokryt
  • Reserved slug validator (talkide-be#38) UUID nekontroluje — UUID je v oddělené subdomain namespace a nemůže kolidovat s rezervovanými slovy jako api, www apod.
  • Stabilní “share preview link”: Kai může vygenerovat URL ihned po první deploy; URL zůstane platná i po dalších redeploy stejného projektu

5. TLS strategie: secret-copy do tenant namespace

Wildcard certifikát *.talkide.app je spravován cert-managerem jako K8s Secret talkide-tls-cert v platformním namespace talkide. Kubernetes neumí cross-namespace secret reference — Ingress v tenant-{slug} ns nemůže přímo odkazovat na secret z talkide ns.

Zvolená strategie: zkopírovat secret do tenant namespace při namespace provisioning.

Pattern je konzistentní s registry-talkide pull secret, který K8sNamespaceProvisioner (ADR-015 / B.2) již kopíruje do každého nového tenant namespace. Rozšíření: K8sNamespaceProvisioner zkopíruje také talkide-tls-cert při vytvoření namespace.

Výhody:

  • Každý Ingress má explicitní tls blok se secretName: talkide-tls-cert — čitelné a debuggable bez znalosti cert-manager internals
  • Vyhneme se per-app cert (Let’s Encrypt rate limit: 50 certů / doménu / týden)
  • Nulová závislost na cert-manager annotation na Ingress — cert je již hotov

Cert renewal follow-up: cert-manager renewuje secret v platformním namespace talkide. Po renewal je nutné re-syncovat nový secret do všech tenant ns. Mechanismus — rozšíření K8sNamespaceProvisioner o scheduled job/watcher — je odložen jako post-MVP follow-up. Bez tohoto stepu wildcard cert expiruje za 90 dní od posledního kopírování (Let’s Encrypt TTL). Issue evidovat před public-alpha launch.

6. Scope B.5: pouze DEV env

K8sIngressProvisioner přijímá parametr env: String a validuje, že hodnota je "dev". Jakákoliv jiná hodnota způsobí IllegalArgumentException před voláním K8s API.

prod env přidá Stopa B.7. Architektura je záměrně připravena — env je String, ne enum — přidání prod nevyžaduje změnu interface.

7. Kubernetes Ingress manifest

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-{slug}-dev
  namespace: tenant-{tenantSlug}
  labels:
    app: user-app
    tenant: {tenantSlug}
    slug: {slug}
    env: dev
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    # cert-manager.io/cluster-issuer záměrně vynechán — TLS cert je kopírovaný secret,
    # ne dynamicky vydávaný; cert-manager annotation by způsobil konflikt
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - "{projectUuid}.talkide.app"
      secretName: talkide-tls-cert   # zkopírovaný do tenant ns z platformního ns
  rules:
    - host: "{projectUuid}.talkide.app"
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-{slug}-dev   # ClusterIP Service z B.4 (ADR-017)
                port:
                  number: 80

Poznámky k manifestu:

  • ssl-redirect: "true" — HTTP → HTTPS redirect je povinný; žádný plaintext přístup
  • cert-manager.io/cluster-issuer annotation je záměrně vynechán — přítomnost annotation by spustil cert-manager, který by se pokusil vydat nový cert a kolidoval by s existujícím zkopírovaným secret
  • Service backend port: 80 odpovídá ClusterIP Service z ADR-017 (mapuje na containerPort 8080)

8. Idempotence (redeploy)

Get-or-create|patch pattern — konzistentní s ADR-017 (AppDeployer):

Ingress existuje?Akce
Necreate()
Anopatch() s aktuálními rules a tls

patch() na Ingress triggeruje ingress-nginx reload (~sekunda), žádný downtime na BE side. Idempotence umožňuje Kaie volat deploy opakovaně bez vedlejších efektů.

9. Integrace do DeployAppUseCase — atomic operation

IngressProvisioner je volán z DeployAppUseCase po AppDeployer.deployApp(). Pipeline:

BuildService → AppDeployer.deployApp() → IngressProvisioner.provisionIngress()

Volání zůstávají synchronní. Selže-li IngressProvisioner, AppDeployer již uspěl — DeployAppUseCase zaznamená warning do logu a vrátí partial result (deployment OK, ingress fail). Kai obdrží HTTP 200 s ingressStatus: "failed" a může zkusit retry (idempotence zaručuje čistý výsledek).

Rozšíření API kontraktu internal endpointu (rozšíření z B.4):

POST /api/internal/deploy
Content-Type: application/json

{
  "tenantId": 1,
  "slug": "demo",
  "imageTag": "registry.digitalocean.com/talkide/demo:abc123",
  "projectUuid": "01959e8c-6c2e-7fd5-a0e0-abc123456789"
}
{
  "deploymentName": "app-demo-dev",
  "serviceName": "app-demo-dev",
  "ingressName": "app-demo-dev",
  "previewUrl": "https://01959e8c-6c2e-7fd5-a0e0-abc123456789.talkide.app",
  "deploymentStatus": "applied",
  "ingressStatus": "applied"
}

ingressStatus nabývá hodnot applied (úspěch) nebo failed (chyba při provisioning). previewUrl je vždy vrácena (BE ji sestaví ze vstupního projectUuid), i pokud ingressStatus == "failed" — Kai může URL zobrazit uživateli, ale s varováním.

10. Error handling

K8sIngressProvisioner nezachytává KubernetesClientException — propaguje ji ven. DeployAppUseCase ji zachytí, zapíše warning log a vrátí partial result (viz sekce 9).

Mapování výjimek na HTTP response v @RestControllerAdvice:

KubernetesClientException (ingress) → partial result, HTTP 200 s ingressStatus: "failed"
IllegalArgumentException (env != "dev") → 400 Bad Request
  { "code": "VALIDATION", "message": "Only 'dev' env is supported in B.5" }

11. Ingress nevystavuje /api/internal/

ingress-nginx rules pro TalkIDE platformní URL (api.talkide.app) neexposují /api/internal/** cestu. User-app Ingress rules (uuid.talkide.app) míří výhradně na user-app ClusterIP Service — internal endpoint BE platformy je fyzicky v jiném namespace a jiné Service. Žádná explicitní deny rule není nutná; routování zajišťuje K8s namespace izolace.

12. Přidání projectUuid sloupce do projects tabulky

B.5 vyžaduje, aby projects tabulka obsahovala sloupec uuid VARCHAR(36) NOT NULL UNIQUE. Liquibase migrace přidá sloupec a seed data ho naplní. CreateProjectUseCase generuje UUID v6 při vytvoření projektu.

UUID v6 se volí namísto v4 z důvodu time-ordered generování — lepší pro B-tree index v PostgreSQL (nové záznamy jsou sekvenčně za starými, méně page splits). Vizuálně jsou v URL lépe čitelné — prefix kóduje čas vzniku.

13. Test plán

VrstvaToolRozsah
Unit — K8sIngressProvisionermockito-kotlin, explicit mock variables~5 testů
Unit — NoopIngressProvisionermockito-kotlin~3 testy
Integration smoke — K3sIngressSmokeIntegrationTestTestcontainers k3s1 test
Controller test (rozšíření B.4 testu)MockMvcaktualizovat existující test

Unit testy K8sIngressProvisioner (mockito-kotlin):

Explicitní mock variables per resource type (konzistentní s ADR-017):

val ingressOps = mock<NonNamespaceOperation<Ingress, ...>>()
val ingressInNs = mock<MixedOperation<Ingress, ...>>()
val ingressWithName = mock<Resource<Ingress>>()

Pokryté scénáře:

  1. Happy path — nový Ingress (neexistuje → create())
  2. Redeploy — Ingress existuje → patch() s aktuálními rules
  3. Delete Ingress (cleanup při smazání projektu)
  4. K8s API error při create → KubernetesClientException propaguje
  5. env != "dev"IllegalArgumentException před K8s voláním

Integration smoke test (Testcontainers k3s):

Vytvoř Ingress, ověř:

  • Ingress.spec.rules[0].host == "{projectUuid}.talkide.app" (spec je správná)
  • Ingress.spec.tls[0].secretName == "talkide-tls-cert" (TLS blok je přítomen)
  • Ingress.metadata.labels["env"] == "dev" (labels jsou správné)

V k3s bez load balanceru Ingress.status.loadBalancer.ingress bude prázdné — postačí ověřit spec (stejný přístup jako smoke test AppDeployer z ADR-017 errata).


Consequences

Pozitiva

  • KonzistenceIngressProvisioner wrapper zapadá do patternu ADR-014/015/016/017 bez výjimek; nový developer vidí identický Variant B vzor napříč všemi provisionery.
  • Atomic deploy pipeline — Kai volá jeden endpoint, BE orchestruje AppDeployer + IngressProvisioner; žádný 2-step choreography na straně Kaie.
  • Stabilní preview URLprojectUuid se negeneruje při redeploy; uživatel může sdílet odkaz na WIP build; URL zůstane platná po celý lifecycle projektu.
  • Izolace per-app Ingress — smazání projektu = čisté smazání namespace (vč. Ingress); žádná modifikace sdíleného resource.
  • Idempotence — Kai může volat deploy opakovaně; výsledek je vždy konzistentní K8s stav.
  • Partial result handling — selže-li Ingress provisioning, deployment zůstane živý; retry opraví situaci bez potřeby full-redeploy.

Rizika a omezení

  1. Cert renewal bez automatické re-sync — secret talkide-tls-cert zkopírovaný do tenant ns není automaticky aktualizován při cert-manager renewal. Bez follow-up implementace watcher/scheduleru expiruje TLS cert za 90 dní od posledního kopírování. Nutno evidovat jako blocking::public-alpha issue před launch.

  2. UUID v URL — UUID v6 je méně přívětivé než slug (01959e8c-....talkide.app), ale zajišťuje stabilitu a neduplicitnost. Uživatelsky přívětivý stable URL = PROD slug v B.7.

  3. Single wildcard cert = flat namespace*.talkide.app pokrývá jen jeden subdomain level. Multi-level subdomény (např. dev.{uuid}.talkide.app) by vyžadovaly druhý wildcard certifikát. Pro MVP flat namespace postačuje.

  4. Custom domény odloženy — uživatelé nemohou v B.5 přidat vlastní doménu (cname na user-app). Vyžaduje per-app TLS cert (cert-manager + DNS challenge) — post-alpha feature.

  5. Localhost-only internal endpoint předpokládá Kai v stejném podu — konzistentní omezení z ADR-017; Kai musí zůstat Node.js sidecar v BE podu.


Alternatives Considered

TLS strategie A: per-app cert (cert-manager cluster-issuer annotation)

Cert-manager vydá Let’s Encrypt cert pro každý {uuid}.talkide.app dynamicky při vytvoření Ingress. Výhoda: zero-copy, cert je přímo v tenant ns. Nevýhody:

  • Let’s Encrypt rate limit: 50 certů / registrovaná doména / týden — při aktivní platformě (desítky projektů) dosažitelný limit
  • Latence: první request na novou app čeká na cert issuance (~30 s)
  • Komplexita: cert-manager musí mít přístup k DNS challenge pro *.talkide.app

Odmítnuto ve prospěch secret-copy strategie (wildcard cert je jeden cert pro všechny apps).

TLS strategie B: default-ssl-certificate flag ingress-nginx

ingress-nginx podporuje globální --default-ssl-certificate flag — Ingress bez tls bloku dostane wildcard cert automaticky. Výhoda: žádné kopírování secret. Nevýhody:

  • Ingress manifesty nemají explicitní tls blok — obtížně debuggable (není vidět, jaký cert je použit bez inspekce nginx config)
  • Vyžaduje změnu ingress-nginx Helm values (globální flag) — infrastrukturní zásah mimo scope B.5
  • Ztráta per-Ingress flexibility pro budoucí custom domény

Odmítnuto ve prospěch explicitního tls bloku s zkopírovaným secret.

Shared Ingress v platformním namespace talkide

Jeden Ingress resource v ns talkide s dynamicky spravovanými rules pro každou user-app. Výhoda: jeden resource místo N. Nevýhody:

  • Race condition při souběžných deployích (dva BE instance modifikují stejný resource)
  • Smazání projektu vyžaduje patch shared resource (ne čisté delete)
  • BE pod potřebuje cluster-wide write oprávnění na Ingress (narušuje least-privilege)
  • Debugging: kubectl get ingress -n talkide zobrazí stovky rules dohromady

Odmítnuto ve prospěch per-app Ingress.

UUID v4 místo UUID v6 pro projectUuid

UUID v4 je plně náhodný — žádná časová struktura. V URL jsou vizuálně identické. Nevýhoda: náhodné UUID způsobují random page splits v B-tree indexu PostgreSQL při insertech — výkonnostní degradace při škálování. UUID v6 (time-ordered) generuje sekvenčně nové hodnoty → nové rows se přidávají na konec indexu → výrazně méně page splits.

Odmítnuto v4 ve prospěch v6.

Separátní endpoint POST /api/internal/ingress (Možnost B)

Kai by volal dvě operace: nejprve POST /api/internal/deploy, poté POST /api/internal/ingress. Výhoda: separace concerns, každý endpoint má vlastní retry logiku. Nevýhody:

  • Kai musí orchestrovat pořadí volání — přidává komplexitu na straně AI agenta
  • Riziko, že Kai zapomene zavolat ingress endpoint (prompt engineering fail)
  • Více kódu, více testů, více surface area

Odmítnuto ve prospěch atomic operace v DeployAppUseCase.


Implementation Notes

  • Issue: talkide-be#22https://gitlab.com/talkide/talkide-be/-/work_items/22
  • ADR-014 (B.1): K8s client foundation — KubernetesClient bean, fabric8 DSL patterns
  • ADR-015 (B.2): K8sNamespaceProvisioner — vzor pro kopírování secrets do tenant ns
  • ADR-017 (B.4): AppDeployer — naming convention app-{slug}-{env}, package features/deployment/
  • Navazující: #23 / Stopa B.6 (Pod status watch loop / SSE → UI streaming)
  • Navazující: #24 / Stopa B.7 (Publish flow, PROD env Ingress {slug}.talkide.app)
  • Cert renewal follow-up: evidovat jako blocking::public-alpha issue — re-sync talkide-tls-cert do tenant ns po cert-manager renewal (scheduled job nebo K8s watcher)
  • talkide-be#38 (reserved slug validator): slug vstupuje do Ingress jména; nevalidní slug = K8s API error — validator musí být hotov před B.5 deployem
  • talkide-be#44 (tenant slug RFC-1123 validator): stejná závislost jako v ADR-017

Errata

(Bude doplněno při opravách nebo upřesněních.)


Was this page helpful?

Thanks for the feedback.