← Übersicht  ·  Skripte & Dateien  ·  Gedankenspiel · Nur Zeichnungen

Dieselbe Bibliothek auf WebSphere Liberty und OpenShift

Ein Gedankenspiel in vierzehn Zeichnungen: Was ändert sich, wenn die acht Spring-Boot-Dienste stattdessen als Jakarta-EE-Anwendungen auf Open Liberty laufen – und mit den Mitteln ausgerollt werden, die OpenShift selbst mitbringt? Der Anlass ist nicht Neugier: der gemessene Engpass des bestehenden Stacks waren acht JVM-Kaltstarts (Zeichnung 35 der Diagrammseite). Liberty hat ein anderes Startprofil. Ob das die gestaffelte Reihenfolge überflüssig machen würde, ist die Frage, auf die diese Seite hinausläuft.

Was hier Messung ist und was Schätzung

Die Zahlen zum bestehenden Spring-Boot-Stack sind gemessen – sie stammen aus rollout-20260902-012327.csv und crc-monitor-*.csv dieses Laptops. Alle Liberty-Werte sind Größenordnungen aus der Dokumentation, nichts davon wurde hier nachgemessen. In Zeichnung 13 stehen beide nebeneinander und sind entsprechend gekennzeichnet. Die Seite ist ein Entwurf zum Verstehen, keine Anleitung zum Nachbauen. Ausführbare Befehle stehen nur als markierte Referenz in der Quellenbasis.

Begriffe und Dateien lesen: Ein WAR ist das auslieferbare Java-Webarchiv. JNDI ist der vom Server aufgelöste Name für eine Ressource wie jdbc/libraryDS. Eine Probe ist der Health-Check, mit dem OpenShift entscheidet, ob ein Container Verkehr bekommt oder neu gestartet wird. Bei Dateien gilt hier immer die Herkunftsmarkierung: du legst an bei Quellcode, wird generiert bei gerenderten Manifests und Release-Zuständen, wichtig zu kennen bei vom Server verwalteten Ergebnissen. So ist server.xml eine von dir gepflegte Konfiguration, ein gerendertes Deployment dagegen ein Ergebnis der Auslieferung.

Farbrollen wie auf der Diagrammseite – derselbe Ton bedeutet dasselbe:

Stand: 2. September 2026 14 Diagramme, 0 Befehle Open Liberty · Jakarta EE 10 · MicroProfile 6

Teil A – was Liberty ist

01 WebSphere traditional gegen Liberty

Zwei Produkte mit ähnlichem Namen und kaum einer Gemeinsamkeit – die Verwechslung steht am Anfang fast jeder Einarbeitung.

WebSphere traditional (ND) – ab 1998 Deployment Manager verwaltet die ganze Zelle Node Agent Node Agent Node Agent AppServer AppServer AppServer Eine Cell ist die Verwaltungseinheit. Konfiguration liegt verteilt in einem XML-Baum und wird synchronisiert. Start in Minuten, Speicher in Gigabyte, alles ist da – ob man es braucht oder nicht. Liberty – ab 2012, voelliger Neubau ein einzelner Prozess kein Manager, kein Agent, keine Zelle eine Datei: server.xml alles, was der Server tun soll, steht darin Start in Sekunden, Speicher in dreistelligen Megabyte. Geladen wird nur, was in den Features steht – Zeichnung 02. Waagerecht skaliert wird nicht mehr im Server, sondern darunter: mehr Container. Genau deshalb passt es zu Kubernetes. Und innerhalb von Liberty noch einmal zwei Open Liberty Quelloffen, Eclipse Public License. Image: icr.io/appcafe/open-liberty Kein Lizenzschritt, kein Konto, sofort verwendbar. Fuer ein Gedankenspiel und fuers Lernen die richtige Wahl – und deshalb liegt diese Seite darauf. Derselbe Laufzeitkern wie unten. WebSphere Liberty Kommerziell von IBM. Image: icr.io/appcafe/websphere-liberty Das Dockerfile muss die Lizenz ausdruecklich annehmen – ohne sie laeuft der Container nicht an. Dafuer: Support, Migrationswerkzeuge fuer traditional, und die Lizenz, die viele Unternehmen ohnehin schon halten. Alles auf dieser Seite gilt fuer beide gleichermassen – der Kern ist derselbe Code. Unterschiedlich sind nur Bezugsweg, Lizenz und Support. Wer mit Open Liberty entwickelt, kann spaeter auf WebSphere Liberty wechseln, ohne die Anwendung anzufassen.
„WebSphere“ meint zwei sehr verschiedene Dinge. Das klassische Produkt ist eine verteilte Verwaltungslandschaft, Liberty ein einzelner Prozess mit einer Konfigurationsdatei. Wer für Container plant, meint immer Liberty – und die Wahl zwischen Open und WebSphere ist danach eine Frage von Lizenz und Support, nicht von Technik.

02 server.xml und Features

Der Server kann viel, lädt aber nur, was dasteht – und genau daran hängen Startzeit, Speicher und Angriffsfläche.

server.xml – die eine Datei <server description="library"> <featureManager> <feature>restfulWS-3.1</feature> <feature>cdi-4.0</feature> <feature>persistence-3.1</feature> <feature>mpConfig-3.0</feature> <feature>mpHealth-4.0</feature> <feature>mpJwt-2.1</feature> </featureManager> <httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="9080"/> <dataSource jndiName="jdbc/libraryDS"> … </server> laedt Was ein Feature mitbringt Ein Bausatz aus OSGi-Bundeln: die Umsetzung einer Spezifikation samt allem, was sie braucht. restfulWS-3.1 zieht JAX-RS nach, JSON-B, und was sonst dazugehoert. Es gibt Sammelfeatures wie jakartaee-10.0 – bequem, aber sie laden dann eben auch alles. schmal gewaehlt sechs Features wie links Start: wenige Sekunden Speicher: dreistellige MB Weniger Code geladen heisst auch: weniger, das verwundbar sein kann. alles eingeschaltet jakartaee-10.0 als Ganzes Start: laenger Speicher: mehr Bequem beim Anfangen, teuer bei jedem einzelnen Pod-Start. Der Unterschied zu Spring Boot, in einem Satz Bei Spring Boot entscheidet die Abhaengigkeitsliste, was im Artefakt landet – und was da ist, wird beim Start untersucht und verdrahtet. Bei Liberty entscheidet die Konfigurationsdatei, was der Server laedt – unabhaengig davon, was die Anwendung mitbringt. Praktische Folge: dieselbe Anwendung kann in einem schmalen Server laufen, ohne dass man an ihr etwas aendert. Und dieselbe Datei sagt einem Aussenstehenden auf einen Blick, was dieser Dienst kann und was nicht – bei einem Fat-JAR muss man die Abhaengigkeiten lesen. Die Datei wird zur Laufzeit ueberwacht: aendert sie sich, uebernimmt Liberty die Aenderung, ohne neu zu starten. Fuer Container ist das weniger wichtig als frueher – aber es ist der Grund, warum Zeichnung 09 anders ausfaellt als bei einer ConfigMap in Spring Boot.
Der Server ist so groß, wie die Liste lang ist. Features sind keine Abhängigkeiten der Anwendung, sondern Fähigkeiten des Servers – und weil sie in einer lesbaren Datei stehen, ist der Funktionsumfang eines Dienstes hier eine Konfigurations- und keine Bau-Entscheidung.

