← Übersicht  ·  Skripte & Dateien  ·  cloud-host · OpenShift · Build-Kette

Die Build-Kette in OpenShift

Die Zeichnungen dazu stehen hier bei den jeweiligen Schritten. Hier spielst du sie von Hand durch: oc new-app baut aus Quellcode ein Image per S2I, ein echter Webhook löst einen Build aus, ein Tag allein löst die Tag-Falle nicht, feature/develop/main bilden auf Test, QS und Prod ab – und ein winziger Helm-Chart deployt das Ergebnis wirklich. EX280-relevant.

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

Voraussetzung: ein Cluster mit oc login als Admin, Namespace library. Die interne Registry muss eingeschaltet sein (Schritt 7 zeigt wie). Auf SNO/CRC steht sie per Default auf Removed.

1 Die Kette im Überblick

Vier Objektarten, ein Weg: Quellcode wird zu einem laufenden Container.

1 QuelleGit-Repo, Binary-Verzeichnis oder Dockerfile – der Input eines Builds
2 BuildConfigdas Rezept: welche Strategie (S2I/Docker/Custom), welcher Input, wohin
3 Build-Podein Lauf des Rezepts – baut das Image, pusht es
4 ImageStreamder clusterinterne Zeiger auf das Ergebnis – Digest statt Tag, das löst die Tag-Falle (Schritt 6)
5 Deploymentzieht über den ImageStream oder direkt aus der Registry (Schritt 7)

Nur Schritt 2 und 3 (BuildConfig, Build-Pod) sind optional – wer extern baut (wie die Bibliothek, Schritt 12), pusht direkt in Schritt 4 und überspringt den Build-Pod im Cluster komplett.

2 Mit oc new-app starten

oc new-app rät aus der Eingabe, was zu tun ist, und legt einen ganzen Satz Objekte auf einmal an.

oc new-app ist ein schlaues Gerüst-Werkzeug: du gibst ihm etwas, es rät, was du willst, und legt einen Satz Objekte an. Eine Git-URL wird zu Build-Kette plus Deployment, ein Image nur zu einem Deployment, ein Template zu dessen Objekten. Gut zum Ausprobieren – für Produktion nimmt man Helm.

