Skip to content

Example: Keycloak in-cluster

The generic recipe is in Onboard an app. This page is the Keycloak fill-in on this company’s first Project Loom node — not a special snowflake. The next server copies workloads/keycloak/{base,overlays} and changes hosts.

The operator lives in namespace keycloak. Each instance is an ApplicationSet app: keycloak-staging and keycloak-prod.

flowchart TD
  Op[apps/keycloak-operator] --> Operator[Keycloak Operator]
  AS[apps/workloads.yaml] --> Stg[overlays/staging]
  AS --> Prod[overlays/prod]
  Stg --> CR1[Keycloak CR name keycloak]
  Stg --> PG1[keycloak-pg]
  Prod --> CR2[Keycloak CR name keycloak]
  Prod --> PG2[keycloak-pg]
  Operator -. manages .-> CR1
  Operator -. manages .-> CR2

apps/keycloak-operator.yaml syncs platform/keycloak-operator into keycloak, pinned to 26.6.3, ServerSideApply (CRDs are large).

workloads/keycloak/base/keycloak.yaml is HTTP-only behind Traefik. Overlay patches set hostname and Ingress host.

spec:
instances: 1
db:
vendor: postgres
host: keycloak-pg-rw
database: keycloak
http:
httpEnabled: true
ingress:
enabled: false

CNPG cluster keycloak-pg (secret keycloak-pg-app). Operator Service keycloak-service on :8080. TLS Secret keycloak-tls. UAT uat.auth.webbies.dev (overlay still staging), prod auth.webbies.dev.

keycloak-config-cli (Sync Job) applies workloads/keycloak/overlays/<env>/realms/*.yaml. Not KeycloakRealmImport. First-time service account: Provision §8. Lockout later: Restore.

App-specific clients (Penvoice public PKCE, Talon confidential BFF, …) live in those realm files because those products are on this node. The next app adds its own realm file — that is product config, not platform process.

The API is a resource server: it validates JWTs against that env’s issuer. Issuer URL is an Infisical value, not Git. Same pattern for any OIDC app.