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.
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.
# 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.
| Strategie | macht |
|---|---|
| Source (S2I) | Quellcode + Builder-Image → fertiges App-Image, kein Dockerfile nötig – das assemble-Skript des Builder-Images baut |
| 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 |
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.
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.
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,
ausser 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.
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.
# 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), aussen 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.
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.
spec.source.git.ref: develop hört auf genau diesen Branch – Build → Tag :test → Deploy mit dem Test-Overlayspec.source.git.ref: main – Build → Tag :prod → Deploy mit dem Prod-Overlay, oft mit manueller Freigabe davorDer 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 aussenrum (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-Stage | wie die Bibliothek es vereinfacht |
|---|---|
| ein BuildConfig je Umgebung, gebunden an einen Branch | kein 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-Stufe | ein Namespace library auf einer SNO |
| Promotion = nächstes Overlay deployen, ausgelöst durch Git | lokal (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.
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.
# 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
| Baustein | real |
|---|---|
| BuildConfig | keins 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 |
| Push | Jenkins pusht mit podman push unter library/<dienst>:<git-sha> in die interne Registry – der Pfad legt automatisch einen ImageStream je Dienst an |
| Tag-Strategie | ein unveränderlicher Tag je Build (der Git-SHA) – kein :latest, darum kein Image-Trigger nötig, das Helm-Chart referenziert den SHA direkt |
| Rechte | die SA pipeline pusht mit einem Dauer-Token, Rollen system:image-builder + registry-editor (A5) |
| Bekannte Störung | nach 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.