03 Was eigentlich deployt wird

Bei Spring Boot steckt der Server im Artefakt. Bei Liberty steckt das Artefakt im Server. Das dreht mehr um, als es klingt.

Spring Boot – ein Fat-JAR deine Klassen Spring, Hibernate, Jackson … alle Abhaengigkeiten eingebetteter Tomcat – der Server ist Teil der Anwendung java -jar app.jar – mehr braucht es nicht Ein Artefakt, rund 40 MB. Wer es hat, hat alles. Liberty – ein WAR im Server catalog.war – nur deine Klassen plus WEB-INF/lib fuer das, was der Server nicht stellt liegt in /config/apps/ oder dropins/ Liberty stellt JAX-RS, CDI, JPA, JTA bereit die Anwendung bringt sie nicht mit – sie setzt sie voraus Zwei Teile, die zusammenpassen muessen: WAR und Server-Features. Was daraus folgt Die Schichten liegen guenstiger Der Server steckt in einer unteren Image-Schicht und aendert sich selten. Nur das WAR ist neu. Ein neuer Build uebertraegt also wenige MB statt der ganzen Laufzeit – siehe Zeichnung 18 der Diagrammseite. Aktualisieren wird zweigeteilt Sicherheitsluecke in der Laufzeit? Neues Basis-Image, Anwendung unveraendert. Bei einem Fat-JAR muss dafuer jede der acht Anwendungen neu gebaut und neu getestet werden. Dafuer zwei Dinge, die passen muessen Fehlt ein Feature, das die Anwendung braucht, scheitert sie erst beim Start – nicht beim Bauen. Und wer eine Bibliothek ins WAR packt, die der Server auch stellt, bekommt sie doppelt – mit schwer zu lesenden Klassenlade-Fehlern. Es gibt auch den Mittelweg: Liberty kann sich selbst als ausfuehrbares JAR verpacken. Damit hat man wieder ein Artefakt – und verliert genau den Schichtenvorteil oben. Fuer Container nimmt man deshalb ueblicherweise die getrennte Form.
Zwei Teile statt einem. Die Trennung kostet, dass Anwendung und Server-Features zueinander passen müssen – und zahlt dafür, dass beide getrennt aktualisiert und getrennt übertragen werden. Bei acht Diensten auf derselben Laufzeit ist das kein Detail, sondern der Unterschied zwischen einem und acht Rebuilds.

04 Das Container-Image

Wohin die Dateien gehören – und warum es configDropins gibt.

Die Schichten des Images 4 · deine Anwendung – catalog.war, wenige MB 3 · server.xml und Treiber-Bibliotheken 2 · Open Liberty – die Laufzeit 1 · JVM und Betriebssystem-Grundlage Nur Schicht 4 aendert sich bei jedem Commit. Die unteren drei kommen aus dem Cache und werden nie erneut uebertragen. Was im Container unter /config liegt /config/ server.xml configDropins/ defaults/ – vor server.xml gelesen overrides/ – danach, gewinnt also apps/ – ausdruecklich in server.xml genannt dropins/ – wird einfach eingelesen Alles in overrides/ laesst sich als ConfigMap oder Secret einhaengen – ohne das Image anzufassen. Genau dafuer ist das Verzeichnis da. dropins/ ist bequem und in Produktion unueblich: was dort landet, laeuft. Das Muster, auf dem der Rest dieser Seite aufbaut Das Image enthaelt eine vollstaendige, lauffaehige Grundkonfiguration. Alles Umgebungsabhaengige – Datenbankadresse, Keycloak-Aussteller, Protokollstufe – kommt als kleine XML-Schnipsel in configDropins/overrides/ dazu, eingehaengt aus ConfigMaps und Secrets. Dasselbe Image laeuft damit unveraendert lokal, auf CRC und auf einem Produktionscluster. Es ist die Liberty-Entsprechung zu den gestapelten values-Dateien aus Zeichnung 26 der Diagrammseite. Ein Unterschied zu eurem jetzigen Aufbau, der leicht untergeht: Liberty ueberwacht diese Dateien im Betrieb. Aendert sich ein eingehaengter Schnipsel, uebernimmt der Server die Aenderung, ohne dass der Pod neu startet – genau das, was bei einer ConfigMap in Spring Boot nicht passiert (Zeichnung 25 der Diagrammseite). Die Kehrseite: eine Aenderung kann im Betrieb wirken, ohne dass ein Rollout sie sichtbar macht.
Ein Image, viele Umgebungen. Die Trennung zwischen der Grundkonfiguration im Image und den eingehängten Schnipseln in configDropins/overrides ist das Muster, das Liberty containerfreundlich macht – und der Punkt, an dem sich sein Konfigurationsmodell am deutlichsten von Spring Boot unterscheidet.

Teil B – der Server stellt bereit, die Anwendung fragt

05 JNDI – wem die Datenbankverbindung gehört

Der größte begriffliche Unterschied zu Spring Boot – und der, an dem man merkt, dass Jakarta EE aus einer anderen Zeit kommt, mit einem anderen Zuschnitt der Zuständigkeiten.

