Upgrade Keycloak 23 → 26 PostgreSQL 16 → 17 via ArgoCD - gpillon/k4all GitHub Wiki

Summary

Component Before After
Helm Chart keycloak-18.7.1 keycloak-25.1.1
Keycloak bitnamilegacy/keycloak:23.0.7-debian-12-r0 bitnamilegacy/keycloak:26.3.2-debian-12-r2
PostgreSQL bitnami/postgresql:16.1.0-debian-11-r25 bitnamilegacy/postgresql:17.5.0-debian-12-r20
Namespace keycloak keycloak
ArgoCD App keycloak (namespace argocd) keycloak (namespace argocd)
Ingress https://<REDACTED>/ https://<REDACTED>/

Prerequisites

  • kubectl configured with the cluster kubeconfig (KUBECONFIG=/path/to/kubeconfig)
  • Admin access to the Kubernetes cluster
  • ArgoCD Application keycloak with manual sync (no auto-sync)

Technical Context

PostgreSQL 16 → 17 is a major version upgrade. The binary data files on disk (in the PVC data-keycloak-postgresql-0) are NOT compatible across major versions. A dump/restore strategy is required to migrate the data.

Keycloak automatically handles database schema migrations via Liquibase. The migration path executed was: 23.0.7 → 24.0.0 → 24.0.3 → 25.0.0 → 26.0.0 → 26.1.0 → 26.2.0 → 26.3.0


Procedure

Phase 1: Database backup

export KUBECONFIG=/path/to/kubeconfig

# pg_dump in custom format (compressed) from the running PostgreSQL 16 pod
kubectl exec -n keycloak keycloak-postgresql-0 -- \
  env PGPASSWORD=<REDACTED> pg_dump -U postgres -d bitnami_keycloak -Fc \
  > keycloak_backup.dump

# Verify the file is not empty (expected ~200KB)
ls -la keycloak_backup.dump

Phase 2: Stop services

Stop Keycloak first (it depends on PostgreSQL), then PostgreSQL.

# Stop Keycloak
kubectl scale statefulset keycloak -n keycloak --replicas=0

# Stop PostgreSQL
kubectl scale statefulset keycloak-postgresql -n keycloak --replicas=0

# Verify all pods are terminated
kubectl get pods -n keycloak
# Expected output: "No resources found in keycloak namespace."

Note: ArgoCD will not overwrite these changes because auto-sync is disabled.

Phase 3: Delete PVC

Delete the PVC containing PostgreSQL 16 binary data, which is incompatible with PostgreSQL 17.

kubectl delete pvc data-keycloak-postgresql-0 -n keycloak

Phase 4: Update the ArgoCD Application

Modify the ArgoCD Application configuration with the new parameters.

Key changes:

  • targetRevision: 18.7.125.1.1
  • proxyHeaders: "xforwarded,forwarded"xforwarded (Keycloak 26 does not accept multiple values)
  • Added global.security.allowInsecureImages: true (required to use bitnamilegacy images)
  • All passwords, ingress config, metrics and affinity settings are preserved
# Apply the patch (argocd-patch.yaml file with the new values)
kubectl patch application keycloak -n argocd --type merge --patch-file argocd-patch.yaml

Contents of argocd-patch.yaml:

spec:
  source:
    targetRevision: "25.1.1"
    helm:
      values: |-
        image:
          registry: docker.io
          repository: bitnamilegacy/keycloak
        global:
          security:
            allowInsecureImages: true
          postgresql:
            auth:
              password: <REDACTED>
              postgresPassword: <REDACTED>
        proxyHeaders: xforwarded
        auth:
          adminPassword: <REDACTED>
          adminUser: <REDACTED>
        postgresql:
          image:
            registry: docker.io
            repository: bitnamilegacy/postgresql
          primary:
            affinity:
              nodeAffinity:
                requiredDuringSchedulingIgnoredDuringExecution:
                  nodeSelectorTerms:
                    - matchExpressions:
                        - key: kubernetes.io/hostname
                          operator: In
                          values:
                            - <REDACTED>
        ingress:
          annotations:
            cert-manager.io/cluster-issuer: letsencrypt-prod
          enabled: true
          hostname: <REDACTED>
          ingressClassName: nginx
          labels: {}
          path: '{{ .Values.httpRelativePath }}'
          pathType: ImplementationSpecific
          servicePort: http
          tls: true
        metrics:
          enabled: true
          prometheusRule:
            enabled: true
            namespace: monitor
          serviceMonitor:
            enabled: true
            namespace: monitor
        production: true
        proxy: edge
        affinity:
          nodeAffinity:
            requiredDuringSchedulingIgnoredDuringExecution:
              nodeSelectorTerms:
                - matchExpressions:
                    - key: kubernetes.io/hostname
                      operator: In
                      values:
                        - <REDACTED>

Phase 5: ArgoCD sync and database restore

5a. Delete the old Keycloak StatefulSet

The updated chart modifies immutable fields in the StatefulSet. It must be deleted so ArgoCD can recreate it.

kubectl delete statefulset keycloak -n keycloak --cascade=orphan

5b. First ArgoCD sync (with Keycloak at 0 replicas)

For this first sync, temporarily set replicaCount: 0 in the ArgoCD Application values so that only PostgreSQL 17 is created.

# Add "replicaCount: 0" to the values and apply the patch, then:
kubectl patch application keycloak -n argocd --type merge \
  -p '{"operation":{"initiatedBy":{"username":"admin"},"sync":{"syncStrategy":{"hook":{}},"syncOptions":["RespectIgnoreDifferences=true"]}}}'