Du gibst etwas rein – oc new-app rät den Rest. Git-URL (https://…) Container-Image (--image=nginx) Template-Name / lokales Verzeichnis oc new-app erkennt die Sprache an Dateien im Repo (pom.xml…) wählt ein passendes S2I-Builder-Image oc status zeigt den Graphen was angelegt wird – je nach Eingabe Git-URLImageStream(s) + BuildConfig (Source) + Deployment + Service Container-Image → nur Deployment + Service + ImageStream (kein Build) Template → die Objekte des Templates, mit Parametern keine Route – die kommt separat mit oc expose svc/catalog (B4) seit OCP 4.14: Deployment, nicht DeploymentConfig (B2) gemeinsames Label app=catalog auf allem → zusammen löschbar oc new-build – nur die Build-Seite legt ImageStream + BuildConfig an, kein Deployment für den Fall, dass der Deploy woanders liegt (Helm, GitOps) oc new-build https://github.com/org/catalog --name=catalog Chained Build: --source-image= nimmt das Artefakt aus einem anderen Build (C3) Sprache erzwingen: -i ubi9/openjdk-17 oder --strategy=docker nützliche Flags -o yaml --dry-run=client – alles zeigen, nichts anlegen -l app=catalog – gemeinsames Label --name=catalog · -e KEY=val (env) --context-dir=svc/catalog für Monorepos oc delete all -l app=catalog räumt den ganzen Satz auf wann welches Werkzeug oc new-app: Ausprobieren, Demo, Prüfung (EX280) schnell was Laufendes, ohne YAML zu schreiben Produktion: Helm (B11) oder Kustomize deklarativ, versioniert im Git, reviewbar Trick: oc new-app --dry-run -o yaml als Startpunkt für ein Chart Merksatz: new-app = Build + Deploy + Service (keine Route), new-build = nur Build, --dry-run zeigt es vorher Bibliothek: new-app nur zum Skizzieren – die 8 Dienste kommen als Helm-Chart (B11), gebaut von Jenkins außerhalb (E1) OCP-Unterschied: oc new-app ist OpenShift-eigen – das nächste Kubernetes-Äquivalent ist kubectl create deployment (nur das Deployment).
Ein Kommando, ein ganzer Objektsatz. oc new-app erkennt an der Eingabe, was du willst: eine Git-URL wird zu ImageStream + BuildConfig + Deployment + Service, ein Container-Image nur zu Deployment + Service, ein Template zu dessen Objekten. Eine Route legt es nicht an – die kommt mit oc expose. oc new-build macht nur die Build-Seite. Mit --dry-run -o yaml siehst du alles vorher.
  • oc new-app rät aus der Eingabe: Git-URL → BuildConfig + S2I, Image → nur Deployment, Template → dessen Objekte
  • legt typisch an: ImageStream(s), BuildConfig, Deployment, Service
  • keine Route – die kommt separat mit oc expose svc/catalog (B4)
  • die Sprache wird an Dateien im Repo erkannt (pom.xml, package.json, go.mod…) → passendes Builder-Image
  • oc new-app -o yaml --dry-run=client zeigt alles vorher, ohne etwas anzulegen
  • oc new-build: nur die Build-Seite (ImageStream + BuildConfig), kein Deployment – für den Fall, dass der Deploy woanders liegt
  • -l app=catalog setzt gemeinsame Labels → oc delete all -l app=catalog räumt den ganzen Satz zusammen auf
  • weitere Flags: --name=, -e KEY=val, --strategy=docker|source, -i <imagestream>, --context-dir=
  • seit OCP 4.14 entsteht ein Deployment, nicht DeploymentConfig (B2)
  • gut zum Ausprobieren und für die Prüfung – für Produktion nimmt man Helm (B11) oder Kustomize (deklarativ, versioniert, reviewbar)
  • Trick: oc new-app --dry-run -o yaml als Startpunkt für ein eigenes Chart abgreifen
  • Bibliothek: new-app höchstens zum Skizzieren – die 8 Dienste kommen als Helm-Chart (ein generisches library-service-Chart je Dienst gefüllt), deployt von Jenkins nach dem externen Build (E1)
  • OCP-Unterschied: oc new-app ist OpenShift-eigen – das nächste Kubernetes-Äquivalent ist kubectl create deployment
Laptop · Bash
# zuerst nur ansehen, ohne etwas anzulegen:
oc new-app https://github.com/sclorg/nodejs-ex -n library --dry-run=client -o yaml --name=demo-build | head -40

# jetzt wirklich:
oc new-app https://github.com/sclorg/nodejs-ex -n library --name=demo-build -l app=demo-build

oc get is,bc,deploy,svc -n library -l app=demo-build

new-app legt typisch an: einen ImageStream (Schritt 6), ein BuildConfig (Schritt 3), ein Deployment und einen Service. Keine Route – die kommt separat mit oc expose svc/demo-build. Die Sprache wird an Dateien im Repo erkannt (package.json, pom.xml, go.mod…), das passende S2I-Builder-Image wird automatisch gewählt.

nur die Build-Seiteoc new-build legt nur ImageStream + BuildConfig an, kein Deployment – für den Fall, dass der Deploy woanders liegt (Helm, Schritt 12).

3 Das BuildConfig-Objekt

Das Rezept „wie entsteht dieses Image“ – jeder Lauf ist ein eigenes Build-Objekt in einem Build-Pod.

Ein BuildConfig beschreibt, wie aus Quellcode ein Image wird. Jeder Lauf ist ein Build-Objekt, ausgeführt in einem Build-Pod (als builder-SA). Drei Strategien: S2I (Code + Builder-Image, kein Dockerfile), Docker (aus einem Dockerfile), custom (eigenes Builder-Image steuert alles). Ergebnis landet als ImageStreamTag (C1).

Input → Strategie → Build-Pod → ImageStreamTag. Input source.git + ref Binary (--from-dir) Dockerfile inline Files aus anderem Image contextDir für Monorepos BuildConfig catalog „wie entsteht dieses Image“ · du legst an / oc new-build (C4) Strategie (genau eine): Source (S2I) – Code + Builder-Image, kein Dockerfile Docker – aus einem Dockerfile (intern Buildah) Custom – eigenes Builder-Image steuert den Ablauf runPolicy: Serial / Parallel / SerialLatestOnly · resources begrenzen Build-Pod ein Build-Objekt je Lauf, läuft als builder-SA Lebenszyklus: New → Pending → Running → Complete / Failed / Error / Cancelled braucht CPU im Cluster – das ist der Haken Logs: oc logs -f build/catalog-3 Output gepusht als ImageStreamTag in die interne Registry (C5) oder in eine externe Registry (mit Push-Secret) der Digest im Stream löst über den Trigger den Rollout aus (C1/C6) postCommit-Hook: Smoke-Test direkt nach dem Build Ergebnis ist ein gewöhnliches OCI-Image Trigger am BuildConfig – was einen Build startet Webhook – GitHub / GitLab / Bitbucket / generic: eine URL, die du im Repo registrierst ImageChange – neuer Builder-Image-Tag → neu bauen (Security-Patches holen) ConfigChange – Änderung am BuildConfig selbst von Hand: oc start-build catalog, oc start-build catalog --from-dir=. --follow oc cancel-build catalog-3 · oc get builds · Webhook-URL: oc describe bc catalog der Haken – Build-Pods kosten Cluster-CPU ein Java-Build zieht ganz Maven, kompiliert, testet – das im Cluster auf SNO oder dem kleinen Bibliothek-Cluster: konkurriert mit den Diensten besser: CI außerhalb baut, im Cluster nur ein spiegelnder Stream (C1) S2I / BuildConfig sind gut zum Lernen und für kleine Teams ohne eigene CI Ressourcen deckeln: resources im BuildConfig oder die Build-Defaults-Config Bibliothek: die 8 Dienste werden von Jenkins außerhalb gebaut (E1) – kein BuildConfig im Cluster, nur spiegelnde Streams Merksatz: BuildConfig = das Rezept, Build = ein Lauf, Strategie = S2I / Docker / Custom, Output = ImageStreamTag S2I-Innenleben (assemble/run, inkrementell): C3 · welche Objekte oc new-app daraus macht: C4 OCP-Unterschied: identisch – die Docker-Strategie nutzt in beiden Buildah, keinen Docker-Daemon.
Ein Rezept, viele Läufe. Der BuildConfig hält Input (Git / Binary / Dockerfile), Strategie (S2I / Docker / Custom) und Output. Jeder Build läuft in einem Build-Pod als builder-SA und pusht das Ergebnis als ImageStreamTag. Trigger (Webhook, ImageChange, ConfigChange) starten ihn. Der Haken: Build-Pods brauchen Cluster-CPU – auf kleinen Clustern baut man besser außerhalb und spiegelt nur (C1).
  • ein BuildConfig ist das Rezept „wie entsteht dieses Image“ – jeder Lauf ist ein Build-Objekt in einem Build-Pod (als builder-SA)
  • Source (S2I): Quellcode + Builder-Image → fertiges App-Image, kein Dockerfile – das assemble-Skript baut (C3)
  • Docker: baut aus einem Dockerfile im Repo – intern mit Buildah, kein Docker-Daemon im Cluster
  • Custom: ein eigenes Builder-Image bestimmt den ganzen Ablauf (selten, für Spezial-Tooling)
  • Input: Git (mit ref), Binary (oc start-build --from-dir), Dockerfile inline, Files aus einem anderen Image – contextDir für Monorepos
  • Trigger am BuildConfig: Webhook (GitHub / GitLab / Bitbucket / generic, eine URL im Repo), ImageChange am Builder-Image (holt Base-Image-Patches automatisch), ConfigChange
  • Output: gepusht als ImageStreamTag in die interne Registry (C5) – der neue Digest kann über einen Image-Trigger den Rollout auslösen (C1, C6)
  • Lebenszyklus: New → Pending → Running → Complete / Failed / Error / Cancelled. runPolicy: Serial / Parallel / SerialLatestOnly
  • der Haken: Build-Pods brauchen Cluster-CPU – auf der Single-Node-SNO konkurrieren sie direkt mit den 8 laufenden Diensten
  • wann man S2I doch nimmt: kein Dockerfile pflegen wollen, oder Auto-Rebuild bei einem Base-Image-CVE (ImageChange am Builder). Wer schon eine CI hat, baut besser dort
  • Befehle: oc start-build catalog --from-dir=. --follow, oc logs -f build/catalog-3, oc cancel-build catalog-3, oc get builds
  • Bibliothek: kein BuildConfig im Cluster – Jenkins baut die 9 Images mit podman build (Maven im Container gegen die Host-Engine), dann podman push in die interne Registry. Die ImageStreams entstehen durch den Push, nicht durch einen Build-Pod
  • OCP-Unterschied: identisch – die Docker-Strategie nutzt in beiden Buildah
Strategiemacht
Source (S2I)Quellcode + Builder-Image → fertiges App-Image, kein Dockerfile nötig – das assemble-Skript des Builder-Images baut
Dockerbaut aus einem Dockerfile im Repo – intern mit Buildah, kein Docker-Daemon im Cluster
Customein eigenes Builder-Image bestimmt den ganzen Ablauf – selten, für Spezial-Tooling

Trigger am BuildConfig: ein Webhook (GitHub/GitLab/generic, eine URL im Repo), ImageChange am Builder-Image (holt Base-Image-Patches automatisch nach), ConfigChange. Lebenszyklus: New → Pending → Running → Complete (oder Failed/Error/Cancelled). runPolicy steuert, ob parallele Builds erlaubt sind: Serial, Parallel, SerialLatestOnly.

4 Einen Build laufen lassen

Ein manueller Start, live verfolgt.

Laptop · Bash
oc start-build demo-build -n library --follow
# oder ohne zu warten:
oc start-build demo-build -n library
oc get builds -n library
oc logs -f build/demo-build-2 -n library

# ein Build, der zu lange braucht oder haengt:
oc cancel-build demo-build-2 -n library

der Haken auf SNOBuild-Pods brauchen echte Cluster-CPU – auf einer Single-Node konkurrieren sie direkt mit allen laufenden Diensten. Ein Build mitten in einer Demo kann spürbar bremsen.

Fertig gebaut landet das Image automatisch als neuer Eintrag im ImageStream (Schritt 6) – oc get istag demo-build:latest zeigt den frischen Digest.

5 Webhook live auslösen

oc new-app hat in Schritt 2 automatisch zwei Webhook-Trigger mit zufälligem Secret angelegt – die sieht man sich einmal live an, statt sie nur als Konzept zu kennen.

Laptop · Bash
oc describe bc/demo-build -n library | grep -A2 "Webhook"
# Webhook Generic:
#     URL:  https://api.sno.<ip>.nip.io:6443/apis/build.openshift.io/v1/namespaces/library/buildconfigs/demo-build/webhooks/<secret>/generic
# Webhook GitHub:
#     URL:  https://api.sno.<ip>.nip.io:6443/.../webhooks/<secret>/github

# die generic-URL genuegt sich selbst - einfach POSTen loest einen Build aus:
WEBHOOK_URL=$(oc describe bc/demo-build -n library | awk '/Webhook Generic/{getline; print $2}')
curl -sk -X POST "$WEBHOOK_URL"
oc get builds -n library -w

Ein echtes GitHub-/GitLab-/Bitbucket-Repo trägt genau so eine URL unter Settings → Webhooks ein – ein Push landet dann automatisch hier. Der Unterschied zur generic-URL: die drei Provider-Typen parsen den Payload und prüfen darin den Branch gegen spec.source.git.ref – passt der Branch nicht, wird der Build still ausgelassen (der Webhook antwortet trotzdem mit 200). generic prüft nur das Secret in der URL, keinen Branch, außer du schickst selbst einen git.ref im JSON-Body mit.

wo das Secret herkommtBei oc new-app generiert OpenShift es automatisch. Von Hand: oc set triggers bc/demo-build --from-webhook --webhook-secret=<secret>, oder in der YAML unter spec.triggers[].generic.secretReference.

6 ImageStream, Digest, die Tag-Falle

Ein ImageStream ist ein clusterinterner, namespaced Zeiger auf Images – er speichert keine Bytes, sondern je Tag die Digest-Historie.

Ein ImageStream speichert keine Image-Bytes – das Image liegt in einer Registry. Der Stream führt Buch: welcher Digest (sha256) stand wann hinter welchem Tag. Zusammen mit dem Image-Trigger (C6) löst das die klassische Tag-Falle: ein neuer Push unter :latest löst auf OpenShift wirklich einen Rollout aus.

Der Stream merkt sich den Digest, nicht nur den Tag – darum wirkt :latest hier zuverlässig. Registry hält die Image-Bytes (Layer, Manifest) intern (….svc:5000, C5) oder extern (ghcr.io) jedes Image hat einen unveränderlichen Digest sha256:9f2e...c1a4 ein Tag (:1.4, :latest) ist nur ein Label, das jederzeit auf ein neues Image zeigen kann ImageStream catalog clusterintern, namespaced · speichert keine Bytes führt je Tag die Digest-Historie: catalog:latest -> sha256:9f2e...c1a4 (jetzt) -> sha256:41ab...77de (davor) catalog:prod -> sha256:41ab...77de ein ImageStreamTag = eine Marke, löst zu einem Digest auf lookupPolicy.local: true → ein reines Deployment darf image: catalog:latest schreiben, der Stream löst es auf Deployment / DC der Image-Trigger (C6) trägt beim neuen Tag den Digest in die Pod-Vorlage → neues ReplicaSet → Rollout der Pod läuft dann auf catalog@sha256:9f2e... nicht auf dem Tag – der Tag ist nur die Auswahl Pods ziehen mit dem Token der default-SA – kein Extra-Secret (C5) Freigabe: oc tag catalog:sha-abc catalog:prod – ein Zeiger, atomar import / spiegeln Trigger (C6) die Tag-Falle – und warum OpenShift sie löst reines Kubernetes: Pod-Vorlage sagt image: catalog:latest du pushst catalog:latest neu → die Vorlage ändert sich nicht → nichts passiert dein Fix ist gebaut, aber nie ausgerollt – du suchst den Fehler im Code OpenShift: der Trigger löst den Tag zum Digest auf und schreibt @sha256:… in die Vorlage neuer Push → neuer Digest → Vorlage ändert sich → Rollout der einzige Ort, an dem :latest zuverlässig wirkt – sonst nie :latest verwenden extern bauen, intern spiegeln oc import-image catalog --from=ghcr.io/org/catalog:1.4 importPolicy.scheduled: true → periodisch nachimportieren (~15 min) Build-Pods kosten CPU im Cluster (C2) – auf SNO die schlechtere Wahl Bibliothek / cloud-host: CI baut extern, im Cluster nur ein spiegelnder Stream je Dienst + Trigger am Deployment der Hetzner-Server zieht direkt aus ghcr.io (Diagrammseite 01) Befehle: oc get is, oc get istag, oc describe is catalog (Digest-Historie), oc tag, oc import-image Merksatz: Registry = Bytes, ImageStream = Digest-Historie je Tag, Trigger = schreibt den Digest in die Pod-Vorlage Preis: ImageStream gibt es nur bei OpenShift – ein Chart, das ihn nutzt, läuft auf k3s nicht (portabel: nur Deployment + Digest im Image-Feld) OCP-Unterschied: identisch – ImageStreams sind Kern-OpenShift, in OKD und OCP gleich.
Ein Zeiger mit Gedächtnis. Der ImageStream hält keine Bytes – die liegen in der Registry. Er merkt sich, welcher Digest hinter jedem Tag stand. Der Image-Trigger (C6) schreibt diesen Digest in die Pod-Vorlage, statt den Tag stehen zu lassen. Darum löst ein erneuter Push unter :latest hier wirklich einen Rollout aus – die Tag-Falle, die reines Kubernetes hat, ist zu. Freigeben heißt einen Zeiger verschieben: oc tag catalog:sha-abc catalog:prod.
  • ein ImageStream ist ein clusterinterner, namespaced Zeiger auf Images – er speichert keine Bytes, sondern je Tag die Digest-Historie
  • ein ImageStreamTag (catalog-service:9f3ab21, catalog-service:prod) ist eine Marke, die zu einem Digest (sha256) auflöst
  • die Tag-Falle: mit reinem Kubernetes bleibt die Pod-Vorlage bei catalog:latest – ein erneuter Push ändert sie nicht, es passiert nichts
  • OpenShift löst sie auf zwei Arten: der Image-Trigger (C6) trägt den neuen Digest in die Pod-Vorlage – oder man pusht von vornherein unter einem unveränderlichen Tag (so macht es die Bibliothek)
  • der einzige Ort, an dem :latest zuverlässig wirkt – im Chart und im Deployment sonst nie :latest
  • Freigabe durch Um-Taggen: oc tag catalog-service:9f3ab21 catalog-service:prod – ein Release ist nur ein Zeiger, atomar und umkehrbar
  • ein externes Image in den Cluster holen: oc import-image catalog --from=ghcr.io/org/catalog:1.4 --confirm, importPolicy.scheduled: true importiert periodisch nach
  • lookupPolicy.local: true lässt ein reines Deployment den Tag über den Stream auflösen, statt die Registry direkt zu nennen
  • Pods ziehen mit dem Token der default-/builder-SA – kein Extra-Secret für die interne Registry (C5)
  • Befehle: oc get is, oc get istag, oc describe is catalog-service (Digest-Historie), oc tag, oc import-image
  • Preis: ImageStream gibt es nur bei OpenShift – ein Helm-Chart, das ImageStreamTag-Referenzen nutzt, läuft auf reinem Kubernetes nicht
  • Bibliothek: Jenkins baut die 9 Images außerhalb und podman push sie in die interne Registry – der Pfad library/catalog-service:<git-sha> legt automatisch einen ImageStream catalog-service in ns library an. Ein unveränderlicher Tag je Build, darum kein :latest und kein Image-Trigger nötig
  • OCP-Unterschied: identisch – ImageStreams sind Kern-OpenShift, in OKD und OCP gleich
Laptop · Bash
oc get is demo-build -n library
oc describe is demo-build -n library     # zeigt die Digest-Historie je Tag
oc get istag demo-build:latest -n library -o jsonpath='{.image.dockerImageReference}{"\n"}'

die Tag-FalleMit reinem Kubernetes bleibt die Pod-Vorlage bei demo-build:latest – ein erneuter Push ändert sie nicht, es passiert einfach nichts. OpenShift löst das auf zwei Arten: ein Image-Trigger (C6) trägt den neuen Digest automatisch in die Pod-Vorlage ein – oder man pusht von vornherein unter einem unveränderlichen Tag (so macht es die Bibliothek, Schritt 12).

Ein externes Image in den Cluster holen: oc import-image demo --from=registry.access.redhat.com/ubi9/ubi:latest --confirm, importPolicy.scheduled: true importiert periodisch nach. lookupPolicy.local: true lässt ein reines Deployment den Tag über den Stream auflösen, statt die Registry direkt zu nennen. Pods ziehen mit dem Token der default-/builder-ServiceAccount – kein Extra-Secret für die interne Registry nötig.

7 Die interne Registry von Hand

Der image-registry-ClusterOperator verwaltet sie im Namespace openshift-image-registry – auf SNO/CRC per Default auf Removed.

OpenShift bringt eine eigene Container-Registry mit, verwaltet von einem Operator. Sie hat zwei Adressen: eine interne (im Cluster, zum Ziehen und für Build-Pods) und eine externe Route (zum Schieben von außen, muss erst freigeschaltet werden). Pods ziehen ohne Extra-Secret. oc adm prune images hält sie klein.

Eine Registry, zwei Adressen: innen zum Ziehen, außen (opt-in) zum Schieben. interne Registry Deployment image-registry in openshift-image-registry verwaltet vom image-registry-ClusterOperator (G6) Storage: emptyDir (Test, Daten weg beim Neustart) oder PVC auf SNO/CRC oft erst managementState: Managed setzen jeder Push aktualisiert die Tag-Historie des ImageStream (C1) Disk: oc get pvc -n openshift-image-registry innen – zum Ziehen & für Build-Pods image-registry.openshift-image-registry.svc:5000 ein ClusterIP-Service (B3) – nur clusterintern außen – zum Schieben (opt-in) default-route-openshift-image-registry.apps.<cluster> eine Route (B4) – existiert erst nach: oc patch configs.imageregistry.operator.openshift.io/cluster \ --type=merge -p '{"spec":{"defaultRoute":true}}' Pods ziehen – ohne Extra-Secret die default- und builder-SA jedes Namespace haben ein auto-provisioniertes Pull-Secret für die interne Registry (A6/B10) cross-namespace ziehen: oc policy add-role-to-user system:image-puller ... -n <quelle> von außen schieben oc whoami -t | podman login --username kubeadmin --password-stdin \ default-route-openshift-image-registry.apps.<cluster> selbstsigniertes Zertifikat → podman push --tls-verify=false oder die Router-CA ins System-Trust legen Token läuft ab (A4) – oc whoami -t holt einen frischen pull / build-push Pruning – die Registry klein halten oc adm prune images --keep-tag-revisions=3 --keep-younger-than=60m zeigt nicht referenzierte Layer und Manifeste; erst --confirm löscht aus dem Storage dazu oc adm prune builds / deployments automatisch: der imagePruner-CronJob (wenn im Operator konfiguriert) ohne Pruning läuft die emptyDir/PVC voll → Pushes schlagen fehl Bibliothek / cloud-host der Hetzner-Server kommt nicht an diese Registry (Netz-Trennung) die 8 Dienst-Images liegen in ghcr.io (Diagrammseite Zeichnung 01) die interne Registry brauchst du nur für S2I-Output / Test-Builds (C2) wer sie extern nutzt: die Route freischalten und ein Pull-Secret verteilen Status: oc get co image-registry Merksatz: ….svc:5000 = innen (pull, build-push), Route default-route-….apps = außen (opt-in, push), Token als Passwort Pods brauchen kein Pull-Secret für die interne Registry – die SA hat schon eins OCP-Unterschied: identisch – die interne Registry ist Kern-OpenShift, in OKD und OCP gleich (Quay ist ein zusätzliches Produkt, keine Voraussetzung).
Innen und außen. Die interne Registry ist ein ClusterIP-Service unter image-registry.openshift-image-registry.svc:5000 – dahin ziehen Pods und schieben Build-Pods. Die externe Route default-route-openshift-image-registry.apps.<cluster> existiert erst nach defaultRoute: true; dahin schiebt man mit podman login (Token über --password-stdin). Pods brauchen kein Extra-Secret. oc adm prune images zeigt die Vorschau; mit --confirm wird gelöscht.
  • ein Operator (image-registry-ClusterOperator) verwaltet sie im Namespace openshift-image-registry – auf SNO/CRC steht sie per Default auf Removed
  • einschalten: oc patch configs.imageregistry.operator.openshift.io/cluster --type=merge -p '{"spec":{"managementState":"Managed","defaultRoute":true}}' plus ein Storage-Backend
  • zwei Adressen: innen image-registry.openshift-image-registry.svc:5000 (ClusterIP, zum Ziehen und für Build-Pods)
  • außen default-route-openshift-image-registry.apps.<cluster> (eine Route) – existiert erst nach defaultRoute: true, dahin schiebt man vom Laptop oder aus der CI
  • selbstsigniertes Zertifikatpodman push --tls-verify=false oder die Router-CA vertrauen
  • Anmeldung: oc whoami -t | podman login -u kubeadmin --password-stdin default-route-openshift-image-registry.apps.<cluster> – das Token landet nicht in der Shell-History
  • Pods ziehen ohne Extra-Secret: die default-/builder-SA jedes Namespace hat ein auto-provisioniertes Pull-Secret für die interne Registry (A6, B10)
  • cross-namespace ziehen: der ziehenden SA die Rolle oc policy add-role-to-user system:image-puller ... -n <quell-namespace> geben
  • Pruning: oc adm prune images --keep-tag-revisions=3 --keep-younger-than=60m zeigt eine Vorschau, --confirm erst nach Prüfung ergänzen
  • automatisch: der imagePruner-CronJob (im Operator konfiguriert) – ohne Pruning läuft der Storage voll und Pushes schlagen fehl
  • jeder Push aktualisiert die Tag-Historie des ImageStream (C1)
  • Bibliothek: die interne Registry ist das Ziel für alle 9 Images. Einmalig managementState: Managed, ein Storage-PVC (vorab als RWO anlegen – der Operator will RWX, local-path kann nur RWO), defaultRoute: true. Jenkins pusht mit dem Dauer-Token der pipeline-SA (Rollen system:image-builder + registry-editor), Cred okd-registry
  • nach sno.ps1 up hängt der Operator an einer veralteten Leader-Lease (Attempting to acquire leader lease) – der Post-up-Hook (sno-wsl.sh remediate) löscht sie, sonst bekommt der erste podman login ein connection refused
  • Status: oc get co image-registry. OCP-Unterschied: identisch – Quay ist ein zusätzliches Produkt, keine Voraussetzung
Laptop · Bash
# einschalten (Storage-PVC vorher als RWO anlegen, siehe G3):
oc patch configs.imageregistry.operator.openshift.io/cluster --type=merge \
  -p '{"spec":{"managementState":"Managed","defaultRoute":true}}'

oc get co image-registry -w

# anmelden - das Token landet nicht in der Shell-History:
HOST=$(oc get route default-route -n openshift-image-registry -o jsonpath='{.spec.host}')
oc whoami -t | podman login -u kubeadmin --password-stdin --tls-verify=false "$HOST"

# ein Image von aussen pushen:
podman tag registry.access.redhat.com/ubi9/ubi-minimal:latest "$HOST/library/handtest:v1"
podman push --tls-verify=false "$HOST/library/handtest:v1"

oc get is handtest -n library

Zwei Adressen: innen image-registry.openshift-image-registry.svc:5000 (ClusterIP, zum Ziehen und für Build-Pods), außen die Route aus defaultRoute: true – dahin schiebt man vom Laptop oder aus der CI. Das Zertifikat ist auf OKD/SNO selbstsigniert, darum --tls-verify=false oder die Router-CA vertrauen. Cross-namespace ziehen braucht eine Extra-Rolle: oc policy add-role-to-user system:image-puller system:serviceaccount:<ns>:default -n library.

8 Freigabe durch Um-Taggen

Ein Release ist nur ein Zeiger – atomar und umkehrbar, kein Rebuild.

Laptop · Bash
oc tag library/demo-build:latest library/demo-build:prod
oc get istag demo-build:prod -n library

# ein Rollback ist derselbe Befehl, rueckwaerts:
oc tag library/demo-build:v-vorherige library/demo-build:prod

Ein Deployment, das demo-build:prod referenziert (mit lookupPolicy.local: true), sieht den neuen Digest sofort – ohne Rebuild, ohne Push. Genau das nutzen Image-Trigger (C6), um einen Rollout auszulösen, sobald sich ein Tag bewegt.

9 Branch, Trigger, Umgebung

Das klassische Muster: feature/… baut nur, develop deployt nach Test/QS, main oder release/… deployt nach Prod – jeder Branch löst über denselben Webhook-Mechanismus (Schritt 5) etwas anderes aus.

feature/*Push löst höchstens Tests/Lint aus (CI außerhalb, E1) – kein BuildConfig hört auf diesen Branch, also auch kein Deploy
developein BuildConfig mit spec.source.git.ref: develop hört auf genau diesen Branch – Build → Tag :test → Deploy mit dem Test-Overlay
main / releaseein zweites BuildConfig mit spec.source.git.ref: main – Build → Tag :prod → Deploy mit dem Prod-Overlay, oft mit manueller Freigabe davor

Der Trick liegt in Schritt 5: die drei Provider-Webhooks prüfen den Branch aus dem Payload gegen spec.source.git.ref – ein Push auf feature/x trifft die Webhook-URL des develop-BuildConfigs zwar auch (dieselbe Repo-Konfiguration meldet an alle passenden Hooks), löst dort aber keinen Build aus, weil der Branch nicht passt. Ein BuildConfig pro Umgebung ist deshalb der einfachste Weg, Branch und Umgebung fest zu verdrahten – die Alternative ist ein CI-System außenrum (Jenkins Multibranch, Tekton mit einem CEL-Interceptor-Filter auf body.ref), das selbst entscheidet, welche Pipeline für welchen Branch läuft.

Konfiguration je Umgebung

Zwei übliche Wege, beide schon in dieser Anleitung gezeigt: ein ImageStream-Tag je Stufe (:test/:prod, Schritt 8 – Promotion ist Um-Taggen) und ein Helm-Values-Overlay je Stufe (values-test.yaml/ values-prod.yaml, unterscheidet DB-Host, Replica-Zahl, Ressourcen-Limits, Feature-Flags – der Chart und die Image-Digest bleiben gleich).

klassisches Branch-per-Stagewie die Bibliothek es vereinfacht
ein BuildConfig je Umgebung, gebunden an einen Branchkein BuildConfig im Cluster – Jenkins baut extern (Schritt 12)
Tag/Overlay je Stufe (:test/:prod)ein unveränderlicher Tag je Build, der Git-SHA – kein :latest, keine Stufen-Tags
Namespace- oder Cluster-pro-Stufeein Namespace library auf einer SNO
Promotion = nächstes Overlay deployen, ausgelöst durch Gitlokal (Spring dev, H2) → CRC (volles OpenShift lokal) → SNO (die Demo-Prod) – von Hand angestossen

Kein Widerspruch: das klassische Muster lohnt sich, sobald mehrere Leute an mehreren Umgebungen gleichzeitig arbeiten. Bei einem Ein-Personen-Demoprojekt mit einer SNO ist der direkte Weg schneller und ehrlicher – mehr Umgebungen vorzutäuschen als es gibt, hilft niemandem.

10 Vom Tag zum Deploy: Helm

Schritt 9 hat erklärt, welches Overlay welche Umgebung bekommt – hier folgt der Befehl, der es wirklich tut. Ein winziger Chart reicht, um die Verbindung zu zeigen.

Laptop · Bash
mkdir -p demo-build-chart/templates

cat > demo-build-chart/Chart.yaml <<'YAML'
apiVersion: v2
name: demo-build-chart
version: 0.1.0
YAML

cat > demo-build-chart/values.yaml <<'YAML'
image:
  repository: image-registry.openshift-image-registry.svc:5000/library/demo-build
  tag: latest
replicaCount: 1
YAML

cat > demo-build-chart/templates/deployment.yaml <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-build-helm
spec:
  replicas: {{ .Values.replicaCount }}
  selector: { matchLabels: { app: demo-build-helm } }
  template:
    metadata: { labels: { app: demo-build-helm } }
    spec:
      containers:
        - name: app
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports: [{ containerPort: 8080 }]
YAML

cat > values-test.yaml <<'YAML'
image: { tag: latest }
replicaCount: 1
YAML

cat > values-prod.yaml <<'YAML'
image: { tag: prod }
replicaCount: 2
YAML

# Test-Umgebung: das :latest-Image aus Schritt 4, ein Replica
helm upgrade --install demo-build ./demo-build-chart -n library -f values-test.yaml
helm list -n library

# "Promotion": derselbe Chart, das naechste Overlay, der Tag aus Schritt 8
helm upgrade --install demo-build ./demo-build-chart -n library -f values-prod.yaml
oc get pods -n library -l app=demo-build-helm

Genau das ist die Zeile aus der Tabelle in Schritt 9: derselbe Chart, nur das Overlay und der Image-Tag wechseln – kein neuer Build, kein neuer Push, nur ein anderer Zeiger und andere Werte. Das Release-Objekt (ein Secret im Namespace) trägt den Verlauf, helm rollback demo-build 1 -n library geht zurück. Das volle Modell – Release, Diff, Rollback, wo Helm aufhört – steht in helm-am-bibliothek-chart.html, diese Anleitung zeigt nur die Verbindung zur Build-Kette.

11 Aufräumen: Pruning

Jeder Push schreibt eine neue Zeile in die Tag-Historie – ohne Aufräumen läuft der Storage der Registry irgendwann voll.

Laptop · Bash
# erst eine Vorschau, nichts wird geloescht:
oc adm prune images --keep-tag-revisions=3 --keep-younger-than=60m

# nach Pruefung wirklich loeschen:
oc adm prune images --keep-tag-revisions=3 --keep-younger-than=60m --confirm

Automatisch läuft das über den imagePruner-CronJob, den der Operator konfiguriert – von Hand ist er für den seltenen Fall, dass der Storage schon eng wird.

12 Wie die Bibliothek es macht

Bausteinreal
BuildConfigkeins im Cluster – Jenkins baut die 9 Images extern mit podman build (Maven im Container gegen die Host-Engine), kein Build-Pod, keine Cluster-CPU dafür verbraucht
PushJenkins pusht mit podman push unter library/<dienst>:<git-sha> in die interne Registry – der Pfad legt automatisch einen ImageStream je Dienst an
Tag-Strategieein unveränderlicher Tag je Build (der Git-SHA) – kein :latest, darum kein Image-Trigger nötig, das Helm-Chart referenziert den SHA direkt
Rechtedie SA pipeline pusht mit einem Dauer-Token, Rollen system:image-builder + registry-editor (A5)
Bekannte Störungnach sno.ps1 up hängt der Operator oft an einer veralteten Leader-Lease – der Post-up-Hook (sno-wsl.sh remediate) räumt das, sonst bekommt der erste podman login ein connection refused

oc new-app/new-build nutzt die Bibliothek höchstens zum Skizzieren – für echten Deploy kommt Helm (B11), nicht der generierte Satz aus Schritt 2.

+ Die Kurzfassung

BuildBuildConfig = Rezept, Build = ein Lauf davon – optional, extern bauen geht genauso gut
ImageStreamZeiger auf Digests, nicht auf Bytes – löst die Tag-Falle per Image-Trigger oder unveränderlichem Tag
Registryzwei Adressen (intern Service, extern Route) – Pods ziehen ohne Extra-Secret, du pusht mit deinem Token
⌂ Cockpit