Skip to content

CRD Reference — bnerd-keycloak-operator

API group: k8s.bnerd.com/v1alpha1


KeycloakInstance

Namespaced | Short name: kci

One CR = one Keycloak deployment (StatefulSet + Services + optional Ingresses).

Spec fields

Field Type Required Description
spec.version string no Keycloak version to deploy ("26.6.4", "stable", "latest", "26"). Resolved via KeycloakVersionMap/default. Defaults to the latest alias when empty. Ignored when spec.image is set.
spec.replicas int32 no StatefulSet replica count. Defaults to 2 (the reference default); must be >= 1 when set.
spec.hosts.host string yes Public account/auth hostname. Drives KC_HOSTNAME=https://<host> and the public Ingress rule. Structurally required even when spec.ingress.enabled: false.
spec.hosts.adminHost string no Admin console hostname. Empty = no admin Ingress and KC_HOSTNAME_ADMIN is left unset.
spec.ingress.enabled bool no Defaults to true. false = render Services only (dev/kind); no Ingress objects are created.
spec.ingress.class string no Public Ingress class. Defaults to "nginx".
spec.ingress.adminClass string no Admin Ingress class. Defaults to "nginx-internal".
spec.ingress.clusterIssuer string no cert-manager.io/cluster-issuer annotation value. Empty = no TLS annotation/secret on either Ingress (plain HTTP, dev/kind).
spec.ingress.annotations map[string]string no Extra annotations merged onto both Ingresses; the operator's own security-snippet and (if set) cluster-issuer annotations always take precedence.
spec.postgres.managed bool no true = operator applies a PerconaPGCluster named <name>-pg (capability-gated on the pgv2.percona.com CRD). false = BYO via credentialsSecret.
spec.postgres.topology "" | ha | standalone no Managed-only. ha provisions 3 Postgres instance replicas; anything else (including empty) provisions 1. Rejected at validation if set to any other value.
spec.postgres.pgbouncer bool no Managed-only. Enables the PgBouncer proxy sidecar (2 replicas) in the PerconaPGCluster; when false the proxy block is still emitted (Percona 2.8.2 requires it) with 0 replicas.
spec.postgres.nodes int no Managed-only. Overrides the Postgres instance replica count derived from topology: 0 (default) uses the topology default (ha → 3, standalone → 1); any value > 0 wins over it. Must be >= 0; setting it with managed: false is rejected at validation.
spec.postgres.credentialsSecret string required when managed: false BYO Secret name. Keys: host, dbname, user, password (+ optional port). A Secret missing a required key permanently fails the instance (CredentialsSecretInvalid).
spec.adminSecret string no Name of a pre-existing bootstrap admin Secret (keys username, password). Empty (default) = the operator generates <name>-admin-credentials once (random 32-char password, never rotated) and tracks it in status.secrets for finalizer GC.
spec.providers[].name string yes, per entry Unique, DNS-label-safe suffix; the initContainer is named provider-<name>.
spec.providers[].image string yes, per entry Image carrying the JARs to inject (theme, SPI provider, ...). Must run as a numeric non-root user (e.g. USER 1000:1000) — the pod's runAsNonRoot: true policy makes the kubelet reject root (or unresolvable-username) provider images, and the pod never starts. Its initContainer copies path/*.jar into the shared providers volume mounted internally at /bnerd-providers (distinct from path so the empty volume never shadows the image's JARs); Keycloak reads them at /opt/keycloak/providers.
spec.providers[].path string no Directory inside the image holding *.jar files. Defaults to /providers (the .ref theme image instead uses /theme — set explicitly for images that don't use the default layout).
spec.providers[].pullPolicy corev1.PullPolicy no initContainer image pull policy. Defaults to IfNotPresent.
spec.imagePullSecrets []LocalObjectReference no Pod-level pull secrets; cover the Keycloak image and every provider image.
spec.realmImport.secretName string no Secret containing the realm JSON to import. Mounted read-only at /opt/keycloak/data/import; presence adds --import-realm to the start command.
spec.image string no Full image override (registry/repo:tag); bypasses KeycloakVersionMap resolution entirely. status.observedVersion becomes the tag portion of this value.
spec.resources corev1.ResourceRequirements no Compute resources for the Keycloak container. Defaults to the reference envelope: 500m/1700Mi request, 2000m/2000Mi limit.
spec.placement.nodeSelector map[string]string no Node selector for the Keycloak pod.
spec.placement.tolerations []corev1.Toleration no Tolerations for tainted nodes.
spec.env []corev1.EnvVar no Raw extra environment variables, merged last onto the container env (escape hatch — can override any operator-set variable by name).
spec.metrics.serviceMonitor bool no true = create 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.

Status fields

Field Type Description
status.phase string Lifecycle phase: Pending | Provisioning | Deploying | Ready | Failed.
status.host string Set to spec.hosts.host once the instance reaches Ready.
status.observedVersion string Keycloak version (or image tag, when spec.image is set) last observed as resolved/running.
status.observedGeneration int64 The metadata.generation this status reflects — lets a consumer (e.g. kubectl wait, an hq status sync) distinguish a status that reflects the current spec from a stale one.
status.conditions []metav1.Condition Validated, BackendsReady, Progressing, Ready, UpgradeInProgress, MetricsExporterReady. See reason vocabulary below.
status.secrets []string Names of operator-owned Secrets tracked for finalizer GC (currently: the generated admin-credentials Secret, when the operator created it).

Condition reason vocabulary

Condition Reason Meaning
Validated SpecValid Spec passed ValidateSpec.
Validated ValidationFailed Permanent — bad spec (missing BYO secret name, invalid topology, missing host with ingress enabled, invalid replica count, negative or BYO-only postgres.nodes, invalid provider name).
Ready VersionResolutionPending Transient — KeycloakVersionMap/default absent, or the requested version/alias not found in it. Requeues after 30s; phase stays Pending.
BackendsReady WaitingForCredentials Transient — BYO credentialsSecret not found yet. Requeues after 30s.
BackendsReady CredentialsSecretInvalid BYO Secret found but missing a required key. Phase Failed, but bounded-retried (~60s requeue) because the fix — correcting the Secret — is user-side and neither owned nor watched by the operator; a corrected Secret recovers without editing the CR.
BackendsReady PerconaCRDMissing Permanent — spec.postgres.managed: true but pgv2.percona.com/PerconaPGCluster is not installed. Never silently falls back.
BackendsReady PostgresProvisioning Transient — managed PerconaPGCluster applied, waiting for its <name>-pg-db-credentials Secret.
BackendsReady PostgresReady Database credentials are available.
Ready / Progressing WorkloadNotReady / Deploying StatefulSet applied, waiting for it to become ready and for the rollout to converge (updatedReplicas >= spec.replicas, updateRevision == currentRevision, observedGeneration caught up) so no old-revision pods still serve.
Ready WorkloadReady StatefulSet is ready and the rollout has fully converged onto the current revision; phase is Ready, Progressing is cleared, observedVersion is the running version.
MetricsExporterReady ServiceMonitorApplied spec.metrics.serviceMonitor: true and the ServiceMonitor was applied.
MetricsExporterReady ServiceMonitorCRDMissing Warning (not Failed) — spec.metrics.serviceMonitor: true but the monitoring.coreos.com ServiceMonitor CRD is absent; deployment continues without metrics scraping.

Phase, Host, Version, Age

HA / production hardening

These are applied automatically — there is no spec surface to disable them:

  • PodDisruptionBudget — an owned PDB named <name> (maxUnavailable: 1) is created when the desired replica count is >= 2. It is removed when the instance is scaled to a single replica and while a cross-minor upgrade is draining (so the drain-to-zero recreate is never blocked). A single-replica instance never gets one.
  • Pod anti-affinity — soft (preferredDuringScheduling, weight 100, kubernetes.io/hostname) so replicas spread across nodes where possible, while a single-node dev cluster still schedules them.
  • Security context — pod-level runAsNonRoot: true + seccompProfile: RuntimeDefault; the Keycloak container and every provider initContainer get allowPrivilegeEscalation: false and capabilities.drop: [ALL]. runAsUser is deliberately left unset — the Keycloak/provider images declare their own non-root user, and pinning a UID breaks provider init containers. Consequence for provider images: because runAsUser is unset, every provider image must itself declare a numeric non-root USER (e.g. USER 1000:1000); the kubelet rejects a root or unresolvable-username image under runAsNonRoot, and the pod never starts.
  • Update strategy — the StatefulSet uses an explicit RollingUpdate strategy (one-by-one), protecting Infinispan in-memory session state.
  • Events — a Kubernetes Event (reason: Phase<NewPhase>) is emitted each time status.phase changes, alongside the upgrade-orchestration Events (UpgradeRecreateStarted, UpgradeCompleted, DowngradeBlocked, ...).

KeycloakVersionMap

Cluster-scoped | Short name: kcvm

Maps Keycloak version strings/aliases to container images. The operator always looks up the KeycloakVersionMap named default.

Spec fields

Field Type Required Description
spec.versions map[string]VersionEntry yes Maps a Keycloak version string (e.g. "26.6.4") to a VersionEntry.
spec.versions[*].image string yes Full container image reference (including tag) for this version.
spec.aliases map[string]string no Maps symbolic names (e.g. "latest", "stable", "26") to a key present in spec.versions. Resolution is a single alias hop followed by an exact-key match — there is no prefix/fuzzy matching (unlike bnerd-gitlab-operator's resolver). An alias pointing at a version not present in spec.versions is an error, listing the known versions.

Latest (from .spec.aliases.latest), Age