← Übersicht  ·  Skripte & Dateien  ·  Lokal · OpenShift Local (CRC) auf dem Laptop

Bibliothek-Enterprise auf CRC deployen

Derselbe Stack, den die Jenkins-Pipeline auf den Hetzner-OKD ausrollt – 8 Spring-Services (mit Config-Server), Postgres, Redis, Kafka, Keycloak – läuft auch lokal in der CRC-VM. Gleiche Helm-Charts, gleiche Bitnami-Infrastruktur, nur ein anderes Umgebungs-Overlay. Der Unterschied, der alles bestimmt: sechs vCPUs statt eines 32-GB-Nodes. Diese Seite dokumentiert den Weg und die vier Fallen, die dabei jeweils Stunden gekostet haben.

Stand: 2. September 2026 Ende-zu-Ende getestet (13 Pods, 3 Routen, HTTP 200) CRC 2.63 · OKD 4.22 · Windows 10 + Hyper-V

Platzhalter einsetzen – werden in allen Befehlen unten ersetzt, nur im Browser, nichts wird gesendet

Was gegenüber OKD anders ist

Drei Dinge – und eine Konsequenz, die man nicht umgehen kann.

Hetzner-OKDCRC lokal
Registryclusterintern, über externe Routeclusterintern, über default-route-openshift-image-registry.apps-crc.testing
Overlayenv/okd-values.yamlenv/crc-values.yaml
Domäneapps.sno.<ip>.nip.ioapps-crc.testing
Node32 GB, dedizierter Server6 vCPU / 16 GB in einer Hyper-V-VM
Die Konsequenz

Auf sechs vCPUs können die acht Spring-Boot-Dienste nicht gleichzeitig kalt starten. Der Deploy legt sie deshalb mit replicas=0 an, ein zweites Skript fährt sie einzeln hoch. Warum das keine Vorsichtsmaßnahme, sondern zwingend ist, steht in Schritt 06.

01 CRC dimensionieren

Die Vorgabewerte reichen für diesen Stack nicht. Alle drei Änderungen greifen erst beim Neustart.

EinstellungVorgabeNötigGrund
memory1075216384Bei 10,5 GB sind bereits 93 % des Node-Speichers von OpenShift selbst angefordert – es bleiben rund 700 MiB für Anwendungen
disk-size3145Jedes Image liegt doppelt: einmal in der Registry-Storage, einmal in CRI-O nach dem Pull
cpus46Weniger macht die JVM-Kaltstarts unzumutbar langsam
Laptop · PowerShell oder bash
crc config set memory 16384
crc config set disk-size 45
crc config set cpus 6
crc stop; crc start   # disk-size waechst beim Start, die Partition wird mitvergroessert
Falle 0: crc start bricht mit unable to allocate … MB of RAM ab

Hyper-V reserviert den konfigurierten memory-Wert als Startup-RAM exklusiv für die CRC-VM. Ist auf dem Host nicht genug frei – typischerweise weil eine andere Hyper-V-VM parallel läuft (z. B. eine minikube-VM) – schlägt der Start mit Insufficient system resources exist to complete the requested service fehl. Prüfen mit Get-VM | Select Name,State,MemoryStartup; nicht benötigte VMs vorher mit Stop-VM <name> stoppen, oder memory in der Tabelle oben reduzieren, bis es in den freien Speicher passt.

Wo die Dateien liegen sollten

~/.crc/machines/ enthält die VM-Disk und gehört auf eine SSD. Liegt sie auf einer klassischen Festplatte, ist der Start vollständig I/O-gebunden – gemessen: iowait 62–74 %, crictl images braucht 22 Sekunden, und daraus entstehen reihenweise ImageInspectError: DeadlineExceeded. Der Bundle-Cache unter ~/.crc/cache/ darf dagegen auf der langsamen Platte liegen – er wird nur beim Start gelesen.

02 Jars und Images bauen

Den Maven-Reactor einmal auf dem Host bauen statt neunmal im Container.

