Upgrades & Version Management¶
The operator uses a KeycloakVersionMap CRD to map Keycloak version strings
and aliases to container images. Understanding version resolution and how
the operator orchestrates same-minor vs. cross-minor changes is essential
before performing an upgrade.
How version resolution works¶
When the operator reconciles a KeycloakInstance, it resolves spec.version
through this priority chain:
spec.image(escape hatch) — if set, this exact image is used directly, bypassingKeycloakVersionMapresolution entirely.status.observedVersionbecomes the tag portion of this value.KeycloakVersionMap/default— the operator always looks up the cluster-scopedKeycloakVersionMapnameddefault.- Alias resolution — a value like
"latest","stable", or"26"resolves throughspec.aliasesto a key present inspec.versions. This is a single alias hop followed by an exact-key match — there is no prefix/fuzzy matching (unlikebnerd-gitlab-operator's resolver). - Exact match —
"26.6.4"matches the entry directly.
If the version map is not present, or the requested version/alias is not
(yet) found in it, resolution is transient: the instance stays Pending
with condition reason VersionResolutionPending and requeues after 30
seconds — the map may simply not be applied yet, or may be updated later to
add the version.
The default KeycloakVersionMap¶
examples/versionmap-default.yaml ships the qualified 26.x ladder:
apiVersion: k8s.bnerd.com/v1alpha1
kind: KeycloakVersionMap
metadata:
name: default
spec:
versions:
"26.5.7":
image: quay.io/keycloak/keycloak:26.5.7
"26.6.4":
image: quay.io/keycloak/keycloak:26.6.4
"26.7.0":
image: quay.io/keycloak/keycloak:26.7.0
aliases:
latest: "26.7.0"
stable: "26.6.4"
"26": "26.6.4"
"26.5": "26.5.7"
"26.6": "26.6.4"
"26.7": "26.7.0"
The Helm chart installs this same content automatically
(versionMap.installDefault: true, the default) — see
Installation § Applying the default KeycloakVersionMap.
Same-minor vs. cross-minor: how an upgrade is applied¶
The operator inspects the running vs. target Keycloak version and picks one of two upgrade strategies:
| Change | Strategy | Availability |
|---|---|---|
Same minor (e.g. 26.6.3 → 26.6.4) |
Plain one-by-one rolling update on the StatefulSet | Stays available throughout |
Cross-minor or major (e.g. 26.6.4 → 26.7.0) |
Drain-first recreate: scale to zero, then bring the new version up | Full outage for the duration |
A cross-minor change never runs mixed versions — the operator always scales
the old version to zero before starting the new one, reporting the
transition via the UpgradeInProgress condition and a UpgradeCompleted
Event once done.
A cross-minor upgrade is a full outage, with no automatic rollback
Because a cross-minor change drains to zero before the new version
starts, the instance is unavailable for the duration. If the new version
fails to come up, the instance stays down — there is no automatic
rollback. Recovery is to fix the target version (correct spec.version,
or set the break-glass annotation below and point back at the previous
version) and let the operator recreate.
Ready and status.observedVersion only advance once the StatefulSet
rollout has fully converged onto the new revision (updatedReplicas >=
spec.replicas, updateRevision == currentRevision, observedGeneration
caught up) — so readiness is never reported while old-revision pods still
serve.
Performing an upgrade¶
Upgrades are declarative: update spec.version on the KeycloakInstance.
kubectl get kci my-keycloak -n my-keycloak
# NAME PHASE HOST VERSION AGE
# my-keycloak Ready auth.example.com 26.6.4 30d
kubectl patch kci my-keycloak -n my-keycloak \
--type merge \
-p '{"spec":{"version":"26.7.0"}}'
kubectl get kci my-keycloak -n my-keycloak -w
# NAME PHASE HOST VERSION AGE
# my-keycloak UpgradeInProgress auth.example.com 30d
# my-keycloak Ready auth.example.com 26.7.0 30d
Check status.observedVersion matches spec.version once done:
No downgrades (with a break-glass override)¶
A downgrade is refused by default: the instance is set to phase:
Failed, condition reason VersionDowngradeBlocked, and the running
workload is left untouched (not scaled down, not recreated).
To force a downgrade in an emergency, set the break-glass annotation:
kubectl annotate kci my-keycloak -n my-keycloak \
k8s.bnerd.com/allow-downgrade="true" --overwrite
kubectl patch kci my-keycloak -n my-keycloak \
--type merge \
-p '{"spec":{"version":"26.6.4"}}'
An allowed cross-minor downgrade is itself performed as a drain-first recreate (same outage profile as a forward cross-minor upgrade) — never a live mixed-version swap.
Upgrading the operator itself¶
This section is about upgrading the operator (e.g. helm upgrade from
0.1.x to 0.2.0) — a different thing from the Keycloak version upgrades above,
which the operator performs for you on a running instance.
0.2.0 pre-upgrade check: default-on NetworkPolicy
0.2.0 renders an owned NetworkPolicy on every instance by default
(spec.networkPolicy.enabled defaults to true — see
Network isolation). On a cluster
whose CNI enforces NetworkPolicy (Cilium, Calico, ...), upgrading the
operator immediately starts isolating every already-running instance to a
rule set it never had before. Answer these two questions before
upgrading, for every existing instance:
- Does this instance have
spec.hosts.adminHostset, and does its admin ingress controller run in a namespace other thaningress-nginx-internal(the defaultadminIngressControllerSelectormatch wheneveringress.adminClassdiffers from the public class; since v0.3.2 an admin Ingress on the public class reuses the public rule instead)? If yes, setspec.networkPolicy.adminIngressControllerSelectorto the correct namespace before upgrading, or the admin console silently stops being reachable. - Does any realm on this instance make its own outbound network
calls — the general case, not a fixed list. The two most common
instances: external Identity Provider federation (SAML/OIDC to
Google, Microsoft, a corporate IdP — outbound HTTPS during login) and
an SMTP relay for realm email (see
SMTP / outbound email —
outbound to whatever host/port the realm's
smtpServerconfig names). Neither, nor anything else a realm is configured to call out to, is covered by any default egress rule (the:443rule in the isolation table is only for aurl:provider JAR fetch, an unrelated one-time init-container concern). If any realm on the instance does this, setspec.networkPolicy.egressWhitelistto that destination's CIDR(s) before upgrading:Skipping this means that traffic breaks with no error surfaced by the operator — it's just silently dropped by the CNI.spec: networkPolicy: egressWhitelist: - name: corp-idp # documentation only, not rendered cidr: 203.0.113.5/32 # port: 443 is the default; set it explicitly for SMTP (587/465/25)egressWhitelisttargets a CIDR only (vanilla NetworkPolicy cannot express hostname/FQDN rules) — resolve the target's IP range up front if you only have a hostname. DNS resolution itself needs no pre-upgrade action. An earlier draft of this fix required settingspec.networkPolicy.dnsEgressCIDRto the cluster's DNS Service ClusterIP; real-cluster testing found that ineffective (a NetworkPolicy egress rule is evaluated against the packet's post-DNAT destination, not the ClusterIP a client dials, so naming that address doesn't help). The DNS egress rule's default peer — a namespaceSelector+podSelector match on CoreDNS's own pods, opening both port 53 and CoreDNS's real container port (commonly8053) — is verified working reliably on a real Calico cluster and needs no per-instance configuration; see Network isolation for the mechanism and what is and isn't proven about why it works.dnsEgressCIDRremains available as an escape hatch for a non-DNAT'd DNS endpoint the default peer cannot express, but it is not something you need to set for an ordinary upgrade.
Neither question is visible on a kind cluster (kindnet does not
enforce NetworkPolicy at all) — a "the e2e suite passed" upgrade check is
not sufficient evidence here. If you're not sure whether your cluster's
CNI enforces NetworkPolicy, treat both questions as "yes" until you've
checked. See also Known Limitations.
An instance that answers "no" to both needs no action — the default rule set already covers it.
Customising the version map¶
To qualify a new Keycloak version:
Add the entry under spec.versions:
Update the relevant alias if appropriate:
An alias pointing at a version not present in spec.versions is rejected at
apply time (the error message lists the known versions). Editing the map
does not automatically trigger a reconcile on running instances —
version resolution only re-runs when the KeycloakInstance spec changes.