Skip to content

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:

  1. spec.image (escape hatch) — if set, this exact image is used directly, bypassing KeycloakVersionMap resolution entirely. status.observedVersion becomes the tag portion of this value.
  2. KeycloakVersionMap/default — the operator always looks up the cluster-scoped KeycloakVersionMap named default.
  3. Alias resolution — a value like "latest", "stable", or "26" resolves through spec.aliases to a key present in spec.versions. This is a single alias hop followed by an exact-key match — there is no prefix/fuzzy matching (unlike bnerd-gitlab-operator's resolver).
  4. 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.326.6.4) Plain one-by-one rolling update on the StatefulSet Stays available throughout
Cross-minor or major (e.g. 26.6.426.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:

kubectl get kci my-keycloak -n my-keycloak \
  -o jsonpath='{.status.observedVersion}{"\n"}'

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.

Customising the version map

To qualify a new Keycloak version:

kubectl edit keycloakversionmap default

Add the entry under spec.versions:

spec:
  versions:
    "26.7.1":
      image: quay.io/keycloak/keycloak:26.7.1

Update the relevant alias if appropriate:

spec:
  aliases:
    stable: "26.7.1"
    "26.7": "26.7.1"

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.