Laptop · Git Bash · Projektwurzel
# 1. Alle Module bauen - erzeugt die *-exec.jar je Service
cd library-platform; mvn -B package -DskipTests; cd ..

# 2. Images aus den fertigen Jars - Dockerfile.prebuilt-jar hat KEINE Maven-Stage
for s in api-gateway catalog-service config-server discovery-server fine-service \
         lending-service member-service notification-service reservation-service; do
  podman build -f infra/Dockerfile.prebuilt-jar --build-arg MODULE=$s --build-arg PORT=8080 \
    -t library/$s:crc library-platform
done
podman build -f library-frontend/Dockerfile -t library/library-frontend:crc library-frontend
Laptop · PowerShell · Äquivalent
# 1. Alle Module bauen - erzeugt die *-exec.jar je Service
cd library-platform; mvn -B package -DskipTests; cd ..

# 2. Images aus den fertigen Jars - Dockerfile.prebuilt-jar hat KEINE Maven-Stage
$services = "api-gateway","catalog-service","config-server","discovery-server","fine-service",
  "lending-service","member-service","notification-service","reservation-service"
foreach ($s in $services) {
  podman build -f infra/Dockerfile.prebuilt-jar --build-arg MODULE=$s --build-arg PORT=8080 `
    -t library/${s}:crc library-platform
}
podman build -f library-frontend/Dockerfile -t library/library-frontend:crc library-frontend
Falle 0b: Cannot connect to Podman nach längerer Standzeit

Setzt podman machine start voraus – ohne läuft jeder Befehl oben gegen einen toten Socket. Reicht ein einfacher Start nicht und bleibt der Fehler ssh: rejected: connect failed (open failed) bzw. podman info hängt, liegt es am systemd-User-Manager innerhalb der WSL-Maschine: podman machine ssh podman-machine-default sudo systemctl status user@1000.service zeigt dann Failed to spawn executor: Device or resource busy. Ein bloßes podman machine stop/start behebt das nicht – nur ein vollständiger wsl --shutdown (beendet alle WSL-Distros, nicht nur diese) gefolgt von podman machine start räumt den hängenden Zustand weg.

Warum Dockerfile.prebuilt-jar

Das reguläre library-platform/Dockerfile baut Maven im Container – hermetisch und reproduzierbar, aber bei neun Images baut es den Reactor neunmal. Auf einem Laptop ist das der Unterschied zwischen Minuten und einer Stunde. Für CI bleibt das reguläre Dockerfile die richtige Wahl.

discovery-server wird gebaut, aber nicht deployt – in Kubernetes übernimmt die Cluster-DNS die Rolle von Eureka. Das ist auf OKD genauso.

03 In die CRC-Registry pushen

Erst die Route der clusterinternen Registry aktivieren, dann anmelden und schieben.

Laptop · Git Bash
oc patch configs.imageregistry.operator.openshift.io/cluster \
  --patch '{"spec":{"defaultRoute":true}}' --type=merge
oc new-project library

R=default-route-openshift-image-registry.apps-crc.testing
oc login -u kubeadmin -p "$(cat ~/.crc/machines/crc/kubeadmin-password)" \
  https://api.crc.testing:6443 --insecure-skip-tls-verify=true
podman login --tls-verify=false -u kubeadmin -p "$(oc whoami -t)" $R

for s in api-gateway catalog-service config-server fine-service lending-service \
         member-service notification-service reservation-service library-frontend; do
  podman tag  library/$s:crc $R/library/$s:crc
  podman push --tls-verify=false $R/library/$s:crc
done
Bash-Syntax, nicht Bash-exklusive Tools

oc, helm und podman sind auch unter Windows native Binaries und liefen genauso in PowerShell – das Label oben bezieht sich nicht auf die Tools, sondern auf die Shell-Syntax im Block: die Variablenzuweisung R=…, die Command-Substitution $(cat …) und die Schleife for … do … done sind POSIX-Bash und scheitern 1:1 in PowerShell. Wer lieber durchgehend PowerShell nutzt, findet das Äquivalent direkt darunter.

Eigene Falle im Äquivalent: Windows PowerShell reicht ein einfach-gequotetes '{"spec":…}' an eine native .exe wie oc weiter, indem es dabei die inneren " entfernt – oc bekommt dann {spec:{defaultRoute:true}} ohne Anführungszeichen und meldet invalid character 's' looking for beginning of object key string. Abhilfe: die inneren " mit \ escapen, wie im Block unten.

Laptop · PowerShell · Äquivalent
oc patch configs.imageregistry.operator.openshift.io/cluster `
  --patch '{\"spec\":{\"defaultRoute\":true}}' --type=merge
oc new-project library

$R = "default-route-openshift-image-registry.apps-crc.testing"
oc login -u kubeadmin -p (Get-Content "$HOME\.crc\machines\crc\kubeadmin-password") `
  https://api.crc.testing:6443 --insecure-skip-tls-verify=true
podman login --tls-verify=false -u kubeadmin -p (oc whoami -t) $R

$services = "api-gateway","catalog-service","config-server","fine-service",
  "lending-service","member-service","notification-service",
  "reservation-service","library-frontend"
foreach ($s in $services) {
  podman tag  library/${s}:crc $R/library/${s}:crc
  podman push --tls-verify=false $R/library/${s}:crc
}
Falle 1: Podman in WSL erreicht 127.0.0.1 des Hosts nicht

Unter Windows läuft Podman in einer WSL2-Maschine. CRC veröffentlicht seine Routen auf 127.0.0.1 des Windows-Hosts – das ist aus der WSL-VM heraus eine andere Maschine, denn WSL nutzt hier NAT und kein Mirrored-Networking. Ein /etc/hosts-Eintrag auf 127.0.0.1 zeigt dort ins Leere.

CRC bindet Port 443 aber auf allen Interfaces, nur Port 6443 bleibt auf Loopback. Der Host ist also über das WSL-Gateway erreichbar – die Gateway-Adresse steht in der Default-Route der WSL-Maschine:

wsl -d podman-machine-default -- ip route     # -> default via 172.25.224.1 dev eth0
wsl -d podman-machine-default -- sudo sh -c \
  'echo "172.25.224.1 default-route-openshift-image-registry.apps-crc.testing" >> /etc/hosts'

Steht dort noch ein alter Eintrag mit 127.0.0.1, muss er entfernt werden – bei mehreren Treffern gewinnt der erste. Die Gateway-Adresse kann sich nach einem WSL-Neustart ändern.

Der Eintrag ist nicht dauerhaft: Die Kopfzeile in /etc/hosts selbst verrät es – „automatically generated by WSL“. Nach jedem Neustart der podman-machine-default-Maschine (auch einem einfachen podman machine stop/podman machine start, erst recht nach einem wsl --shutdown) ist die Datei wieder auf den Auto-generierten Stand zurückgesetzt und der Eintrag fehlt – erkennbar am selben Fehlerbild wie oben, nur diesmal auf Port 80 statt 443 (dial tcp 127.0.0.1:80: connect: connection refused beim podman push), weil ohne gültigen Host-Eintrag ein NSS-Modul (resolve) still eine falsche Adresse liefert, bevor der Push auf Klartext-HTTP zurückfällt. Abhilfe von überall aus – PowerShell, Git Bash, egal – da podman.exe das SSH selbst übernimmt:

podman machine ssh podman-machine-default 'echo "172.25.224.1 default-route-openshift-image-registry.apps-crc.testing" | sudo tee -a /etc/hosts'

Wer dauerhaft nicht neu eintragen will, kann in der Maschine /etc/wsl.conf um [network] / generateHosts = false ergänzen – dann bleibt der einmal gesetzte Eintrag über Neustarts hinweg erhalten, muss dann aber bei einer neuen Gateway-Adresse manuell nachgezogen werden.

04 Bitnami-Infra + Keycloak

Weitgehend identisch zur OKD-Anleitung – dieselben Charts, dieselben zwei Fallen (restricted-v2 und die Bitnami-Paywall). Nur Keycloak braucht andere Probe-Werte.

Laptop · Git Bash (→ CRC)
helm repo add bitnami https://charts.bitnami.com/bitnami; helm repo update
NS=library; COMPAT='--set global.compatibility.openshift.adaptSecurityContext=auto'

helm upgrade --install postgres bitnami/postgresql -n $NS $COMPAT \
  --set fullnameOverride=postgres \
  --set auth.username=library --set auth.password=library --set auth.database=library \
  --set auth.postgresPassword=library --set primary.persistence.enabled=false --wait

helm upgrade --install redis bitnami/redis -n $NS $COMPAT \
  --set fullnameOverride=redis --set auth.enabled=false --set architecture=standalone \
  --set master.persistence.enabled=false --wait

helm upgrade --install kafka bitnami/kafka -n $NS $COMPAT \
  --set global.security.allowInsecureImages=true \
  --set image.registry=docker.io --set image.repository=bitnamilegacy/kafka \
  --set fullnameOverride=kafka --set controller.replicaCount=1 \
  --set listeners.client.protocol=PLAINTEXT --set listeners.controller.protocol=PLAINTEXT \
  --set controller.persistence.enabled=false --timeout 8m

# Keycloak - Probe-Werte deutlich grosszuegiger als auf OKD, siehe Falle 2
helm upgrade --install keycloak bitnami/keycloak -n $NS $COMPAT \
  --set global.security.allowInsecureImages=true \
  --set image.registry=docker.io --set image.repository=bitnamilegacy/keycloak \
  --set fullnameOverride=keycloak --set service.ports.http=8080 \
  --set auth.adminUser=admin --set auth.adminPassword=admin \
  --set postgresql.enabled=false --set externalDatabase.host=postgres \
  --set externalDatabase.user=library --set externalDatabase.password=library \
  --set externalDatabase.database=keycloak_db \
  --set production=false --set proxyHeaders=xforwarded \
  --set startupProbe.enabled=true --set startupProbe.initialDelaySeconds=60 \
  --set startupProbe.periodSeconds=15 --set startupProbe.failureThreshold=40 \
  --set livenessProbe.initialDelaySeconds=300 --set livenessProbe.failureThreshold=10 \
  --set readinessProbe.initialDelaySeconds=120 --set readinessProbe.failureThreshold=20 \
  --timeout 10m
Laptop · PowerShell · Äquivalent
helm repo add bitnami https://charts.bitnami.com/bitnami; helm repo update
$NS = "library"; $COMPAT = @("--set","global.compatibility.openshift.adaptSecurityContext=auto")

helm upgrade --install postgres bitnami/postgresql -n $NS @COMPAT `
  --set fullnameOverride=postgres `
  --set auth.username=library --set auth.password=library --set auth.database=library `
  --set auth.postgresPassword=library --set primary.persistence.enabled=false --wait

helm upgrade --install redis bitnami/redis -n $NS @COMPAT `
  --set fullnameOverride=redis --set auth.enabled=false --set architecture=standalone `
  --set master.persistence.enabled=false --wait

helm upgrade --install kafka bitnami/kafka -n $NS @COMPAT `
  --set global.security.allowInsecureImages=true `
  --set image.registry=docker.io --set image.repository=bitnamilegacy/kafka `
  --set fullnameOverride=kafka --set controller.replicaCount=1 `
  --set listeners.client.protocol=PLAINTEXT --set listeners.controller.protocol=PLAINTEXT `
  --set controller.persistence.enabled=false --timeout 8m

# Keycloak - Probe-Werte deutlich grosszuegiger als auf OKD, siehe Falle 2
helm upgrade --install keycloak bitnami/keycloak -n $NS @COMPAT `
  --set global.security.allowInsecureImages=true `
  --set image.registry=docker.io --set image.repository=bitnamilegacy/keycloak `
  --set fullnameOverride=keycloak --set service.ports.http=8080 `
  --set auth.adminUser=admin --set auth.adminPassword=admin `
  --set postgresql.enabled=false --set externalDatabase.host=postgres `
  --set externalDatabase.user=library --set externalDatabase.password=library `
  --set externalDatabase.database=keycloak_db `
  --set production=false --set proxyHeaders=xforwarded `
  --set startupProbe.enabled=true --set startupProbe.initialDelaySeconds=60 `
  --set startupProbe.periodSeconds=15 --set startupProbe.failureThreshold=40 `
  --set livenessProbe.initialDelaySeconds=300 --set livenessProbe.failureThreshold=10 `
  --set readinessProbe.initialDelaySeconds=120 --set readinessProbe.failureThreshold=20 `
  --timeout 10m
Warum $COMPAT ein Array sein muss, kein String

Bash spaltet eine unquotierte Variable an Leerzeichen in mehrere Argumente auf – deshalb funktioniert COMPAT='--set foo=bar' gefolgt von $COMPAT dort als zwei Argumente. PowerShell tut das nicht: $COMPAT = "--set foo=bar" bleibt beim Aufruf einer nativen .exe ein einziges Argument mit eingebettetem Leerzeichen, das Helms Flag-Parser nicht als --set erkennt und stillschweigend falsch verarbeitet (verifiziert – ein Python-sys.argv-Test zeigt das Argument unverändert als einen String, nicht aufgeteilt). Der PowerShell-Weg: $COMPAT als Array anlegen und mit dem @-Sigil statt $ „splatten“ – @COMPAT reicht dann jedes Element als eigenes Argument durch, genau wie Bashs Wortaufspaltung.

Was die Schalter bedeuten

Vier Installationen, 49 Schalter – die meisten wiederholen sich. Nach Zweck sortiert:

Schaltergilt fürbewirkt
helm repo add bitnami …
helm repo update
Vorbereitung Ein Chart-Repository ist ein Verzeichnis fertiger Installationspakete – wie eine Paketquelle für apt, nur für Kubernetes. add trägt die Adresse unter dem Kurznamen bitnami ein, update holt die Liste der verfügbaren Versionen. Erst danach versteht Helm die Schreibweise bitnami/postgresql. Beides ist einmalig pro Rechner nötig, nicht pro Cluster – und schadet nicht, wenn man es wiederholt.
NS=library; COMPAT='--set global.compatibility…' Vorbereitung Eine gewöhnliche Shell-Variable, kein Helm-Begriff. Sie steht hier nur, damit der Namensraum und die lange Wiederholung darunter einmal dastehen statt viermal – wer einen anderen Namensraum nutzt, ändert genau diese Zeile. Beim Einfügen in die eigene Konsole muss sie mitkopiert werden, sonst sind die Variablen darunter leer.
-n $NS alle vier Der Namespace – die Schublade im Cluster, in der diese Anwendung liegt. Ohne die Angabe landet alles in default. Er muss vorher existieren; Helm legt ihn nicht selbst an.
global.compatibility.openshift.adaptSecurityContext=auto alle vier OpenShift verbietet unter restricted-v2 feste UID/fsGroup – genau die setzen die Bitnami-Charts. Der Schalter lässt das Chart seine securityContext-Blöcke an die SCC anpassen. Ohne ihn startet kein einziger Pod.
fullnameOverride=postgres usw. alle vier Erzwingt den Service-Namen exakt so. Helm hängt sonst den Release-Namen an (postgres-postgresql). Die Anwendung erwartet aber postgres, kafka, redis-master, keycloak – so stehen die Adressen in den values-Dateien.
persistence.enabled=false Postgres, Redis, Kafka Kein PersistentVolume, die Daten liegen im emptyDir des Pods. Spart Platz und Zeit – kostet aber alle Daten, sobald der Pod neu erstellt wird (siehe Falle 3).
--wait / --timeout 8m alle vier --wait lässt Helm warten, bis die Pods bereit sind, statt sofort zurückzukehren. Ohne Zeitangabe bricht es nach 5 Minuten ab – auf der CRC-VM zu knapp.
global.security.allowInsecureImages=true
image.registry=docker.io
image.repository=bitnamilegacy/…
Kafka, Keycloak Bitnami hat im August 2025 die versionsgetaggten Images aus docker.io/bitnami/* entfernt – der Chart-Pin läuft ins Leere (manifest unknown). Das eingefrorene bitnamilegacy springt ein; das Chart akzeptiert ein solches „fremdes“ Image aber nur mit dem ersten Schalter.
listeners.client.protocol=PLAINTEXT
listeners.controller.protocol=PLAINTEXT
Kafka Kein TLS und kein SASL innerhalb des Clusters. Für einen lokalen Lern-Cluster angemessen – in Produktion nicht.
controller.replicaCount=1 Kafka Ein einzelner KRaft-Controller statt drei. Kafka braucht seit KRaft kein ZooKeeper mehr; einer reicht ohne Ausfallsicherheit.
postgresql.enabled=false
externalDatabase.host=postgres
Keycloak Das Keycloak-Chart würde sonst eine zweite Postgres-Instanz mitinstallieren. Stattdessen nutzt es die vorhandene, mit eigener Datenbank keycloak_db.
production=false Keycloak Entwicklungsmodus: kein TLS-Zwang, kein erzwungener Hostname. Der Preis steht in Falle 3 – der iss-Claim folgt dem Host-Header des ersten Requests.
proxyHeaders=xforwarded Keycloak Keycloak vertraut den X-Forwarded-*-Kopfzeilen des OpenShift-Routers. Ohne das erzeugt es Weiterleitungen auf http://keycloak:8080 statt auf die externe Adresse.
startupProbe.failureThreshold=40
livenessProbe.initialDelaySeconds=300
readinessProbe.initialDelaySeconds=120
Keycloak Die eigentliche Rettung: Keycloak baut beim Start Quarkus neu, was auf dieser VM 5 Minuten 34 dauert. Die Standardwerte des Charts geben ihm rund zwei Minuten und töten ihn mitten im Build – endlos. Die startupProbe deckt mit 40 Versuchen à 15 s ein Zehn-Minuten-Fenster ab; solange sie läuft, sind Liveness und Readiness pausiert.
Falle 2: Keycloak braucht 5,5 Minuten – die Standard-Probes geben ihm zwei

Beim Start baut Keycloak Quarkus neu. Im Log steht am Ende Quarkus augmentation completed in 334467ms – also 5 Minuten 34. Die Liveness-Probe des Bitnami-Charts schlägt vorher an, tötet den Container, der Neustart baut wieder von vorn. Eine Schleife, in der Keycloak nie fertig wird. Die Werte oben brechen sie auf.

05 Deployen – bewusst mit replicas 0

Das Skript legt Datenbanken, Realm, Secrets, Helm-Releases und Routen an – startet aber nichts.

Laptop · Git Bash · Projektwurzel
bash infra/crc/deploy-crc.sh
Echtes Bash-Skript, kein PowerShell-Äquivalent

Anders als der Registry-Push in Schritt 03 ist das hier kein einzelnes Kommando mit Bash-Syntax drumherum, sondern eine eigenständige .sh-Datei mit den Idempotenz-Checks aus dem nächsten Absatz. Die lässt sich nicht sinnvoll 1:1 nach PowerShell übersetzen, ohne sie doppelt zu pflegen. bash <datei>.sh braucht unter Windows entweder Git Bash (kommt mit Git for Windows, meist schon im PATH) oder eine WSL-Bash – Hauptsache, oc, helm und podman sind darin erreichbar.

Das Skript ist idempotent: Jeder Schritt prüft erst und handelt nur bei Bedarf – CREATE DATABASE hinter einer Existenzprüfung, helm upgrade --install statt helm install, oc create … --dry-run | oc apply statt oc create, Routen nur wenn nicht vorhanden. Ein abgebrochener Lauf kann darum ohne Aufräumen wiederholt werden – was man hier öfter braucht, als einem lieb ist.

Falle 3: persistence.enabled=false ist gefährlicher, als es klingt

Die Bitnami-Charts laufen ohne PVC, die Daten liegen im emptyDir. Ein Container-Neustart ist harmlos – das Volume überlebt ihn. Sobald aber der Pod neu erstellt wird, etwa nach einer Verdrängung durch DiskPressure, sind alle sieben Datenbanken und der Keycloak-Realm weg. Die Dienste laufen dann weiter gegen eine Datenbank, die unter ihnen verschwunden ist – ohne dass es sofort auffällt. Abhilfe: deploy-crc.sh erneut laufen lassen.

Kleinigkeit mit großer Wirkung: Git Bash und oc cp

Git Bash wandelt Argumente, die wie POSIX-Pfade aussehen, in Windows-Pfade um. Aus library/keycloak-0:/tmp/realm.json wird dabei library\keycloak-0;C, und oc cp findet den Pod nicht. Ein doppelter Schrägstrich unterdrückt das gezielt für dieses eine Argument: …keycloak-0://tmp/realm.json. MSYS_NO_PATHCONV=1 wäre zu grob – es lässt auch den KUBECONFIG-Pfad unkonvertiert und legt oc.exe lahm.

06 Gestaffelt hochfahren

Der eigentliche Kern dieser Anleitung.

Laptop · Git Bash · Projektwurzel
bash infra/crc/rollout-staged.sh   # ~20 Minuten, schreibt eine CSV mit Messwerten

Auch dieses Skript ist ein eigenständiges .sh – dieselbe Begründung wie in Schritt 05, kein PowerShell-Äquivalent, Git Bash oder WSL-Bash genügt.

Falle 4: Acht JVMs parallel legen den Cluster dauerhaft lahm

Beim Parallelstart aller acht Spring-Dienste gemessen:

KennzahlWert
Load average (bei 6 vCPU), Maximum aus crc-monitor-*.csv157
Lauffähige Prozesse (Momentaufnahme im Vorfall)104
vmstat (dieselbe Momentaufnahme)us=64 sy=35 id=0 wa=0

wa=0 ist die entscheidende Zahl: reine CPU-Sättigung, kein I/O-Problem. Eine schnellere Platte hätte hier nichts gebracht.

Die Folge war eine Rückkopplung: Der kube-apiserver bekam keine Rechenzeit mehr (TLS handshake timeout), die Probes liefen in Timeouts, die Liveness-Probe tötete Pods, jeder Neustart erzeugte einen weiteren Kaltstart – und damit noch mehr Last. Der Cluster fand da nicht mehr von allein heraus; selbst crictl stop lief in Timeouts. Er musste hart neu gestartet und die Dienste noch vor dem Hochlauf auf replicas=0 gesetzt werden.

Nacheinander gestartet bleibt die Load im niedrigen einstelligen Bereich. Die 5,25 vor config-server stand noch aus dem abgebrochenen Parallelversuch an, danach lag sie zwischen 1,1 und 3,6. Gemessene Startdauern (aus rollout-20260902-012327.csv, dieselbe Datei wie Zeichnung 35 der Diagrammseite):

DienstDauerLoad davorLoad danach
config-server121 s5,252,17
catalog-service144 s2,002,75
member-service116 s2,091,13
lending-service630 s *
reservation-service560 s3,6
fine-service97 s3,232,03
notification-service117 s2,132,32
api-gateway116 s1,871,52
library-frontend (nginx)11 s1,361,74

* Die 630 s waren die Zeitüberschreitung des Rollout-Skripts, nicht die reine Startdauer – lending-service wurde danach trotzdem bereit. Die Load-Spalten für lending und reservation sind in der CSV nicht einzeln festgehalten.

Median der sechs regulären JVM-Dienste: rund 117 Sekunden. lending-service und reservation-service fielen mit 560–630 s deutlich heraus. Das Frontend ist nginx und in elf Sekunden oben – deshalb bekommt es auch nicht das Overlay mit den 150-Sekunden-Probes, sondern seine eigenen Werte aus library-frontend-values.yaml.

07 Prüfen

Laptop · Git Bash
oc get pods -n library
# 13 Pods, alle 1/1: 8 Services + Frontend + postgres/redis/kafka/keycloak

oc get route -n library
# library / keycloak / library-frontend, jeweils .apps-crc.testing

curl -k https://library.apps-crc.testing/actuator/health
# {"status":"UP","groups":["liveness","readiness"]}
curl -ksI https://library-frontend.apps-crc.testing/ | head -1
curl -k https://keycloak.apps-crc.testing/realms/library | head -c 60
Laptop · PowerShell · Äquivalent
oc get pods -n library
# 13 Pods, alle 1/1: 8 Services + Frontend + postgres/redis/kafka/keycloak

oc get route -n library
# library / keycloak / library-frontend, jeweils .apps-crc.testing

curl.exe -k https://library.apps-crc.testing/actuator/health
# {"status":"UP","groups":["liveness","readiness"]}
curl.exe -ksI https://library-frontend.apps-crc.testing/ | Select-Object -First 1
$realm = curl.exe -k https://keycloak.apps-crc.testing/realms/library
$realm.Substring(0, [Math]::Min(60, $realm.Length))
Falle: curl ohne .exe ist in PowerShell kein curl

PowerShell bringt einen eingebauten Alias curl → Invoke-WebRequest mit (Get-Alias curl zeigt es). curl -k … landet damit nicht beim echten curl.exe, sondern bei Invoke-WebRequest, das -k gar nicht kennt: Invoke-WebRequest : Es wurde kein Parameter gefunden, der dem Parameternamen „k“ entspricht. Der Alias lässt sich zwar entfernen (Remove-Item alias:curl), sicherer ist es aber, den echten Windows-eigenen curl.exe (liegt unter C:\Windows\System32\curl.exe) explizit mit der Endung aufzurufen – wie im Block oben. head -1/head -c 60 gibt es in PowerShell nicht; Select-Object -First 1 ersetzt Ersteres, für die Zeichen-Begrenzung bleibt nur .Substring(…) auf dem eingesammelten String.

Erwartetes Ergebnis

Alle drei Routen antworten mit HTTP 200, der Gateway-Health meldet UP, und Keycloak liefert den Realm samt public_key.

Die vier teuren Fallen auf einen Blick

SymptomUrsacheAbhilfe
invalid destination name …\cache\tmpBundleImage<N> bei crc setup CRC 2.63 lädt Bundles als OCI-Artefakt; die Bibliothek kann ihr temporäres Zielverzeichnis nicht in einem Reparse-Point (Junction) anlegen cache vorübergehend als echtes Verzeichnis, nach dem Setup zurück auf die Junction. Lesen und Entpacken durch die Junction funktionieren einwandfrei – nur das Anlegen bricht
Pods werden Pending, Node trägt Taint disk-pressure openshift-console/downloads schreibt in dieser OKD-Version rund 5 GB pro Stunde in sein emptyDir – 17 GB in 3,5 Stunden gemessen Console-Operator auf Unmanaged, Deployment auf 0 – sonst skaliert er zurück
Keycloak startet endlos neu Quarkus-Build dauert 5,5 Minuten, die Standard-Liveness gibt ihm zwei Probe-Werte aus Schritt 04
TLS handshake timeout, Cluster reagiert nicht mehr Acht JVM-Kaltstarts parallel auf 6 vCPU Gestaffelt hochfahren
Laptop · Git Bash · downloads-Pod stilllegen
oc patch console.operator.openshift.io/cluster --type=merge \
  -p '{"spec":{"managementState":"Unmanaged"}}'
oc scale deploy/downloads -n openshift-console --replicas=0
Laptop · PowerShell · Äquivalent
oc patch console.operator.openshift.io/cluster --type=merge `
  -p '{\"spec\":{\"managementState\":\"Unmanaged\"}}'
oc scale deploy/downloads -n openshift-console --replicas=0

Der downloads-Pod liefert in der Web-Konsole nur die Downloadseite für oc und verwandte Werkzeuge. Auf einem lokalen Entwicklungscluster kostet sein Abschalten nichts.