Wait for PostgreSQL to be ready:

kubectl get pods -n keycloak
# Expected: keycloak-postgresql-0   1/1     Running

5c. Restore the database

# Copy the dump into the pod (Windows workaround: use pipe with cat)
cat keycloak_backup.dump | kubectl exec -i -n keycloak keycloak-postgresql-0 -- \
  sh -c 'cat > /tmp/keycloak_backup.dump'

# Verify the file was copied correctly
MSYS_NO_PATHCONV=1 kubectl exec -n keycloak keycloak-postgresql-0 -- \
  ls -la /tmp/keycloak_backup.dump

# Run the restore
MSYS_NO_PATHCONV=1 kubectl exec -n keycloak keycloak-postgresql-0 -- \
  env PGPASSWORD=<REDACTED> pg_restore -U postgres -d bitnami_keycloak \
  --clean --if-exists /tmp/keycloak_backup.dump

# Verify the restored tables (92 tables expected)
MSYS_NO_PATHCONV=1 kubectl exec -n keycloak keycloak-postgresql-0 -- \
  env PGPASSWORD=<REDACTED> psql -U postgres -d bitnami_keycloak -c "\dt"

# Verify the realms
MSYS_NO_PATHCONV=1 kubectl exec -n keycloak keycloak-postgresql-0 -- \
  env PGPASSWORD=<REDACTED> psql -U postgres -d bitnami_keycloak \
  -c "SELECT id, name FROM realm;"

Windows (Git Bash) note: use MSYS_NO_PATHCONV=1 before kubectl commands that contain Linux paths (e.g. /tmp/...) to prevent automatic path conversion by MSYS.

Phase 6: Full sync and Keycloak startup

Remove replicaCount: 0 from the values and perform the final sync.

# Apply the patch without replicaCount: 0
kubectl patch application keycloak -n argocd --type merge --patch-file argocd-patch.yaml

# Trigger the final sync
kubectl patch application keycloak -n argocd --type merge \
  -p '{"operation":{"initiatedBy":{"username":"admin"},"sync":{"syncStrategy":{"hook":{}},"syncOptions":["RespectIgnoreDifferences=true"]}}}'

Monitor the Keycloak logs to confirm the Liquibase migration:

kubectl logs -f keycloak-0 -n keycloak

Expected output during migration:

migrated realm master to 24.0.0
migrated realm master to 24.0.3
migrated realm master to 25.0.0
migrated realm master to 26.0.0
migrated realm master to 26.1.0
migrated realm master to 26.2.0
migrated realm master to 26.3.0
Keycloak 26.3.2 on JVM (powered by Quarkus 3.20.2) started in ~38s

Verify the final state:

# Pods
kubectl get pods -n keycloak
# Expected: keycloak-0 1/1 Running, keycloak-postgresql-0 1/1 Running

# ArgoCD
kubectl get application keycloak -n argocd -o jsonpath='{.status.sync.status}'
# Expected: Synced

kubectl get application keycloak -n argocd -o jsonpath='{.status.health.status}'
# Expected: Healthy

# Images
kubectl get statefulset keycloak -n keycloak -o jsonpath='{.spec.template.spec.containers[0].image}'
# Expected: docker.io/bitnamilegacy/keycloak:26.3.2-debian-12-r2

kubectl get statefulset keycloak-postgresql -n keycloak -o jsonpath='{.spec.template.spec.containers[0].image}'
# Expected: docker.io/bitnamilegacy/postgresql:17.5.0-debian-12-r20

Issues Encountered and Solutions

1. Image not found for bitnami/keycloak and bitnami/postgresql

Problem: The images docker.io/bitnami/keycloak:26.x and docker.io/bitnami/postgresql:17.6.0-* do not exist in the Docker Hub registry.

Solution: Use bitnamilegacy/keycloak for Keycloak and bitnamilegacy/postgresql for PostgreSQL. Add global.security.allowInsecureImages: true in the Helm values to bypass the Bitnami chart's security check on non-standard images.

2. proxyHeaders does not accept multiple values

Problem: Keycloak 26 does not accept proxyHeaders: "xforwarded,forwarded".

Solution: Use a single value: proxyHeaders: xforwarded (correct for nginx which uses the X-Forwarded-* headers).

3. StatefulSet with immutable fields

Problem: kubectl apply fails with "updates to statefulset spec for fields other than 'replicas'... are forbidden".

Solution: Delete the old StatefulSet with --cascade=orphan and let ArgoCD recreate it from the updated chart.

4. kubectl cp does not work on Windows/Git Bash

Problem: Paths are incorrectly translated by MSYS.

Solution: Use cat file | kubectl exec -i ... -- sh -c 'cat > /path' to copy files into pods, and MSYS_NO_PATHCONV=1 for exec commands with Linux paths.


Rollback

In case a rollback is needed:

  1. Scale down Keycloak and PostgreSQL to 0 replicas
  2. Delete the PVC data-keycloak-postgresql-0
  3. Restore the ArgoCD Application to the previous version:
    • targetRevision: 18.7.1
    • Restore image.repository: bitnamilegacy/keycloak (without allowInsecureImages)
    • Restore proxyHeaders: "xforwarded,forwarded"
  4. Sync ArgoCD (PostgreSQL 16 will be recreated empty)
  5. Restore the backup keycloak_backup.dump into the new PG16 pod
  6. Sync/scale up Keycloak

IMPORTANT: Keep the keycloak_backup.dump file until Keycloak 26 is confirmed to be working correctly in production.

⚠️ **GitHub.com Fallback** ⚠️