Spring Boot – die Anwendung baut sich den Pool selbst application.yml spring.datasource.url: jdbc:postgresql://postgres/catalog_db spring.datasource.password: ${SPRING_DATASOURCE_PASSWORD} HikariCP – im Prozess der Anwendung Groesse, Zeitlimits, alles aus derselben Datei Die Anwendung kennt die Adresse, das Passwort und die Poolgroesse. Wer eines davon aendern will, aendert die Anwendungskonfiguration. Liberty – der Server stellt sie bereit server.xml <dataSource jndiName="jdbc/libraryDS"> <properties.postgresql serverName="postgres" databaseName="catalog_db" user="library"/> Pool im Server – unter dem Namen jdbc/libraryDS die Anwendung sieht nur den Namen, nie die Zugangsdaten Adresse, Passwort und Poolgroesse sind Sache des Betriebs. Wie die Anwendung sie bekommt 1 · per Annotation @Resource(lookup = "jdbc/libraryDS") DataSource ds; Der Server setzt das Feld beim Erzeugen der Bohne. Kein Aufruf, kein Konstruktor-Argument. 2 · ueber persistence.xml <jta-data-source> jdbc/libraryDS</jta-data-source> Der Weg fuer JPA. Der EntityManager bekommt seine Verbindung, ohne dass Code sie je anfasst. 3 · von Hand nachschlagen InitialContext.doLookup(…) Der aelteste Weg – und der einzige, bei dem ein Tippfehler erst zur Laufzeit auffaellt. Heute nur noch dort, wo es nicht anders geht. Die Namensraeume – warum es mehrere Praefixe gibt java:comp/env/… nur fuer diese eine Komponente sichtbar – die engste Sicht java:module / java:app im Modul bzw. in der ganzen Anwendung sichtbar java:global/… serverweit – auch fuer andere Anwendungen im selben Server jdbc/libraryDS ohne Praefix: der Kurzname aus server.xml. In der Praxis dieser. Warum diese Umstaendlichkeit ueberhaupt existiert: Jakarta EE trennt zwei Rollen, die bei Spring Boot verschmolzen sind. Der Anwendungs- entwickler sagt, was er braucht („eine Datenquelle namens jdbc/libraryDS“). Der Betreiber sagt, wo die herkommt. Das WAR laesst sich damit unveraendert gegen Test-, Abnahme- und Produktionsdatenbanken betreiben – ohne Neubau und ohne dass die Zugangsdaten je im Artefakt landen. Der konkrete Gewinn hier: ein Datenbank-Passwort wechseln heisst bei Liberty, einen Schnipsel in configDropins/overrides zu tauschen – der Server zieht nach, ohne Neustart. Bei Spring Boot ist es eine Umgebungsvariable, und die wird nur beim Start gelesen (Zeichnung 25 der Diagrammseite).
Der Name ist der Vertrag. Die Anwendung kennt nur jdbc/libraryDS, nie eine Adresse und nie ein Passwort. Das wirkt umständlich, solange man Entwicklung und Betrieb in einer Hand hat – und wird zum Vorteil, sobald das nicht mehr der Fall ist oder dasselbe Artefakt durch mehrere Umgebungen laufen soll.

06 Sicherheit in Liberty

Zwei getrennte Fragen – wer bist du und was darfst du – und die Zuordnung dazwischen, die in Jakarta EE ein eigenes Konfigurationsstück ist.

