Voraussetzung: Zugang zum Namespace
library. Statt einer echten Novaris/WAS-Installation stehen generische
Red-Hat-UBI-Images stellvertretend für „die alte Legacy-App“ – die
Kubernetes-Mechanik dahinter ist real und identisch.
1 Root und SCC live scheitern lassen
Die alte Novaris-Annahme: läuft als root, chownt
Verzeichnisse. Unter restricted-v2 (A7) scheitert das.
Die alte Novaris-App ist als root gebaut, chownt Verzeichnisse beim Start und schreibt nach festen Pfaden. Unter der Vorgabe-SCC restricted-v2 (A7) bekommt der Pod eine zufällige hohe UID – und alles davon schlägt fehl. Das ist der häufigste Blocker beim Migrieren. Der Fix ist das Image, nicht die SCC.
root, chownt beim Start und bindet Port 80 – unter restricted-v2 (A7) bekommt der Pod eine zufällige hohe UID, und jeder dieser Schritte scheitert mit Operation not permitted oder Permission denied. Der richtige Fix ist das Image: Dateien der Gruppe root, chmod g=u, kein chown, Port 8080. anyuid an genau die eine ServiceAccount ist nur die Brücke, bis das Image sitzt – nie privileged.- die alte App läuft als
root,chownt Verzeichnisse, schreibt nach festen Pfaden und bindet Port 80 – auf WAS traditional lief das jahrelang - unter
restricted-v2(A7) bekommt der Pod eine zufällige hohe UID, nicht 0 – und damit scheitert alles davon chowngibtOperation not permitted, Schreiben gibtPermission denied, Port 80 ist nicht erlaubt- Symptome:
CrashLoopBackOff,container has runAsNonRoot and image will run as root,EACCESin den Logs - welche SCC der Pod bekam, steht in der Pod-Annotation
openshift.io/scc - Fix 1 (der richtige): Image UID-agnostisch bauen – Dateien Gruppe
root,chmod -R g=u, keinchown,USER 1001, Port 8080 - Fix 2 (die Brücke):
anyuidan genau die eine ServiceAccount – nicht am Namespace, nicht an einer Gruppe - Fix 3: nie
privilegedals Reflex –nonroot-v2reicht, wenn das Image nur einen festen Nicht-Null-UID braucht - Brücke einbauen:
oc create sa novaris-legacy -n novaris,oc adm policy add-scc-to-user anyuid -z novaris-legacy -n novaris - dann
serviceAccountName: novaris-legacyin die Deployment-Vorlage – die SCC zählt die der SA, nicht die des Users (A7) - Novaris: die drei Legacy-Module bekommen je eine eigene SA, damit die Ausnahme so klein wie möglich bleibt
- Ziel bleibt
restricted-v2: sobald das Image UID-agnostisch ist, dieanyuid-Ausnahme wieder entfernen – die Brücke ist temporär - OCP-Unterschied: identisch – auf reinem Kubernetes bremst stattdessen die Pod Security Admission (baseline / restricted)
oc create deployment demo-legacy -n library \
--image=registry.access.redhat.com/ubi9/ubi-minimal:latest \
-- sh -c "id; chown -R 1:1 /etc/passwd 2>&1; sleep 3600"
oc get pods -n library -l app=demo-legacy
oc logs -n library -l app=demo-legacy
# id -> uid=1000700003(...) statt 0
# chown -> "Operation not permitted"
oc get pod -n library -l app=demo-legacy -o jsonpath='{.items[0].metadata.annotations.openshift\.io/scc}{"\n"}'
# restricted-v2 - genau die Vorgabe-SCC aus A7
Genau das Symptom aus dem Atlas: eine zufällige hohe UID statt 0,
chown gibt Operation not permitted. Die Brücke, bis ein
Image UID-agnostisch gebaut ist – anyuid an genau eine ServiceAccount,
nie an den Namespace:
oc create sa novaris-legacy -n library
oc adm policy add-scc-to-user anyuid -z novaris-legacy -n library
oc patch deployment demo-legacy -n library --type=merge -p \
'{"spec":{"template":{"spec":{"serviceAccountName":"novaris-legacy"}}}}'
oc get pod -n library -l app=demo-legacy -o jsonpath='{.items[0].metadata.annotations.openshift\.io/scc}{"\n"}'
# anyuid - die Bruecke wirkt
oc logs -n library -l app=demo-legacy
# id -> uid=0(root) - chown auf /etc/passwd (im Image root:root) gelingt jetzt
Der richtige Fix ist das Image
anyuid ist eine Brücke, kein Ziel. Der saubere Weg: Dateien der Gruppe
root, chmod -R g=u, kein chown, USER 1001
– dann läuft die App wieder unter restricted-v2, ohne Ausnahme.
2 Root-Dateisystem sperren, emptyDir mounten
readOnlyRootFilesystem: true sperrt das Image, ein
emptyDir gibt gezielt beschreibbare Inseln.
Legacy-Apps schreiben mitten ins Image: nach /tmp, in Log-Verzeichnisse, in Arbeits- und Temp-Ordner des App-Servers. Die saubere Lösung: das Root-Dateisystem schreibgeschützt stellen (readOnlyRootFilesystem: true) und an jeden Pfad, der beschreibbar sein muss, ein emptyDir mounten. Logs gehen sowieso nach stdout.
readOnlyRootFilesystem: true sperrt jede Schreiboperation in den Image-Layern – und an jeden Pfad, den die App wirklich beschreiben muss, mountest du ein emptyDir. Bei Novaris sind das die vier festen WAS-Verzeichnisse (logs, temp, wstemp, /tmp). Logs stellst du auf stdout um, dann sammelt sie der Cluster (G2). Alles, was einen Neustart überleben muss, gehört in eine PVC (G3) oder einen externen Store – nie in ein emptyDir.- Legacy-Apps schreiben mitten ins Image:
/tmp, Log-Verzeichnisse, App-Server-Arbeits- und Temp-Ordner, Upload-Ordner - Best Practice:
securityContext.readOnlyRootFilesystem: true– nichts in den Image-Layern ist mehr veränderbar - an jeden Pfad, der beschreibbar sein muss, ein
emptyDir-Volume mounten - Novaris (WAS): vier feste Pfade –
.../logs,.../temp,wstemp,/tmp– also vieremptyDir-Mounts emptyDirist pro Pod und weg beim Neustart – für Temp und Logs in Ordnung, weil Logs sowieso nach stdout gehenemptyDir.medium: Memorygibt ein tmpfs-/tmp– schnell, aber es zählt gegen das Memory-LimitsizeLimitauf dememptyDirdeckelt den Verbrauch, damit ein volllaufender Ordner nicht die Nodes-Disk killt- Vorgehen: erst ohne Mounts starten, die
Read-only file system-Fehler sammeln, genau diese Pfade nachrüsten - Falle: Upload-Ordner als
emptyDir– die Datei landet auf Pod A, der Download geht an Pod B - Uploads gehören in Objekt-Storage (S3) oder ein RWX-Volume (G3) – Novaris: Policen-Dokumente wandern nach S3
- Logs statt Log-Dateien: den App-Server auf
stdout/stderrumstellen, dann sammelt Vector sie (G2) readOnlyRootFilesystemgehört zusammen mitallowPrivilegeEscalation: falsein densecurityContext– passt zurestricted-v2(A7)- OCP-Unterschied: keiner –
readOnlyRootFilesystemundemptyDirsind reines Kubernetes
oc delete deployment demo-legacy -n library --ignore-not-found
cat <<'EOF' | oc apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-legacy
namespace: library
spec:
replicas: 1
selector: { matchLabels: { app: demo-legacy } }
template:
metadata: { labels: { app: demo-legacy } }
spec:
serviceAccountName: novaris-legacy
containers:
- name: novaris
image: registry.access.redhat.com/ubi9/ubi-minimal:latest
command: ["sh", "-c", "echo start > /opt/novaris/logs/app.log 2>&1; sleep 3600"]
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
EOF
oc logs -n library -l app=demo-legacy
# "Read-only file system" - genau der erwartete erste Fehler
oc set volume deployment/demo-legacy -n library --add \
--type=emptyDir --mount-path=/opt/novaris/logs
oc rollout status deployment/demo-legacy -n library
oc exec -n library deploy/demo-legacy -- cat /opt/novaris/logs/app.log
Das Vorgehen aus dem Atlas eins zu eins: erst ohne Mounts starten, den
Read-only file system-Fehler sammeln, dann genau diesen Pfad als
emptyDir nachrüsten – nicht pauschal jeden denkbaren Pfad.
3 Sticky Sessions am Router
Mit mehreren Replicas landet der zweite Request bei einem anderen Pod – die Route-Annotation pinnt den Client per Cookie.
Die alte Novaris-App hält den Warenkorb und den Login im JVM-Heap. Mit einer Replica ging das gut. Mit drei Replicas landet der zweite Request bei einem anderen Pod, der die Session nicht kennt – der Nutzer ist plötzlich ausgeloggt. Zwei Auswege: Sticky Sessions am Router (schnell, aber ohne echte Ausfallsicherheit) oder den Zustand raus aus dem Heap in Redis.
haproxy.router.openshift.io/balance: source pinnt einen Client per Cookie oder Quell-IP an denselben Pod – ein gültiger Zwischenschritt, aber ohne Ausfallsicherheit. Der echte Fix ist ein externer Store: spring-session-data-redis fürs Web-Frontend, ein signiertes JWT für die REST-API. Danach ist jeder Pod austauschbar und ein Neustart kostet keine Session.- Die alte Novaris-App hält Login und Warenkorb im JVM-Heap – mit einer Replica unauffällig
- mit mehreren Replicas geht der zweite Request per Round-Robin an einen anderen Pod, der die Session nicht kennt
- Symptom: der Nutzer wird mitten im Ablauf ausgeloggt, der Warenkorb ist leer, HTTP 302 auf die Login-Seite
- Workaround Sticky Sessions: Route-Annotation
haproxy.router.openshift.io/balance: source - der Router pinnt dann einen Client per Cookie oder Quell-IP an denselben Pod
- Sticky hat keine Ausfallsicherheit: stirbt der Pod, ist die Session weg – und beim Rollout wandern alle Nutzer um
- Sticky verhindert außerdem gleichmäßige Last und sauberes Herunterskalieren – nur als Brücke, nicht als Ziel
- echter Fix: Zustand raus aus dem Heap in einen externen Store –
spring-session-data-redis, eine Dependency ohne App-Code - bei WAS: die Session-Replikation auf einen externen Store umstellen, danach ist jeder Pod austauschbar
- die Redis-TTL entspricht dem Session-Timeout – abgelaufene Sessions räumt Redis selbst weg
- für reine APIs: gar keine Session – ein signiertes
JWTimAuthorization-Header, der Server hält nichts - JWT-Preis: Widerruf ist schwer – kurze Laufzeit plus Refresh-Token, keine sensiblen Daten in der Payload
- Novaris-Entscheidung: Web-Frontend nach Redis, REST-API auf JWT, Sticky nur während der Migration (H6)
- OCP-Unterschied: die
balance-Annotation ist OpenShift-Router-spezifisch – auf Ingress-NGINX ist esaffinity
oc create deployment demo-session -n library \
--image=registry.k8s.io/e2e-test-images/agnhost:2.39 --replicas=3 \
-- /agnhost netexec --http-port=8080
oc expose deployment/demo-session -n library --port=8080
oc expose service/demo-session -n library
oc annotate route/demo-session -n library \
haproxy.router.openshift.io/balance=source
oc get route demo-session -n library -o jsonpath='{.spec.host}{"\n"}'
# ohne Sticky-Cookie: jeder Request kann an einen anderen Pod gehen
for i in 1 2 3; do curl -s "http://$(oc get route demo-session -n library -o jsonpath='{.spec.host}')/hostname"; echo; done
# mit Cookie-Jar: derselbe Client landet wiederholt beim selben Pod
c=$(mktemp)
for i in 1 2 3; do curl -s -c "$c" -b "$c" "http://$(oc get route demo-session -n library -o jsonpath='{.spec.host}')/hostname"; echo; done
agnhost netexec ist ein Standard-Testimage aus dem Kubernetes-Projekt,
das unter anderem seinen eigenen Hostnamen zurückgibt – genug, um Pinning
sichtbar zu machen, ohne eine echte Session-App zu bauen. Die drei Aufrufe ohne Cookie
streuen über die Pods, die drei mit Cookie landen wiederholt beim selben.
Grenze von StickyStirbt der gepinnte Pod, ist die Session weg – und bei einem Rollout wandern alle Nutzer gleichzeitig um. Der echte Fix bleibt, den Zustand aus dem Heap in Redis oder ein JWT auszulagern.
4 Den EJB-Timer als CronJob nachbauen
Ein @Schedule-Timer feuert in jeder JVM – mit drei
Replicas dreimal. Der CronJob löst das sauber.
Die alte Novaris-App hat einen EJB-Timer, der jede Nacht die Prämien neu berechnet, und einen @Singleton-Bean als Cache. Beides ging davon aus, dass es genau eine JVM gibt. Mit drei Replicas feuert der Timer dreimal und jeder Pod hat seinen eigenen, abweichenden Cache. Die saubere Lösung ist fast immer: den Timer aus der App holen und zum CronJob machen.
@Schedule-EJB läuft in jeder JVM – also in jeder Replica – und ein @Singleton ist nur pro JVM einer. Die sauberste Antwort ist der CronJob: der Timer wird ein eigenes Objekt mit concurrencyPolicy: Forbid, unabhängig von den Web-Pods. Reicht das nicht (der Job braucht App-Zustand), dann Leader-Election über die Lease-API oder ein verteilter Lock. Der Singleton-Cache wandert nach Redis. Bei Novaris wird die nächtliche Prämienberechnung ein CronJob, der Tarif-Cache geht nach Redis.- Novaris hat einen EJB-Timer für die nächtliche Prämienberechnung und einen
@Singleton-Cache – beide gingen von einer JVM aus - ein
@Schedule-Timer ist in jeder JVM aktiv – mit drei Replicas feuert er dreimal, es gibt doppelte Buchungen - ein
@Singletongilt nur pro JVM – jeder Pod hat seinen eigenen Cache, sie driften auseinander - dasselbe Muster trifft
@Startup-Init, In-Memory-Locks und lokale Zähler - Lösung 1 –
CronJob(empfohlen): der Timer wird ein eigenes Objekt,schedule: "0 2 * * *",concurrencyPolicy: Forbid - der CronJob nutzt dasselbe Image mit anderem Entrypoint und läuft unabhängig von den Web-Replicas (F4)
- Lösung 2 – Leader-Election: die Replicas wählen über die
Lease-API einen Leader, nur der führt den Timer aus - Leader-Election lohnt, wenn der Job Zustand aus der laufenden App braucht – mehr Code, dafür echtes Failover
- Lösung 3 – eine Replica:
replicas: 1plusstrategy.type: Recreate, einfach, aber kein HA für diesen Dienst - Lösung 4 – verteilter Lock: Redis
SET key NX PXoder DBSELECT ... FOR UPDATE, der erste Pod nimmt ihn, die anderen überspringen - beim Lock die Expiry setzen – sonst blockiert ein abgestürzter Pod den Job dauerhaft
- Singleton-Cache: raus nach Redis, dann teilen sich alle Pods denselben Zustand (H3)
- Novaris-Zuordnung: Prämienberechnung wird ein
CronJob, der@Singleton-Tarif-Cache wandert nach Redis - OCP-Unterschied: keiner –
CronJob,LeaseundRecreatesind Standard-Kubernetes
cat <<'EOF' | oc apply -f -
apiVersion: batch/v1
kind: CronJob
metadata:
name: praemien-berechnung
namespace: library
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: berechnung
image: registry.access.redhat.com/ubi9/ubi-minimal:latest
command: ["sh", "-c", "echo \"Praemien berechnet um $(date -Iseconds)\""]
EOF
# nicht bis 2 Uhr nachts warten - von Hand anstossen:
oc create job --from=cronjob/praemien-berechnung praemien-manuell-1 -n library
oc logs -n library job/praemien-manuell-1 -f
oc get cronjob praemien-berechnung -n library
concurrencyPolicy: Forbid verhindert, dass ein zweiter Lauf startet,
solange der erste noch arbeitet – genau das, was ein @Singleton-Timer
in der JVM implizit hatte. Das CronJob-Image kann dasselbe sein wie das der Web-Replicas,
nur mit anderem Entrypoint.
5 Eine Datenquelle ohne Rebuild umkonfigurieren
Die alte Welt: URL und Passwort im Image. Die neue: die Plattform liefert beides, das Image bleibt gleich.
In der alten Welt legt ein Admin die DataSource NovarisDS in der server.xml oder über die WAS-Konsole an – URL, Nutzer und Passwort stehen im Image oder im Profil. Auf OpenShift gehört die Verbindung der Plattform, nicht dem Image: URL als Env aus einer ConfigMap, Passwort aus einem Secret, in Liberty zusammengeführt über ein configDropins/overrides-Fragment.
db2jcc.jar darf im Image bleiben. URL, Nutzer und Passwort gehören nach außen: die URL in eine ConfigMap, das Passwort in ein Secret, in Liberty zusammengeführt über ein configDropins/overrides/datasource.xml-Fragment mit ${env.DB_URL}. Der App-Code ruft weiterhin lookup("jdbc/NovarisDS") – er merkt nichts. Ergebnis: ein Image für alle Umgebungen, Passwort-Rotation ohne Rebuild.- alte Welt: ein Admin legt
NovarisDSin derserver.xmloder über die WAS-Konsole an – URL, Nutzer, Passwort im Image oder Profil - Folgen: jede Umgebung braucht ein eigenes Image, das Passwort liegt im Klartext im Registry-Layer, Rotation heißt Rebuild
- Konfiguration über die Konsole ist nicht reproduzierbar und geht bei jedem Pod-Neustart verloren (I4)
- OpenShift-Form: das Image ist umgebungs-agnostisch und kennt nur den JNDI-Namen
jdbc/NovarisDSplus den Treiber-Jar - die URL und der Nutzer kommen aus einer ConfigMap
db-configals Environment-Variablen (B8) - das Passwort kommt aus einem Secret
db-credentials– als Env oder besser als Datei gemountet - in Liberty legt ein
configDropins/overrides/datasource.xml-Fragment die DataSource an und referenziert${env.DB_URL}und${env.DB_PASSWORD} - das Fragment kommt selbst aus einer ConfigMap und wird nach
.../configDropins/overridesgemountet – Liberty liest es beim Start automatisch - der App-Code ändert sich nicht –
lookup("jdbc/NovarisDS")bleibt unverändert - Spring-Boot-Variante:
SPRING_DATASOURCE_URLund_PASSWORDals Env – dieselbe Idee ohne XML - die HikariCP-Pool-Größen (max, timeout) sind auch Konfiguration und gehören in die ConfigMap
- Datei-Mount statt Env fürs Passwort: es taucht nicht in
oc set env --listoder Crash-Dumps auf (B8) - Novaris:
NovarisDSraus aus derserver.xml,db-credentialsals Secret, Pool-Werte als ConfigMap – ein Image für Dev, Test und Prod - OCP-Unterschied: keiner – ConfigMap, Secret und
configDropinssind Kubernetes plus Liberty-Standard
oc create configmap db-config -n library \ --from-literal=DB_URL=jdbc:postgresql://catalog-db:5432/novaris oc create secret generic db-credentials -n library \ --from-literal=DB_PASSWORD=ein-echtes-passwort oc set env deployment/demo-legacy -n library --from=configmap/db-config oc set env deployment/demo-legacy -n library --from=secret/db-credentials oc exec -n library deploy/demo-legacy -- env | grep DB_ # DB_URL sichtbar, DB_PASSWORD ebenfalls als Env - fuer Produktion lieber gemountet # dieselbe Datenquelle in Test statt Prod: nur die ConfigMap aendern, kein neues Image oc set data configmap/db-config -n library \ --from-literal=DB_URL=jdbc:postgresql://catalog-db-test:5432/novaris oc rollout restart deployment/demo-legacy -n library
Der App-Code ändert nichts – ein Liberty-configDropins/overrides-
Fragment oder SPRING_DATASOURCE_URL liest einfach die Umgebungsvariablen. Wer
das Passwort nicht in oc set env --list sehen will, mountet das
Secret stattdessen als Datei (secrets-in-openshift.html).
6 Vorher/Nachher als Tafel
Die Zusammenfassung von Block H als eine Tafel: links die alte Novaris-Landschaft auf WebSphere Application Server traditional, rechts dieselbe Anwendung auf OpenShift mit Open Liberty. Jede Zeile ist eine Design-Entscheidung, die sich mit der Plattform ändert – und am Ende drei Dinge, die auch nach der Migration schwer bleiben.
legacy-parent auf WAS traditional – Deployment Manager, Node Agents, vertikale Skalierung, Konsolen-Konfiguration, EAR-Deploy, Session-Replikation. Rechts modern-parent auf Liberty/OpenShift – Deployment, horizontale Skalierung mit HPA, GitOps, Image-Replace, zustandslose Pods mit Redis. Jede Zeile verweist auf das Diagramm in Block H, das sie im Detail behandelt. Der Kasten unten nennt drei Fälle, die Architektur bleiben und sich nicht wegmigrieren lassen: verteilte XA-Transaktionen, echte stateful Singletons, warme JVM-Caches.- Topologie: DMgr + Node Agents + n App Server in einer Zelle → ein
Deploymentmit N austauschbaren Pods - Skalierung: vertikal (mehr Heap/CPU pro JVM) → horizontal (mehr Replicas,
HPAnach CPU oder Requests/s) - Konfiguration: Admin-Konsole und
wsadmingegen den laufenden Server (I4) →Deployment+ ConfigMap + Secret in Git, per GitOps (E3) - Artefakt: eine
.earins laufende System deployen → ein Container-Image komplett ersetzen (C1) - Session: WAS-Session-Replikation zwischen App-Servern → zustandslose Pods plus Redis oder JWT (H3)
- Timer:
@Schedule-EJB im App-Server →CronJoboder Leader-Election (H4) - Datenquelle: JNDI in
server.xmloder Konsole → Env aus ConfigMap und Secret plusconfigDropins(H5) - Dateisystem: schreibt frei ins Profil →
readOnlyRootFilesystemplusemptyDir(H2) - Identität: läuft als root oder festem Betriebsnutzer → zufällige hohe UID unter
restricted-v2(A7, H1) - Routing: Web-Server-Plugin plus IBM HTTP Server →
Routeund Router mit TLS am Edge (B4) - Neustart: Server-Neustart in Minuten mit Zustand im Heap → Pod-Restart in Sekunden mit Zustand außen
- was schwer bleibt: verteilte XA-/Zwei-Phasen-Transaktionen – der Recovery-Log ist in Pods schwer sicher abzulegen
- was schwer bleibt: echte stateful Singletons mit strikter Ordnung – Leader-Election hilft, löst es aber nicht völlig
- was schwer bleibt: langlebige In-Memory-JVM-Caches – der Pod-Neustart ist billig, das Neu-Füllen des Caches nicht
- Migrationsreihenfolge: erst Identität und Dateisystem, dann Session und Timer, dann Konfiguration, zuletzt GitOps
| Aspekt | WAS traditional | OpenShift + Open Liberty |
|---|---|---|
| Topologie | DMgr + Node Agents + App-Server in einer Zelle | ein Deployment mit N austauschbaren Pods |
| Skalierung | vertikal, mehr Heap pro JVM | horizontal, mehr Replicas, HPA |
| Konfiguration | Admin-Konsole, wsadmin | Deployment + ConfigMap + Secret in Git, GitOps |
| Artefakt | eine .ear ins laufende System | ein Container-Image komplett ersetzen |
| Session | WAS-Session-Replikation | zustandslose Pods plus Redis oder JWT |
| Timer | @Schedule-EJB im App-Server | CronJob oder Leader-Election |
| Datenquelle | JNDI in server.xml oder Konsole | Env aus ConfigMap/Secret plus configDropins |
| Dateisystem | schreibt frei ins Profil | readOnlyRootFilesystem plus emptyDir |
| Identität | root oder fester Betriebsnutzer | zufällige hohe UID unter restricted-v2 |
Was auch nach der Migration schwer bleibt: verteilte XA-Transaktionen (der Recovery-Log ist in Pods schwer sicher abzulegen), echte stateful Singletons mit strikter Ordnung (Leader-Election hilft, löst es nicht völlig), und langlebige In-Memory-Caches (der Pod-Neustart ist billig, das Neu-Füllen des Caches nicht). Migrationsreihenfolge: erst Identität und Dateisystem, dann Session und Timer, dann Konfiguration, zuletzt GitOps.
7 Typische Fallen
| Symptom | meist |
|---|---|
CrashLoopBackOff nach dem Deploy | container has runAsNonRoot and image will run as root – Image erwartet UID 0 |
| Upload-Datei nach kurzer Zeit weg | in einem emptyDir gelandet statt in S3 oder einer PVC – landet auf Pod A, Download geht an Pod B |
| Nutzer mitten im Ablauf ausgeloggt | mehrere Replicas ohne Sticky Sessions und ohne externen Session-Store |
| doppelte Buchungen bei der Nachtberechnung | ein @Schedule-Timer lief noch im App-Code statt als CronJob, dreimal parallel |
Passwort taucht in oc set env --list auf | Secret als Env statt als Datei gemountet |
+ Die Kurzfassung
anyuid an einer SA nur die BrückeemptyDir-Inseln, echte Daten in eine PVC oder S3CronJob statt @Scheduleserver.xml – ein Image für alle Umgebungen