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.
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.
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.
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.
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.
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
| 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