Raw Kubernetes manifests for Keycloak
Context and Problem Statement
The initial approach used the Bitnami Keycloak Helm chart with the official quay.io/keycloak/keycloak image. In practice, the Bitnami chart proved to be a poor fit: Bitnami templates hardcode paths under /opt/bitnami/, set runAsUser: 1001 (Bitnami convention), and use Bitnami-specific environment variables (KEYCLOAK_EXTRA_ARGS, KEYCLOAK_DATABASE_*) that have no effect on the official Keycloak image. Making the Bitnami chart work with the official image required overriding nearly every template value, negating the chart's value as a pre-built package.
This mirrors the same problem encountered with RabbitMQ (ADR-0005): Bitnami deprecated their free images, and Bitnami chart templates are structurally incompatible with official upstream images.
Decision Drivers
- Bitnami chart templates assume Bitnami image conventions (
/opt/bitnami/, UID 1001, Bitnami env vars) - The official Keycloak image uses different paths (
/opt/keycloak/), UID 1000/GID 0, and nativeKC_*env vars - Overriding the Bitnami chart to work with the official image requires customizing nearly every field, defeating the purpose of a Helm chart
- AKS Automatic Deployment Safeguards require seccomp profiles, probes, and resource requests on every container. Easier to enforce with full manifest control
- Keycloak is a single StatefulSet + 2 Services + 1 ConfigMap. Low complexity, manageable without a chart
Considered Options
- Bitnami chart with official image (initial approach, abandoned): too many overrides needed
- Raw Kubernetes manifests: StatefulSet, Services, ConfigMap, Secrets managed directly in Terraform
- Keycloak Operator: full operator pattern, higher complexity than needed for single instance
Decision Outcome
Chosen option: "Raw Kubernetes manifests", because Keycloak's deployment footprint is small enough (one StatefulSet, two Services, one ConfigMap for realm import) that a Helm chart adds more complexity than it removes. This follows the same pattern as RabbitMQ (ADR-0005).
Implementation: All Keycloak resources are defined in software/stack/modules/keycloak/main.tf using Terraform kubectl_manifest (StatefulSet, Services) and kubernetes_config_map / kubernetes_secret resources.
Key configuration:
- Image:
quay.io/keycloak/keycloak:26.5.4(pinned tag) args: [start, --import-realm]: native Keycloak CLI argsrunAsUser: 1000,runAsGroup: 0: official image UID/GIDreadOnlyRootFilesystem: false: official image requires writable/opt/keycloak/data/- Health endpoints on management port 9000 (
KC_HTTP_MANAGEMENT_PORT) - PostgreSQL backend (
KC_DB=postgres, JDBC URL to CNPG cluster) - OSDU realm auto-imported via ConfigMap mounted at
/opt/keycloak/data/import datafierclient with service account (email: datafier@service.local) pre-configured in realm JSON- Namespace:
platform(shared with other middleware, see ADR-0017)
Access model: Internal-only, no HTTPRoute/Gateway exposure. OSDU services reach Keycloak via keycloak.platform.svc.cluster.local:8080. Admin console access requires kubectl port-forward.
Consequences
- Good, because full control over every field. AKS safeguards compliance is straightforward
- Good, because no Bitnami dependency, neither chart nor images
- Good, because realm import with
datafierclient and service account email is declared in Terraform (reproducible) - Good, because upgrades are a single image tag change with clear diff
- Bad, because more YAML to maintain compared to a working Helm chart (but less than fighting a broken one)
- Bad, because no Helm lifecycle management (rollback, history)