← Übersicht  ·  Skripte & Dateien  ·  cloud-host · OpenShift · Legacy

Legacy-Apps auf OpenShift modernisieren

Das Modell steht in den Atlas-Diagrammen H1–H6. Referenz ist Novaris, eine fiktive alte Anwendung auf WebSphere Application Server traditional – der Atlas beschreibt sie konkret genug, um die sechs typischen Bruchstellen einer WAS-zu-OpenShift-Migration real durchzuspielen, auch ohne eine echte WAS-Installation: root und SCC, ein schreibgeschütztes Root-Dateisystem, Sticky Sessions, ein EJB-Timer als CronJob, eine Datenquelle ohne Rebuild, und am Ende das Vorher/Nachher als Tafel. EX280-relevant.

Stand: 4. September 2026 OKD 4.x / OCP 4.x ns library

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.

Laptop · Bash
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:

Laptop · Bash
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.

Laptop · Bash
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.

Laptop · Bash
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.

Laptop · Bash
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.

Laptop · Bash
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

AspektWAS traditionalOpenShift + Open Liberty
TopologieDMgr + Node Agents + App-Server in einer Zelleein Deployment mit N austauschbaren Pods
Skalierungvertikal, mehr Heap pro JVMhorizontal, mehr Replicas, HPA
KonfigurationAdmin-Konsole, wsadminDeployment + ConfigMap + Secret in Git, GitOps
Artefakteine .ear ins laufende Systemein Container-Image komplett ersetzen
SessionWAS-Session-Replikationzustandslose Pods plus Redis oder JWT
Timer@Schedule-EJB im App-ServerCronJob oder Leader-Election
DatenquelleJNDI in server.xml oder KonsoleEnv aus ConfigMap/Secret plus configDropins
Dateisystemschreibt frei ins ProfilreadOnlyRootFilesystem plus emptyDir
Identitätroot oder fester Betriebsnutzerzufä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

Symptommeist
CrashLoopBackOff nach dem Deploycontainer has runAsNonRoot and image will run as root – Image erwartet UID 0
Upload-Datei nach kurzer Zeit wegin einem emptyDir gelandet statt in S3 oder einer PVC – landet auf Pod A, Download geht an Pod B
Nutzer mitten im Ablauf ausgeloggtmehrere Replicas ohne Sticky Sessions und ohne externen Session-Store
doppelte Buchungen bei der Nachtberechnungein @Schedule-Timer lief noch im App-Code statt als CronJob, dreimal parallel
Passwort taucht in oc set env --list aufSecret als Env statt als Datei gemountet

+ Die Kurzfassung

Identitätder Fix ist das Image (UID-agnostisch), anyuid an einer SA nur die Brücke
Dateisystemread-only plus gezielte emptyDir-Inseln, echte Daten in eine PVC oder S3
Session/TimerZustand raus aus dem Heap – Redis/JWT statt Sticky, CronJob statt @Schedule
KonfigurationConfigMap/Secret statt server.xml – ein Image für alle Umgebungen