Schritt 1 – wer bist du? (Authentifizierung) basicRegistry <user name="aydin" password="…"/> <group name="librarians">… Benutzer stehen in der server.xml. Nur zum Entwickeln – nie darueber hinaus. ldapRegistry Der klassische Unternehmensweg: Benutzer und Gruppen kommen aus dem Verzeichnisdienst. Liberty fragt bei jeder Anmeldung dort nach – und braucht dessen Zertifikat im Truststore. MP JWT – hier der Weg Gar keine Registry. Die Identitaet steht im Token, das Keycloak ausgestellt hat. Passt zu eurem Stack: derselbe Keycloak, dasselbe Token – Zeichnung 07. Schritt 2 – die Zuordnung, die es bei Spring Security so nicht gibt application-bnd <security-role name="librarian"> <group name="bibliothek-mitarbeiter"/> </security-role> Rolle im Code ← Gruppe in der Umgebung Warum das eine eigene Stufe ist Im Code steht eine fachliche Rolle: librarian. In der Umgebung steht, was dort eben existiert – eine LDAP-Gruppe, ein Keycloak-Claim. Die Datei uebersetzt zwischen beiden. Jeder der drei Wege endet mit einem Benutzer, der Gruppen mitbringt. Schritt 3 – was darfst du? (Autorisierung), zwei Orte im Code – feingranular @RolesAllowed("librarian") public Response markCopyLoaned(…) Steht direkt an der Methode, die es betrifft. Braucht das Feature appSecurity-5.0 – sonst wird die Annotation stillschweigend ignoriert. Kein Fehler, keine Warnung. in web.xml – nach Adresse <security-constraint> <url-pattern>/admin/* <auth-constraint><role-name>librarian Ganze Pfadbereiche auf einmal, ohne Code anzufassen. Grobkoerniger, dafuer an einer Stelle nachlesbar – und unabhaengig davon, welches Framework darunter liegt. Die drei Stellen, an denen es still fehlschlaegt 1. Feature appSecurity-5.0 vergessen → @RolesAllowed tut nichts, und jeder darf alles. Der gefaehrlichste der drei, weil er wie Erfolg aussieht. 2. Rolle im Code, aber keine Zuordnung in application-bnd → niemand hat die Rolle, alles endet mit 403. Sieht nach einem Rechteproblem beim Nutzer aus. 3. Gruppenname stimmt nicht mit dem ueberein, was die Registry oder das Token liefert – derselbe Effekt wie 2, andere Ursache. Genau dazu Zeichnung 07. Alle drei haben gemeinsam: die Anwendung startet fehlerfrei. Sicherheit ist in Jakarta EE Konfiguration, und fehlende Konfiguration ist kein Fehler.
Drei Stufen, die man getrennt halten muss: woher die Identität kommt, wie deren Gruppen auf fachliche Rollen abgebildet werden, und wo diese Rollen geprüft werden. Die mittlere Stufe hat bei Spring Security keine direkte Entsprechung – und sie ist genau der Grund, warum derselbe Code gegen LDAP und gegen Keycloak laufen kann.

07 MP JWT mit eurem Keycloak

Dasselbe Token wie heute, dieselbe Prüfung – und eine Falle, die es bei Spring Security so nicht gibt.

Keycloak – unveraendert derselbe Realm, derselbe Client, dasselbe Token Zeichnung 30 der Diagrammseite gilt unveraendert weiter heute – Spring Security spring.security.oauth2.resourceserver.jwt: issuer-uri: https://keycloak…/realms/library jwk-set-uri: http://keycloak:8080/…/certs Rollen kommen ueber einen eigenen Converter aus realm_access.roles – den schreibt man einmal selbst. stattdessen – Liberty <mpJwt id="keycloak" issuer="https://keycloak…/realms/library" jwksUri="http://keycloak:8080/…/certs" userNameAttribute="preferred_username" groupNameAttribute="groups" /> Kein Code – aber die letzte Zeile ist die Falle. Die Falle: Keycloak und MicroProfile meinen verschiedene Stellen Was Keycloak ins Token schreibt "realm_access": { "roles": ["librarian", "member"] } Verschachtelt, unter einem Keycloak-eigenen Namen. Was MP JWT sucht "groups": ["librarian", "member"] Flach, oberste Ebene, fester Name aus der Spezifikation. Findet es nichts, hat der Benutzer keine Rolle und jede gesicherte Methode endet mit 403. Zwei Loesungen, und die bessere ist die unbequemere: entweder in Keycloak einen Mapper anlegen, der die Rollen zusaetzlich flach als groups ins Token legt – dann ist das Token spezifikationskonform und jeder MP-JWT-Dienst versteht es. Ein Umweg ueber groupNameAttribute hilft hier nicht: Liberty erwartet dort den Namen eines Claims der obersten Ebene, kein Pfad in ein verschachteltes Objekt. Dieselbe Sorte Problem wie der iss-Claim mit http gegen https (Zeichnung 14 der Diagrammseite): das Token ist gueltig, die Pruefung ist richtig – und trotzdem passt es nicht, weil zwei Seiten dieselbe Sache anders benennen. Die Fehlermeldung nennt nie den Namensunterschied.
Dasselbe Token, dieselbe Signatur, ein anderer Name für die Rollen. Liberty braucht für die Anbindung keinen Code, nur fünf Attribute – dafür entscheidet eines davon darüber, ob der angemeldete Nutzer irgendeine Rolle hat. Der saubere Weg führt über Keycloak, nicht über die server.xml.

Teil C – die Übersetzung

08 Spring Boot nach Jakarta EE

Das meiste hat eine Entsprechung – interessant sind die drei Stellen, an denen es keine gibt.

Spring Boot – heute Jakarta EE 10 + MicroProfile 6 Was im Code steht Was daraus wird @RestController @GetMapping @Path @GET – JAX-RS (restfulWS-3.1) @Service @Component @Autowired @ApplicationScoped @Inject – CDI (cdi-4.0) @Entity (dieselbe Annotation) @Entity – identisch, Paket jakarta.persistence @Value @ConfigurationProperties @ConfigProperty – MP Config (mpConfig-3.0) Actuator /actuator/health @Readiness @Liveness – MP Health (mpHealth-4.0) Micrometer /actuator/prometheus @Counted @Timed – MP Metrics, /metrics @FeignClient @RegisterRestClient – MP Rest Client Resilience4j @CircuitBreaker @Retry @CircuitBreaker @Retry – MP Fault Tolerance spring-security-oauth2-resource-server mpJwt-2.1 – Konfiguration statt Code, Zeichnung 07 Drei Stellen ohne Entsprechung – hier liegt die eigentliche Arbeit Spring Data Repositories interface BookRepo extends JpaRepository Die abgeleiteten Abfragen aus dem Methodennamen gibt es in Jakarta EE 10 nicht. Jede Methode wird zu EntityManager plus JPQL von Hand. Jakarta Data (EE 11) holt das nach – noch nicht hier. Betrifft euer markCopyLoanedIfAvailable unmittelbar. Spring Cloud Config Euer config-server hat kein Gegenstueck. MP Config liest aus Umgebung und Dateien, nicht von einem zentralen Dienst. Das ist keine Luecke, sondern ein anderer Ansatz – und er nimmt dem Rollout seine Startreihenfolge. Ausfuehrlich in Zeichnung 09. Spring Kafka @KafkaListener MicroProfile Reactive Messaging kann dasselbe, denkt aber in Stroemen statt in Rueckrufen – die Umstellung ist gedanklich, nicht mechanisch. Alternative: der Kafka-Client direkt, ohne Rahmen. Betrifft notification- und fine-service. Die ehrliche Einschaetzung: die obere Tabelle ist mechanisch – Annotationen tauschen, Pakete umbenennen, das erledigt zum grossen Teil ein Werkzeug. Die drei roten Kaesten sind Handarbeit, und der linke ist bei einer datenbanklastigen Anwendung der mit Abstand groesste Posten. Wer den Aufwand schaetzen will, zaehlt nicht die Klassen, sondern die Repository-Methoden.
Die Zeile-für-Zeile-Übersetzung ist der einfache Teil. Was Arbeit macht, sind die drei Bausteine ohne Gegenstück – und davon ist Spring Data der teuerste, weil jede abgeleitete Abfrage von Hand nachgebaut werden muss. Genau deshalb ist die Frage „wie viele Repository-Methoden?“ die bessere Aufwandsschätzung als „wie viele Klassen?“.

09 Konfiguration – und was das mit der Startreihenfolge macht

Zwei Philosophien: zentral abholen gegen lokal einhängen. Der Unterschied kostet euch heute zwei Minuten pro Rollout.

heute – zentral abholen config-server muss zuerst laufen – 121 s gemessen catalog member lending … HTTP beim Start · config-server:8888 Ist er noch nicht da, laufen alle acht in eine Neustart-Schleife und brauchen Minuten, um sich zu fangen. Genau deshalb steht im Rollout-Skript an dieser einen Stelle ein rollout status. stattdessen – lokal einhaengen ConfigMap Adressen, Protokollstufe Secret DB-Passwort Umgebung env des Pods eingehaengt als Datei configDropins/overrides/ plus microprofile-config.properties Alles liegt im Dateisystem des Pods, bevor die Anwendung startet. Es gibt niemanden zu warten. Kein neunter Dienst, kein Netzaufruf beim Start, keine Startreihenfolge – und 121 s weniger. Die Rangfolge von MicroProfile Config 400 · Systemeigenschaften -D beim Start der JVM 300 · Umgebungsvariablen aus ConfigMap und Secret 100 · im Artefakt microprofile-config.properties eigene Quellen frei einhaengbar, eigene Zahl Hoehere Zahl gewinnt. Dieselbe Idee wie die gestapelten values-Dateien bei Helm – nur zur Laufzeit statt beim Rendern. Was man dabei aufgibt Der config-server kann Werte im laufenden Betrieb aendern und die Dienste per /refresh nachziehen lassen – ohne Neustart, fuer alle acht auf einmal, aus einer Quelle. Das faellt weg. Teilweiser Ersatz: was als Datei eingehaengt ist, zieht Liberty von selbst nach (Zeichnung 04). Was als Umgebungsvariable kommt, nicht – dafuer braucht es einen Rollout. Die zentrale Verwaltung an einer Stelle ersetzt das nicht. Ob das ein Verlust ist, haengt daran, wie oft ihr im Betrieb tatsaechlich Werte aendert. Bei acht Diensten mit stabiler Konfiguration wiegt die eingesparte Startabhaengigkeit vermutlich schwerer.
Der config-server verschwindet – und mit ihm die einzige echte Startreihenfolge im Stack. Das ist kein Nebeneffekt, sondern der größte betriebliche Unterschied dieser Umstellung: ein Dienst weniger, 121 gemessene Sekunden weniger, und acht Dienste, die sich beim Hochfahren in beliebiger Reihenfolge sortieren dürfen.

Teil D – der Deploy auf OpenShift

10 Zwei Bauwege

Ihr baut heute ausserhalb und schiebt hinein. OpenShift kann auch selbst bauen – und erst dadurch ergeben ImageStreams einen Sinn.

A – ausserhalb bauen (euer Weg heute) Maven baut das WAR – Laptop oder Jenkins Podman baut das Image auf dem Liberty-Basisimage push → ghcr.io oder die CRC-Registry helm upgrade – der Cluster zieht das Image Vorteil: derselbe Bau laeuft ueberall gleich, auch ohne Cluster. Der Cluster ist reines Ziel und weiss vom Bauen nichts. B – Source-to-Image, im Cluster BuildConfig zeigt auf das Git-Repository OpenShift startet einen Build-Pod, holt den Quellcode s2i-Skripte legen das WAR ins Liberty-Basisimage Ergebnis landet im ImageStream – Zeichnung 11 Vorteil: kein Bau-Werkzeug ausserhalb, ein Webhook genuegt. Preis: der Bau braucht CPU im Cluster – siehe unten. Wie s2i das WAR findet Das Liberty-s2i-Image bringt zwei Skripte mit, an die sich alles haelt: assemble – laeuft beim Bauen: Maven anwerfen, Ergebnis nach /config/apps/ legen run – laeuft beim Start des fertigen Containers: den Server hochfahren Kein Dockerfile noetig Das ist der eigentliche Gedanke hinter s2i: wer Java schreibt, soll kein Dockerfile pflegen muessen. Es gibt daneben auch den Docker-Strategie-Build. Fuer euren CRC waere B die schlechtere Wahl – und das ist keine Geschmacksfrage: ein Maven-Build ist minutenlang CPU-gebunden, und die sechs vCPUs sind dieselben, auf denen API-Server und etcd laufen. Genau die Kombination, die in Zeichnung 35 der Diagrammseite zu Last 157 gefuehrt hat. Auf einem Cluster mit eigenen Build-Nodes dreht sich das um: dort ist B der bequemere Weg, weil kein Jenkins und keine Registry-Anmeldung ausserhalb noetig ist. Der Unterschied liegt nicht in der Technik, sondern darin, wem die CPU gehoert.
Zwei Wege, und die Wahl hängt an einer einzigen Frage: hat der Cluster Rechenzeit übrig? Auf einem Single-Node mit sechs vCPUs, auf dem die Steuerungsebene mitläuft, ist Bauen im Cluster genau die Art Last, die alles andere ausbremst.

11 ImageStream und Trigger

Das OpenShift-eigene Stück, das reinem Kubernetes fehlt – und die Antwort auf die Tag-Falle aus Zeichnung 18 der Diagrammseite.

neues Image gepusht oder von s2i gebaut ImageStream „catalog-liberty“ :latest → sha256:9f2c4a… (aktuell) :latest → sha256:1de8bb… (vorher) :latest → sha256:44a1c0… (davor) Der Stream merkt sich die Digests – nicht nur den Tag. Trigger – der Stream meldet sich Deployment mit Trigger-Annotation OpenShift traegt den Digest in die Pod-Vorlage ein Vorlage geaendert → neues ReplicaSet → Rollout laeuft an Wozu die Umleitung gut ist In reinem Kubernetes steht im Deployment ein Tag. Wird derselbe Tag neu gepusht, aendert sich an der Pod-Vorlage nichts – also passiert nichts. Genau die Falle aus Zeichnung 18: der alte Container laeuft weiter, und man sucht den Fehler im Code. Mit ImageStream Der Digest steht in der Vorlage, und der aendert sich zwangslaeufig mit dem Inhalt. Der Trigger kann damit auch einen neu eingetroffenen Inhalt erkennen. Produktiv trotzdem Digest oder feste Tags nutzen. Was ein ImageStream sonst noch kann Fremde Registries spiegeln Ein Stream kann auf ghcr.io zeigen und regelmaessig nachsehen, ob es dort etwas Neues gibt. Damit funktioniert der Trigger auch fuer euren Weg A. Beliebige Marken setzen Neben :latest etwa :abnahme und :produktion, die man von Hand weiterschiebt. Freigabe wird damit ein Zeiger, kein neuer Bau. Der Preis: Bindung ImageStreams gibt es nur bei OpenShift. Ein Chart, das sie nutzt, laeuft auf k3s nicht mehr. Und genau das braucht ihr fuer beide Ziele. Die naheliegende Mischung: weiter ausserhalb bauen wie heute, aber im OpenShift-Cluster einen ImageStream davorsetzen, der ghcr.io spiegelt. Dann hat man den Trigger, ohne das Chart an OpenShift zu binden – die Spiegelung steht neben dem Chart, nicht darin. Fuer k3s bleibt es beim heutigen Weg: neuer Tag je Commit, und Helm sorgt dafuer, dass sich die Pod-Vorlage aendert.
Der ImageStream löst die Tag-Falle, indem er den konkreten Image-Inhalt über Digests nachvollziehbar macht statt nur einen beweglichen Tag zu betrachten. Das ist die eleganteste Antwort auf ein Problem, das ihr heute mit dem Commit-Hash als Tag umgeht – aber :latest bleibt für produktive Auslieferungen eine schlechte Wahl. Der Preis ist eine Bindung, die bei zwei Zielplattformen weh tut.

12 Gesundheit und Probes

MP Health liefert genau die zwei Endpunkte, die Kubernetes ohnehin haben will – und einen dritten, der beim Kaltstart entscheidet.

Im Code – eine Annotation je Probe @Liveness @ApplicationScoped public class LebtNoch implements HealthCheck { public HealthCheckResponse call() { return HealthCheckResponse.up("lebt"); } } Jede Klasse mit der Annotation wird gefunden und beim Abruf aufgerufen. Kein Endpunkt von Hand. Was Liberty daraus macht /health/live → livenessProbe – faellt sie aus, wird der Container getoetet /health/ready → readinessProbe – entscheidet ueber die Endpoints-Mitgliedschaft /health/started → startupProbe – solange sie laeuft, pausieren die anderen beiden Der Unterschied zu heute ist klein – aber einer Spring Boot Actuator /actuator/health/liveness /actuator/health/readiness Muessen ausdruecklich eingeschaltet und freigegeben werden – genau das war eine eurer Laufzeitaenderungen. MicroProfile Health Die drei Endpunkte entstehen mit dem Feature – ohne zusaetzliche Freigabe, mit festen Pfaden. Feste Pfade heissen: dieselben Probe-Angaben passen fuer jeden der acht Dienste. Ein Chart, acht Werte weniger. Die Probe-Werte sind der Ort, an dem Startzeit betrieblich weh tut Bei Keycloak musstet ihr die startupProbe auf 40 Versuche a 15 s ziehen, weil der Quarkus-Build 5 min 34 dauert – die Vorgabewerte gaben ihm zwei Minuten und toeteten ihn mitten im Start. Ein endloser Kreislauf, der wie ein Anwendungsfehler aussieht. Je schneller ein Dienst bereit ist, desto weniger muss man diese Werte ueberhaupt kennen. Genau darum geht es in der naechsten Zeichnung.
Dieselben drei Proben, nur ohne Vorbereitung. MP Health liefert feste Pfade, sobald das Feature eingeschaltet ist – keine Freigabe, keine abweichenden Pfade je Dienst. Das ist ein kleiner Gewinn im Chart und ein größerer beim Betrieb, weil eine Fehlerquelle weniger existiert.

13 Die Rollout-Simulation

Die Frage, auf die diese Seite hinausläuft: wäre die gestaffelte Reihenfolge dann noch nötig?

Nur die erste Zeile ist gemessen. Die beiden darunter sind Groessenordnungen aus der Dokumentation – auf diesem Laptop wurde davon nichts nachgeprueft. Sie taugen fuer die Frage „lohnt sich das Nachmessen?“ – nicht als Grundlage fuer eine Entscheidung. 1 · Spring Boot, gestaffelt – gemessen am 2. September 2026 config-server 121 s catalog 144 s lending 630 s reservation 560 s die uebrigen fuenf zusammen 457 s Summe: 1912 s = 32 Minuten Hoechste Last dabei: 5,25 Gleichzeitig gestartet: Last 157, nach 2 h 29 immer noch nicht vollstaendig oben. Deshalb ueberhaupt die Staffelung. 2 · Liberty, geschaetzt – schmaler Feature-Satz config-server entfaellt – Konfiguration kommt aus eingehaengten Dateien (Zeichnung 09) acht Dienste je wenige Sekunden bis bereit Groessenordnung: einige Minuten Ein Liberty mit sechs Features startet deutlich schneller als eine Spring-Boot-Anwendung, die beim Start ihren Kontext durchsucht. Nicht nachgemessen. 3 · Liberty mit InstantOn – geschaetzt acht Dienste Millisekunden bis Sekundenbruchteile je Dienst Wie das geht Beim Bauen wird der bereits gestartete Prozess eingefroren (CRIU) und als Abbild abgelegt. Der Container setzt darauf wieder auf, statt zu starten – das Hochfahren ist schon passiert. Braucht besondere Rechte im Container. Die Antwort auf die Frage vom Anfang Vermutlich ja – die Staffelung waere entbehrlich. Nicht weil Liberty schneller ist, sondern weil der Engpass ein anderer ist: acht Prozesse, die minutenlang gleichzeitig CPU brauchen, ueberlasten sechs Kerne. Prozesse, die je Sekunden brauchen, tun das nicht – sie sind fertig, bevor sich die Last aufbauen kann. Und ohne config-server faellt zusaetzlich die einzige echte Reihenfolge weg. Was diese Zeichnung nicht sagt: ob sich der Umbau lohnt. Zeichnung 08 zeigt, dass jede Repository-Methode Handarbeit waere. Die 32 Minuten fallen einmal am Tag an – die Umstellung einmal, aber sehr lange. Wer es wissen will, misst einen Dienst, nicht acht.
Der ehrliche Schluss ist ein Vorschlag zum Nachmessen, keine Empfehlung. Die Größenordnungen sprechen dafür, dass das Startproblem bei Liberty nicht aufträte – aber das ist eine Vermutung aus der Dokumentation, keine Messung auf dieser Maschine. Der billige Weg zur Gewissheit: einen Dienst umstellen und die Startzeit im selben Cluster vergleichen.

14 Konkreter Umbau: catalog-service

Was bedeutet „Pilot“? Der Pilot ist der erste begrenzte Migrationsversuch mit dem echten catalog-service. Er prüft an einem repräsentativen Dienst, ob REST-Vertrag, PostgreSQL, Redis, Kafka, Keycloak, Konfiguration, Probes und Rollout unter Liberty zusammenpassen. Erst wenn diese Nachweise gelingen, werden die Erkenntnisse auf die übrigen Dienste übertragen. „Pilot“ bedeutet hier also kontrollierter Vorläufer, nicht Demo-Software und nicht automatisch eine fertige Produktionsmigration.

Der bisherige Dienst bleibt fachlich ein Katalog. Geändert werden Verpackung, Laufzeitvertrag und Betrieb – nicht die REST-Ressourcen oder die Datenhoheit.

1 · Bestand: catalog-service heute Spring Boot RESTController · Spring Data JPA config-server · Actuator realm_access.roles GET /api/books · CopyStatus · catalog_db fachlich: Bücher suchen, Bestand lesen und CopyStatus zurückgeben 2 · Laufzeitvertrag Open Liberty JAX-RS · CDI · Jakarta Persistence mpConfig · mpHealth · mpJwt jdbc/libraryDS · application-bnd Keycloak · groups · ConfigMap · Secret Das WAR nutzt den Serververtrag; Passwörter bleiben außerhalb des WAR. 3 · OpenShift-Auslieferung catalog-service:sha-a1b2c3d · Zielzustand /config/server.xml /config/configDropins/overrides /config/apps/catalog.war Deployment → Service → Route readiness vor Verkehr, Liveness für Neustart Was beim Umbau erhalten bleibt und was sich ändert Fachlicher Vertrag GET /books und CopyStatus bleiben. catalog_db bleibt die Datenhoheit. Der lending-service fragt weiter per HTTP. Damit ist die Migration messbar. Betriebsvertrag JNDI ersetzt URL und Pool im Code. Keycloak groups → application-bnd. ConfigMap und Secret liefern Overrides. Der Pod bekommt keine Zugangsdaten ins Image. Nicht automatisch gelöst Spring Data und Resilience4j sind keine reinen Namensänderungen. Repository, Fehlergrenzen und Tests müssen vor dem Rollout separat geprüft werden. Abnahmekette für genau einen Dienst 1 Bestandstest2 API-Test3 DB-Test4 JWT-Test5 Probe-Test6 Rollout-Test7 Vergleich Erst wenn fachliche Antwort, Datenbank, Rollen, Endpoints und Rollout stimmen, lohnt der Vergleich der Kaltstartzeit. Ein schnellerer Start ersetzt keinen Kompatibilitäts- oder Sicherheitstest.
Der erste sinnvolle Versuch ist kein Komplettumbau. catalog-service ist klein genug für einen kontrollierten Vergleich und berührt trotzdem die entscheidenden Verträge: REST, PostgreSQL, Keycloak, Konfiguration, Probes und Rollout. Die übrigen sieben Dienste werden erst danach anhand der tatsächlich gefundenen Unterschiede bewertet.
BestandLiberty-ZielNachweis vor dem nächsten Schritt
RESTControllerJAX-RS-Ressource mit den echten Pfaden /api/books, /api/books/{id} und /api/books/by-isbn/{isbn}Pact-Provider-Vertrag und Controller-Tests gegen 404, 409, 422 sowie die vorhandenen Rollenregeln.
Spring Data JPA + FlywayJakarta Persistence mit derselben catalog_db, Migrationen und jdbc/libraryDSDie Migration V1__init_catalog_schema.sql und ddl-auto=validate müssen zusammenpassen.
config-server + EurekaMicroProfile Config übernimmt Werte; Eureka entfällt im Kubernetes-Profil zugunsten von DNSDer aktuelle Dienst importiert den Config-Server optional und deaktiviert Eureka unter Profil k8s.
Spring SecuritympJwt prüft Issuer/JWKS; Keycloak-groups werden in application-bnd auf Rollen abgebildetje ein positiver Rollenfall, 401 bei ungültigem Token und 403 ohne Rolle.
ActuatorMicroProfile Health: /health/live, /health/ready, /health/startedReadiness entfernt den Service aus Endpoints; Liveness startet nicht wegen DB-Ausfall neu. Die aktuellen Spring-Pfade sind /actuator/health/liveness und /actuator/health/readiness.
Maven/Podman/JenkinsWAR in ein reproduzierbares Liberty-Image; Deployment mit unveränderlichem DigestImage-Inhalt, SBOM/Scan, Digest und Rollout-Revision dokumentieren.

Abgrenzung: Diese Seite bleibt ein Entwurf ohne Befehle. Die Matrix beschreibt die Abnahmekriterien, damit aus der Liberty-Idee ein kontrollierter Versuch wird; sie behauptet nicht, dass die Migration ohne Codeänderungen gelingt.

Quellenbasis des Piloten

Was bedeutet „Quellenbasis“? Damit sind die überprüften Originalstellen im verbundenen Bibliotheksprojekt gemeint. Sie beschreiben den heutigen Ist-Zustand von catalog-service – also echte Endpunkte, Abhängigkeiten, Datenbank-, Messaging-, Security- und Deployment-Annahmen. Die Quellenbasis verhindert, dass der Liberty-Pilot auf erfundenen Demo-Werten aufbaut.

Die folgenden Aussagen sind aus dem verbundenen Bibliothek-Projekt abgeleitet; der Liberty-Teil ist jeweils als Zielzustand markiert.

Quellpfad im BibliotheksprojektBelegÜbernahme in den Pilot
library-platform/catalog-service/README.mdBounded Context, catalog_db, Copy-Status, Cache-Aside und Kafka-Verantwortungfachliche Abnahmekriterien und Ereignisgrenzen
.../catalog-service/src/main/java/.../BookController.javaechte Pfade, Rollenregeln, Paging und ISBN-SucheJAX-RS-Contract bleibt kompatibel; kein erfundener /books-Pfad
.../catalog-service/pom.xmlSpring Web, JPA, Redis, Kafka, Flyway, Validation, Pact und gemeinsame Securityjede Abhängigkeit einzeln ersetzen, behalten oder bewusst streichen
.../catalog-service/src/main/resources/application.ymlPort 8081, optionaler Config-Server, Eureka-Abschaltung im Profil k8s, DB/Redis/Kafka/IssuerIst-/Zielvergleich für Konfiguration, Discovery und Infrastruktur
infra/helm/values/catalog-service-values.yamlzwei bis fünf Replicas, aktuelle Actuator-Pfade, externe Infrastruktur und Secret-KeysProbe- und Secret-Migration darf nicht still andere Betriebsannahmen einführen
library-platform/contracts/pacts/*catalog-service*.jsonProvider-Verträge mit lending- und reservation-servicebestehende Konsumentenverträge vor Liberty-Optimierung schützen

Die acht Dienste: was nicht automatisch gleich bleibt

DienstVoraussichtlicher Liberty-SchwerpunktErster Nachweis
catalog-serviceJAX-RS, Jakarta Persistence, jdbc/libraryDS, CopyStatus, Redis Cache-AsideREST-Contract, Datenbankzustand und Cache-Verhalten parallel vergleichen
member-serviceJPA/Transaktionen und Keycloak-Rollen für Mitgliedsdaten401/403-Matrix plus Lesen/Ändern eines Mitglieds
lending-serviceTransaktionsgrenzen, Aufrufe zu catalog/member und Kafka-EreignisseAusleihe als Erfolgsfall, fachliches 422 und erzeugtes Event
reservation-serviceSynchroner Katalogzugriff und Fehlerbehandlung statt Resilience4j-DefaultTimeout, Fallback und korrekter fachlicher Statuscode
fine-serviceJPA-Modell und Event-Verarbeitung für GebührenIdempotente Verarbeitung eines wiederholten Events
notification-serviceKafka-Consumer, Retry/DLQ und externe ZustellungOffset-Verhalten bei Neustart und nicht erreichbarem Zusteller
config-serverentfällt als Laufzeitabhängigkeit; Konfiguration wandert in Deployment-InputsStart ohne zentrale Konfigurationsabfrage und mit getrennten Secrets
api-gateway / frontendGateway bleibt der Eingang; Frontend ist kein Liberty-JVM-DienstRoute, CORS, JWT-Weitergabe und acht Backend-Ziele prüfen

Konfigurationskern des Pilotdienstes

server.xml / configDropins
<featureManager> mit jaxrs-3.1, cdi-4.0, persistence-3.1, mpConfig-3.0, mpHealth-4.0 und mpJwt-2.1; die DataSource bindet jdbc/libraryDS an den PostgreSQL-Treiber. Versionsnummern müssen vor dem Image-Bau gegen die verwendete Liberty-Runtime geprüft werden.
Keycloak / application-bnd
Keycloak legt die fachlichen Rollen als flachen groups-Claim ab. Liberty prüft Issuer und JWKS; application-bnd ordnet etwa librarian und member den Rollen der Anwendung zu. Ein gültiges Token ohne passende Rolle ist 403, kein Authentifizierungsfehler.
Deployment / Probes
Der Ist-Dienst hört auf Port 8081 und nutzt im Helm-Chart zwei bis fünf Replicas. Im Liberty-Pilot kann der Container intern auf 9080 hören, der Servicevertrag bleibt jedoch fachlich und routenseitig gleich. ConfigMap liefert nichtgeheime Werte, Secret DB-Zugangsdaten.
server.xml · Vertragskern
<server description="catalog-service">
  <featureManager>
    <feature>jaxrs-3.1</feature>
    <feature>cdi-4.0</feature>
    <feature>persistence-3.1</feature>
    <feature>mpConfig-3.0</feature>
    <feature>mpHealth-4.0</feature>
    <feature>mpJwt-2.1</feature>
  </featureManager>
  <dataSource jndiName="jdbc/libraryDS">
    <jdbcDriver libraryRef="postgres-driver"/>
    <properties.postgresql
      databaseName="catalog_db"
      serverName="postgres"
      portNumber="5432"/>
  </dataSource>
</server>
Image-Schichten · unveränderlich
FROM icr.io/appcafe/open-liberty:kernel-slim-java17-openj9
COPY --chown=1001:0 server.xml /config/server.xml
COPY --chown=1001:0 postgres-driver.jar /config/lib/
COPY --chown=1001:0 catalog.war /config/apps/

# Zugangsdaten nie in diesen Layer schreiben.
# Build-Ergebnis:
# catalog-service@sha256:<nach-build-pruefen>
Deployment · Probe-Vertrag
spec:
  template:
    spec:
      containers:
      - name: catalog
        image: catalog-service@sha256:<digest>
        ports: [{name: http, containerPort: 9080}] # Ist: Spring 8081
        startupProbe:
          httpGet: {path: /health/started, port: http}
          failureThreshold: 40
          periodSeconds: 15
        readinessProbe:
          httpGet: {path: /health/ready, port: http}
        livenessProbe:
          httpGet: {path: /health/live, port: http}
        envFrom:
        - configMapRef: {name: catalog-config}
        - secretRef: {name: catalog-db}
Abnahme · erwartete Signale
HTTP 200  GET /books
HTTP 401  Token fehlt/ungültig
HTTP 403  Token gültig, Rolle fehlt
HTTP 422  fachlich nicht ausleihbar

Ready=True  → Service-Endpoint vorhanden
Ready=False → kein Traffic zum Pod
Restart     → nur bei echtem Liveness-Fehler
Revision    → Deployment verweist auf neuen Digest

Vor dem Kopieren prüfen: Das ist ein Pilotgerüst, kein blind einsetzbares Manifest. Liberty-Runtime, Java-Version, JDBC-Treiber, Namespace-Namen, Secret-Keys, TLS und die tatsächlich unterstützten Feature-Versionen müssen gegen das Zielcluster validiert werden.

Querschnittsabgleich mit den Schwesterprojekten

ProjektKonkretes Muster aus dem QuellcodeVerwendung für die Liberty-Entscheidung
ProcurexPROCUREX-PROJEKT/keycloak/README.md beschreibt JWT/JWK Set und Authorization Code + PKCE; helm/procurex/templates/networkpolicy.yaml begrenzt die Netzpfade.procurex-app validiert lokal als Resource Server. Das bestätigt die Trennung von Token-Ausgabe, lokaler Token-Prüfung und Netzfreigaben für die Security-/Deployment-Abnahme.
Auroraaurora-bank-platform/README.md führt common-security, common-events und architecture-tests; der Parent-POM trennt schnelle Tests von Failsafe/Testcontainers unter mvn verify.Überträgt die Test- und Architekturdisziplin auf den Liberty-Pilot: erst Contract-/Architekturtests, danach echte DB-/Messaging-Integration.
NovarisLegacy-EAR/EJB, JMS/IBM MQ und ein moderner Spring-/Jakarta-/OpenShift-Reaktor liegen bewusst nebeneinander.Belegt, warum die Seite Traditional WebSphere, Liberty und Strangler-Migration getrennt behandelt.
Concordiaconcordia-portal/src/main/liberty/config/server.xml verwendet servlet-6.0, appSecurity-5.0, ldapRegistry-3.0 und mpMetrics-5.0; pom.xml nutzt liberty-maven-plugin 3.10.2 und Runtime 24.0+ auf Java 21.Gibt dem Liberty-Teil die betriebliche Perspektive: JNDI-/Rollenvertrag, LDAPS-Truststore, Ports 19080/19443 und realer Health-Check zählen ebenso wie der Java-Code.

Messung statt Versprechen

Die Rollout-Zeichnung darf eine Hypothese bilden; die Entscheidung braucht einen reproduzierbaren Vergleich. Für den Pilotdienst werden dieselbe CRC- beziehungsweise Cluster-Umgebung, dasselbe Datenvolumen, dieselbe Probe-Definition und derselbe Ressourcenrahmen verwendet.

MesspunktAufzeichnungBestehensregel
ImageBuild-Dauer, Imagegröße, Digest, Scan-ErgebnisDigest reproduzierbar; keine Secrets im Layer
StartPod-Erstellung bis Ready=True, getrennt nach Startup und Readinessmindestens fünf Läufe; Median und Streuung statt Einzelwert
FunktionREST-Status, DB-Zustand, JWT-Rolle, erzeugte Eventsfachlich identisches Ergebnis zum Spring-Boot-Dienst
BetriebCPU/RAM-Spitze, Restart-Zahl, Endpoint-Entwicklungkein Restart durch normalen DB- oder Keycloak-Ausfall

InstantOn bleibt optional: Ein eingefrorener Prozess kann Kaltstartzeit verkürzen, verschiebt aber Komplexität in Build und Laufzeit. Vor einer Nutzung müssen CRIU-Kompatibilität, Netzwerkzustand, Zufallsquellen, offene Verbindungen, native Bibliotheken und die erforderlichen Container-Rechte geprüft werden. Die Zahl „Millisekunden“ ist ohne diesen Test keine Zusage.