KeycloakInstance Guide¶
KeycloakInstance is the primary workload resource — one CR describes one
Keycloak deployment. This guide covers each part of the spec that needs more
than a one-line field reference; for the full field-by-field list see the
CRD Reference.
Reconcile picture¶
flowchart LR
U([kubectl apply]) -->|KeycloakInstance CR| K8s[(Kubernetes API)]
K8s --> Op[Operator reconcile loop]
Op -->|1| Val[Validate spec]
Op -->|2| Ver[Resolve version\nvia KeycloakVersionMap]
Op -->|3| PG[Provision/check Postgres\nmanaged or BYO]
Op -->|4| Admin[Ensure admin bootstrap Secret]
Op -->|5| Render[Render + SSA child resources]
Op -->|6| Watch[Watch StatefulSet readiness]
Every child object carries a controller owner reference — Kubernetes garbage
collection deletes them with the KeycloakInstance — except the managed
PerconaPGCluster, which is deliberately retained. See
Managed backend retention.
Hosts¶
spec.hosts.host is structurally required, even when
spec.ingress.enabled: false — it always drives KC_HOSTNAME. Omitting
adminHost means no admin Ingress is created and KC_HOSTNAME_ADMIN stays
unset (Keycloak serves the admin console on the same host as the public one).
Ingress & TLS¶
spec:
ingress:
enabled: true # default; false = Services only (dev/kind)
class: nginx # default; public Ingress class
adminClass: nginx-internal # default; admin Ingress class
clusterIssuer: letsencrypt-prod # empty = no TLS annotation, plain HTTP
annotations:
custom.example.com/foo: bar
enabled: falserenders Services only — no Ingress objects at all. Use this for dev/kind clusters without an ingress controller; reach Keycloak viakubectl port-forward.clusterIssueris the only switch for TLS. Leaving it empty produces plain-HTTP Ingresses (no cert-manager annotation, no TLS secret). Set it to aClusterIssuername already provisioned on the cluster to get a cert-manager-issued certificate on both Ingresses.annotationsare merged onto both Ingresses, but the operator's own security-snippet annotation and (if set) the cluster-issuer annotation always take precedence — you cannot override them viaannotations.- The admin Ingress is only created when
spec.hosts.adminHostis set.
Postgres: BYO vs. Managed¶
BYO (managed: false)¶
apiVersion: v1
kind: Secret
metadata:
name: my-keycloak-pg-credentials
stringData:
host: pg.example.com # required
dbname: keycloak # required
user: keycloak # required
password: <password> # required
port: "5432" # optional; Keycloak defaults to 5432 when absent
These four/five keys are wired directly to KC_DB_URL_HOST /
KC_DB_URL_DATABASE / KC_DB_USERNAME / KC_DB_PASSWORD (and
KC_DB_URL_PORT when port is present) via secretKeyRef. A Secret missing
a required key fails the instance permanently (CredentialsSecretInvalid)
rather than starting Keycloak against a broken DB config — if the Secret
does not exist yet, the instance instead stays Pending and retries.
Managed (managed: true)¶
spec:
postgres:
managed: true
topology: ha # "" (default, 1 replica) or "ha" (3 replicas)
pgbouncer: true # enables the PgBouncer proxy sidecar (2 replicas)
nodes: 0 # 0 = topology default; >0 overrides it
Requires the pgv2.percona.com CRD — see
Installation § Percona PostgreSQL Operator.
The operator applies a PerconaPGCluster named <name>-pg and waits for
Percona to write its generated credentials Secret,
<name>-pg-db-credentials — that Secret is wired in place of a BYO
Secret automatically; you never create or reference a Secret yourself in
this path.
spec.postgres.nodes is managed-only and must be >= 0; setting it with
managed: false is rejected at validation.
Admin bootstrap credentials¶
Keycloak's bootstrap admin username/password always come from a Secret via
secretKeyRef (KC_BOOTSTRAP_ADMIN_USERNAME / KC_BOOTSTRAP_ADMIN_PASSWORD)
— never inline values.
- Set: the operator uses that pre-existing Secret verbatim (keys
username,password); it is not created or garbage-collected by the operator. - Empty (the default): the operator generates
<name>-admin-credentials(usernameadmin, a random 32-character password) once and never rotates it on subsequent reconciles. It is tracked instatus.secretsfor finalizer garbage collection.
Providers: theme + SPI JAR injection¶
Most b'nerd Keycloak instances need custom JARs — a login theme, an SPI
authenticator/event-listener, or both. spec.providers is a first-class
list; each entry becomes its own initContainer that copies *.jar from
path inside the provider image into the shared
/opt/keycloak/providers volume mounted into the Keycloak container:
spec:
providers:
- name: theme # initContainer name: provider-theme
image: registry.bnerd.com/my-org/keycloak-theme:1.4.0
path: /theme # dir inside the image holding *.jar; default "/providers"
- name: audit-spi # initContainer name: provider-audit-spi
image: registry.bnerd.com/my-org/keycloak-audit-spi:2.1.0
# path omitted -> defaults to /providers
imagePullSecrets:
- name: registry-bnerd-com # pod-level; covers the Keycloak image + all providers
Provider names must be unique, non-empty, and DNS-label-safe — they become
the provider-<name> initContainer name.
Provider images must run as a numeric non-root user
The pod runs under runAsNonRoot: true, so the kubelet rejects any
provider (or Keycloak) image whose configured user is root — the pod
never starts. Build provider images with an explicit numeric USER (e.g.
USER 1000:1000); a username the kubelet cannot resolve to a non-zero UID
is also rejected.
Each provider initContainer copies *.jar from path into the shared
providers volume, which it mounts internally at /bnerd-providers (a
distinct path from the in-image path, so the empty volume never shadows
the image's JARs); the Keycloak container sees the result at
/opt/keycloak/providers.
See examples/keycloakinstance-full.yaml for a full theme + SPI example.
Realm Import¶
The named Secret's contents are mounted read-only at
/opt/keycloak/data/import; its presence adds --import-realm to the start
command. Omit realmImport entirely to skip import (the default).
Resources & placement¶
spec:
resources:
requests:
cpu: 500m
memory: 1700Mi
limits:
cpu: 2000m
memory: 2000Mi
placement:
nodeSelector:
dedicated: my-keycloak
tolerations:
- key: dedicated
operator: Equal
value: my-keycloak
effect: NoSchedule
env:
- name: KC_LOG_LEVEL
value: DEBUG
spec.resources defaults to the reference envelope shown above if omitted.
spec.env is a raw escape hatch merged last onto the container env — it can
override any operator-set environment variable by name; use it sparingly.
Optional metrics¶
Creates a Prometheus Operator ServiceMonitor (<name>) scraping the client
Service's management port at /metrics every 30s. Capability-gated on the
monitoring.coreos.com ServiceMonitor CRD: if absent, the operator sets
MetricsExporterReady=False/ServiceMonitorCRDMissing and continues — it
does not fail the instance. Toggling back to false removes the
ServiceMonitor.
Child resource names¶
For a KeycloakInstance named <name>:
| Object | Name |
|---|---|
| StatefulSet | <name> |
Client Service (:8080) |
<name> |
| Headless discovery Service (JGroups DNS_PING) | <name>-discovery |
| Public Ingress | <name> |
Admin Ingress (only when spec.hosts.adminHost is set) |
<name>-admin |
PodDisruptionBudget (only when desired replicas >= 2, not while draining) |
<name> |
ServiceMonitor (only when spec.metrics.serviceMonitor: true + CRD present) |
<name> |
Generated admin bootstrap Secret (only when spec.adminSecret is empty) |
<name>-admin-credentials |
Managed Postgres cluster (only when spec.postgres.managed: true) |
<name>-pg |
| Managed Postgres credentials Secret | <name>-pg-db-credentials |
Managed backend retention¶
The managed PerconaPGCluster is retained on KeycloakInstance
deletion — it carries no owner reference to the KeycloakInstance. Only
operator-owned Secrets (tracked in status.secrets, e.g. the generated
admin credentials) are garbage-collected by the finalizer. This prevents
accidental data loss; remove the PerconaPGCluster manually once you've
verified the data is no longer needed.
HA / production hardening¶
Applied automatically — there is no spec surface to disable these:
- PodDisruptionBudget
<name>(maxUnavailable: 1) when desired replicas>= 2. Removed on scale-to-1 and while a cross-minor upgrade drains, so the drain-to-zero recreate is never blocked. - Soft pod anti-affinity spreads replicas across nodes while still scheduling on a single-node cluster.
- Security context: pod-level
runAsNonRoot+seccompProfile: RuntimeDefault;allowPrivilegeEscalation: false+capabilities.drop: [ALL]on the Keycloak container and every provider initContainer.runAsUseris left unset — the image declares its own non-root user, and pinning a UID breaks provider init containers. - Explicit
RollingUpdateStatefulSet strategy (one-by-one) andterminationGracePeriodSeconds: 60. - Events on every
status.phasetransition (reason: Phase<NewPhase>).
Reconcile phases¶
status.phase moves through Pending → Provisioning → Deploying →
Ready, or Failed on a permanent error (invalid spec, missing Percona CRD,
an invalid BYO credentials Secret). A missing or not-yet-installed
KeycloakVersionMap, or a version not (yet) present in it, is treated as
transient — the instance stays Pending with condition reason
VersionResolutionPending and requeues after 30 seconds rather than failing
permanently.