← Übersicht  ·  Skripte & Dateien  ·  Diagrammseite  ·  OpenShift · Atlas im Aufbau

OpenShift-Atlas

Fortsetzung der Diagrammserie, eigene Nummerierung. Ziel: OpenShift wirklich verstehen – Breite zuerst (jedes Thema mindestens ein Diagramm), Tiefe danach. Jede Einheit ist eine Sitzung, ein Commit, ein Diagramm. Plan, Regeln und Fortschritt stehen unten auf der Seite und wandern nie in den Kopf einer Sitzung.

Farbkanon wie auf der Diagrammseite – derselbe Ton bedeutet dasselbe:

Stand: 5. September 2026 65 / 65 Diagrammen Plan: openshift-atlas-plan.md

Block 0 – Vorbereitung

01 Wie ein OKD-Single-Node auf einem Server entsteht, der kein CoreOS kennt

InfrastrukturSNOInstallationHetzner

SCOS (CentOS Stream CoreOS – das unveränderliche Container-Betriebssystem, das OKD-Node voraussetzen) gibt es bei Hetzner nicht als Image, und hochladen kann man auch keins. Der Umweg: das Rescue-System booten, per kexec in die SCOS-Live-Umgebung springen, und die installiert OKD von dort aus selbst auf die Platte.

Laptop · Windows steuert nur, rechnet nicht PowerShell · sno.ps1 ein Befehl: sno.ps1 install hcloud CLI Server, IP, Firewall anlegen WSL2 – Linux-VM im Laptop openshift-install, oc gibt es nur fuer Linux WSL nutzt dieselbe oeffentliche IP wie der Laptop – die Firewall-Regel fuer 6443 passt automatisch. bleibt ueber den Zyklus reservierte IPv4 okd-sno-ip API-Zert + nip.io haengen dran Firewall okd-sno-fw 22 + 6443 nur von deiner IP 80 + 443 offen hcloud API · HTTPS Server okd-sno · cpx52 (12 vCPU / 24 GB) 1 · Rescue-System (Ubuntu) nur Traeger. Hard-Reset, weil ein Soft-Reboot nicht in den Rescue geht. kexec Ignition als /config.ign (xz-cpio) ans initramfs 2 · SCOS-Live – bootstrap-in-place die Live-Umgebung installiert das System, das sie selbst ist, auf die Platte 2a · node-image + release-image ziehen  ·  ~5–10 min ab hier geht ssh core@ – das OS meldet „CentOS Stream CoreOS“ 2b · bootkube – temporaere Control-Plane rendert die Ignition  ·  ~6 min 2c · coreos-installer schreibt SCOS + fertige Ignition auf die Systemplatte ~1 min, dann Reboot – SSH bricht ab 2d · post-reboot: CSRs genehmigen, alle Cluster-Operatoren, CVO konvergiert ~20–30 min · oc get co zeigt den Stand 3 · fertiger OKD-Single-Node eine Maschine ist Control-Plane-Node UND Worker-Node API:6443 Console + Router:443 interne Registry+ alle Operatoren openshift-install wait-for install-complete gegen api.sno.<ip>.nip.io:6443 · bis ~45 min Die feste IP ist Voraussetzung, nicht Komfort: API-Zertifikate und die nip.io-Namen werden auf sie ausgestellt. Ohne sie kein up aus dem Snapshot. Gesamt ~30–45 min. Danach: sno.ps1 down stoppt den Node, macht einen Snapshot (~10 GB) und loescht den Server – reservierte IP und Snapshot bleiben (~0,75 EUR/Mon). sno.ps1 up erzeugt den Server neu aus dem Snapshot mit derselben IP – der Cluster ist in Minuten wieder da, samt allem was drauf war. Auf echtem OpenShift heisst das OS RHCOS (Red Hat CoreOS), und der Weg ist der Assisted / Agent-based Installer – kein kexec-Umweg, weil das Image im Katalog liegt.
Ein Server, der kein CoreOS kennt, wird über einen Umweg zu einem. Rescue booten, per kexec in die SCOS-Live-Umgebung springen, und die installiert OKD von dort aus selbst auf die Platte – Steuerungsebene und Workload auf derselben Maschine. Der Laptop steuert nur: PowerShell für hcloud, WSL für openshift-install. Die reservierte IP hält den Cluster über down/up hinweg zusammen.
  • OKD-Node brauchen SCOS oder RHCOS, ein unveränderliches Container-Betriebssystem, das der Machine Config Operator verwaltet
  • Hetzner hat kein SCOS-Image und erlaubt keinen Image-Upload, deshalb der Umweg über das Rescue-System
  • zuerst das Rescue-System (Ubuntu) booten und einen Hard-Reset machen, ein Soft-Reboot landet nicht im Rescue
  • dann per kexec in die SCOS-Live-Umgebung springen, die Ignition liegt als /config.ign (xz-cpio) im initramfs
  • bootstrap-in-place Schritt 2a: node-image und release-image ziehen (etwa 5 bis 10 Minuten), ab hier geht ssh core@
  • Schritt 2b: bootkube startet eine temporäre Steuerungsebene, die die Master-Ignition rendert (etwa 6 Minuten)
  • Schritt 2c: coreos-installer schreibt SCOS und die Ignition auf die Platte (etwa 1 Minute), danach Reboot
  • Schritt 2d nach dem Reboot: CSRs genehmigen, alle Cluster-Operatoren starten, der CVO konvergiert (etwa 20 bis 30 Minuten)
  • der Laptop steuert nur: PowerShell für hcloud, WSL2 für openshift-install und oc (die es nur für Linux gibt)
  • WSL nutzt dieselbe öffentliche IP wie der Laptop, darum passt die Firewall-Regel für Port 6443 automatisch
  • die Firewall okd-sno-fw lässt Port 22 und 6443 nur von deiner IP zu, Port 80 und 443 sind offen
  • die reservierte IPv4 okd-sno-ip trägt die API-Zertifikate und die nip.io-Namen, ohne sie läuft up nicht
  • Vorgabe-Server ist cpx52 (12 vCPU, 24 GB) – OKD belegt im Leerlauf schon etwa 11 GB RAM
  • install-config.yaml und single-node-ignition-config entstehen in der WSL, mit baseDomain: <ip>.nip.io und einem Dummy-Pull-Secret
  • der ganze Aufbau dauert etwa 30 bis 45 Minuten, danach liegt die kubeconfig unter ~/.kube/, dazu Konsolen-URL und kubeadmin-Passwort
  • sno.ps1 down stoppt den Node, macht einen Snapshot (etwa 10 GB) und löscht den Server nur bei Erfolg. IP und Snapshot bleiben (etwa 0,75 Euro im Monat)
  • sno.ps1 up baut den Server neu aus dem Snapshot mit derselben IP, in Minuten wieder da, samt Inhalt
  • sno.ps1 destroy löscht alles: Server, Snapshot, reservierte IP und Firewall
  • cluster.ps1, der Kill-Switch für den Multi-Node-Cluster, nimmt okd-sno ausdrücklich aus
  • SCOS ist ein bewegtes Ziel – OKD released schnell, ein SCOS-Build kann brechen. Getestet mit 4.22.0-okd-scos.8 (2026-08-31), bei einem Neubau die Prozedur gegen die dann aktuelle Version prüfen
  • OCP-Unterschied: dort RHCOS statt SCOS und der Assisted- oder Agent-based-Installer – kein kexec-Umweg, weil das Image im Katalog liegt
  • Aufbau sicher nachvollziehen: SNO, CRC und Deploy-Weg – Installationsphasen aus den drei Skripten und dem laufenden Cluster ableiten, ohne einen Server zu starten
Aufgabe 0 – der Rest

Snapshot/Restore: schon erledigt. sno.ps1 down macht erst den Snapshot und löscht den Server nur, wenn er gelang; up holt ihn zurück. cluster.ps1 (der Kill-Switch für den Multi-Node-Cluster) nimmt okd-sno ausdrücklich aus – der ROT-Befund aus der Analyse vom 1. September ist behoben.
--rebuild-Flag: zurückgestellt bis Block E. Ein Rebuild bootet frisch und lässt Argo CD alles aus Git ziehen – das setzt voraus, dass GitOps überhaupt eingerichtet ist. Vorher wäre es nur destroy + install von Hand.

02 CRC – volles OpenShift auf dem Laptop, und wo es aufhört

InfrastrukturCRCOpenShift LocalLaptop

CRC (CodeReady Containers / OpenShift Local) bringt ein vollständiges OpenShift in einer VM auf dem eigenen Rechner – Konsole, Operatoren, Registry, Routes. Alles, was fehlt, folgt aus dem einen, eingefrorenen Node. Der Wert ist die kostenlose, offline nutzbare, im Screen-Share vorführbare Umgebung fürs Gespräch.

Laptop · Hyper-V-VM crc setup einmal, dann crc start Dimensionierung Vorgabe: 4 vCPU · 10,5 GB · 31 GB Disk Bibliothek-Stack: crc config set memory 16384 · cpus 6 · disk-size 45 Pull-Secret von console.redhat.com danach kein Red-Hat-Abo nötig ein Node = Control-Plane + Worker wie SNO, nur lokal – kein Hetzner, kein Tailscale crc console → Web-Konsole (kubeadmin / developer) Domäne *.apps-crc.testing, API auf :6443 crc stop friert den Cluster ein – kein Snapshot offline nutzbar, kostenlos, im Screen-Share vorführbar – die „zeig mal“-Umgebung fürs Gespräch echtes OpenShift ist dabei Web-Konsole – Administrator- und Developer-Perspektive OLM + OperatorHub – Operatoren installieren (D2–D4) interne Registry – ...apps-crc.testing (C5) Routes + HAProxy-Router (B4, F3) SCC + oc adm policy, alle CRDs (A3, A7) OAuth-Stack, RBAC, Projects (A4–A6) die oc-Erfahrung ist dieselbe wie auf OKD/OCP darum eignet sich CRC zum Üben von Konsole, Operator-Installation und RBAC ohne laufende Kosten der Registry-Doppeladress-Trick (C5) gilt hier genauso nur die Domain heißt apps-crc.testing statt nip.io Abgrenzung: drei Umgebungen kind – 2+ Nodes, aber nacktes Kubernetes (B6, F1, G7 brauchen das) SNO – echtes OpenShift auf Hetzner, hinter Tailscale (der Alltag, kostet pro Stunde) CRC – echtes OpenShift lokal, offline, gratis (die Demo-Umgebung) Bibliothek: bibliothek-enterprise-auf-crc.html wo es aufhört – drei Wurzeln Wurzel 1 · ein Node Scheduling, Taints, topologySpread (B6) wirken ins Leere kein Geneve zwischen Node (F1), keine Quorum-Übung (G7) Wurzel 2 · eingefrorenes Image-Bündel oc adm upgrade gesperrt (G5), kein MCO-Rollout (G4) Red Hat liefert CRC als einen festen Stand Wurzel 3 · Laptop, keine Cloud kein Cloud-Disk-Backend – nur ein hostpath-Provisioner kein RWX, keine VolumeSnapshots (G3) Monitoring per Default aus (G1, ~3 GB RAM) bei 10,5 GB bleiben ~700 MiB für Apps – 8 JVMs kalt sprengen das → Deploy mit replicas: 0, gestaffelt hoch kein Punkt ist „OpenShift fehlt ein Feature“ – wer die drei Wurzeln kennt, leitet jede Grenze selbst her was CRC nicht ersetzt Multi-Node-Themen → lokaler kind-Cluster echter Betrieb, Last, Storage, Netz → die SNO Merksatz: CRC gibt die OpenShift-Fläche, nicht die Tiefe Windows: die VM ist Hyper-V – siehe openshift-local-auf-windows.html
Volles OpenShift, ein Node, eingefroren. CRC bringt die ganze OpenShift-Fläche auf den Laptop – Konsole, OLM, Registry, Routes, SCC, dieselbe oc-Erfahrung wie auf OKD. Jede Grenze hat eine von drei Wurzeln: der eine Node, das eingefrorene Image-Bündel, die Laptop-Umgebung ohne Cloud. Wer die drei kennt, leitet jede Einschränkung selbst her. Der Wert ist die kostenlose, offline, im Screen-Share vorführbare Umgebung fürs Gespräch – die Hetzner-SNO hinter Tailscale ist das nicht.
  • CRC (OpenShift Local) ist volles OpenShift in einer VM auf dem Laptop – crc setup einmal, dann crc start
  • Vorgabe 4 vCPU / 10,5 GB / 31 GB Disk – für den Bibliothek-Stack hochgesetzt: crc config set memory 16384, cpus 6, disk-size 45
  • ein Pull-Secret von console.redhat.com, danach läuft CRC ohne Red-Hat-Abo
  • ein einziger Node ist Steuerungsebene und Worker zugleich – wie die SNO, nur lokal und ohne Tailscale
  • echtes OpenShift ist dabei: Web-Konsole, OLM + OperatorHub, interne Registry, Routes, SCC, oc adm, alle CRDs, OAuth
  • die oc-Erfahrung ist dieselbe wie auf OKD/OCP – darum eignet sich CRC zum Üben von Konsole, Operator-Installation und RBAC
  • jede Grenze hat eine von drei Wurzeln: ein Node (B6, F1, G7) · das eingefrorene Image-Bündel (G4, G5) · Laptop statt Cloud (G1, G3, RAM-Deckel) – kein Punkt ist ein fehlendes OpenShift-Feature
  • wo es aufhört – 1 Node: Scheduling, Taints, topologySpread (B6) laufen ins Leere, kein OVN-Geneve-Tunnel (F1)
  • kein echter StorageClass – nur ein hostpath-Provisioner, kein RWX, keine VolumeSnapshots (G3)
  • oc adm upgrade ist gesperrt (G5) – die CRC-Images sind auf eine Version eingefroren, kein MachineConfig-Rollout (G4)
  • Monitoring per Default aus (G1) – enable-cluster-monitoring kostet rund 3 GB RAM, die meist fehlen
  • bei 10,5 GB bleiben ~700 MiB für Apps – 8 JVMs kalt sprengen das, darum deployt die Bibliothek mit replicas: 0 und fährt gestaffelt hoch
  • feste Domain: *.apps-crc.testing und api.crc.testing:6443, kein nip.io – crc stop friert den Cluster ein
  • die drei Umgebungen: kind = 2+ Nodes ohne OpenShift · SNO = echtes OpenShift auf Hetzner hinter Tailscale · CRC = echtes OpenShift lokal, offline, für die Vorführung
  • Merksatz: CRC gibt dir die OpenShift-Fläche, nicht die OpenShift-Tiefe – Anleitungen openshift-local-auf-windows.html und bibliothek-enterprise-auf-crc.html
  • CRC und SNO vergleichen: SNO, CRC und Deploy-Weg – API-Adresse, Node, Cluster Operators, Storage und Ressourcen mit ausschließlich lesenden Abfragen gegenüberstellen

03 Was läuft wo – die echte Topologie im Ruhezustand

InfrastrukturTopologieTailscaleCI/CD

Diagramm 01 zeigt die Installation. Das hier ist der Ruhezustand: Laptop, GitHub, Hetzner-SNO – verbunden über einen Tailscale-Tunnel, weil am Server nur 80/443 offen sind. Der lokale Jenkins in der podman-machine baut und deployt über genau diesen Tunnel.

Laptop · Windows 10 PowerShell sno.ps1 · jenkins.ps1 · cluster.ps1 steuert Hetzner per hcloud-API – nie gefirewallt WSL2 oc, openshift-install (Linux-only) hosts-Datei – der Trick 100.67.147.8 api.sno.62.238.124.7.nip.io podman-machine (WSL2) – ~16 GB / 8 CPU shared-jenkins · Port 8080 baut 9 Images gegen die Host-Podman-Engine init.groovy.d = die 17 Stolpersteine, bei jedem Boot /opt/m2cache Maven-Cache · Compose in shared-infra/ seit 2026-09-01 lokal – vorher ein eigener Hetzner-Server Tailscale-Client macht den Tunnel auf alle drei Skripte haben ein down/up per Hetzner-Snapshot GitHub nursude/LAB-WIP-...-Bibliothek-Enterprise Branch okd-deploy · read-only Deploy-Key hier liegt auch das cloud-host-Repo (dieser Atlas) Tailscale-Tunnel tailc1c4a3.ts.net trägt SSH :22 + Kube-API :6443 *.nip.io – die IP steckt im Namen, kein DNS-Server nötig Hetzner · okd-sno (cpx52) okd-sno-fw – öffentlich nur 80 + 443 reservierte IPv4 62.238.124.7 – API-Zert + nip.io-Wildcard hängen daran, überleben down/up tailscaled (statisches Binary) → 100.67.147.8 ns library 8 Spring + config-server + library-frontend Postgres · Redis · Kafka · Keycloak (Bitnami) Helm-Releases von SA pipeline (Dauer-Token) ns openshift-image-registry interne Registry – Push von aussen über die Route (80/443) openshift-* – Plattform local-path-Provisioner (RWO, am Node) – keine echte HA öffentlich auf 80/443 – die Demo-Fläche Web-Konsole · *.apps.sno.62.238.124.7.nip.io Frontend, Keycloak, api-gateway – je eine edge-Route SCOS unveränderlich – kein apt/dnf am Node API-Zert-SAN ist nur der nip.io-Name – daher der hosts-Eintrag der Deploy-Weg 1  git push → GitHub, das feuert den Trigger 2  curl-Crumb-Trigger → Jenkins buildWithParameters DEPLOY_ONLY=true 3  Jenkins baut 9 Images – Maven im Container, gegen die Host-Podman-Engine 4  podman push → Registry-Route auf 80/443 (self-signed, --tls-verify=false) 5  helm upgrade --install --wait ×9 in ns library – über den Tailnet-hosts-Trick warum so :22 und :6443 zu = kleinere Angriffsfläche – offen ist nur die Demo-Fläche auf 80/443 Kostenkontrolle: sno.ps1 und jenkins.ps1 haben je down/up per Hetzner-Snapshot Hetzner aus → der Laptop-Jenkins baut lokal weiter, nur der Deploy-Schritt wartet jede Kiste prüfbar: oc get ns · hcloud server list · tailscale status
Drei Kisten, ein Tunnel. Am Server sind öffentlich nur 80/443 offen – SSH und die Kube-API laufen über einen Tailscale-Tunnel, und ein hosts-Eintrag biegt den nip.io-API-Namen auf die Tailnet-IP, damit das Zertifikat passt. Der Jenkins steht seit September lokal in der podman-machine, baut gegen die Host-Engine und deployt per helm durch denselben Tunnel. Alles hier ist mit oc get ns, hcloud und tailscale status nachprüfbar.
  • drei Hosts: der Laptop (Windows), GitHub, der Hetzner-Server okd-sno – verbunden über ein Tailscale-Tailnet
  • am Server sind öffentlich nur 80 und 443 offen (okd-sno-fw) – das ist die Demo-Fläche (App-Routes + Web-Konsole)
  • SSH (:22) und die Kube-API (:6443) laufen über den Tailnet-Tunnel – ein Rechner ohne Tailscale kommt nicht an den Cluster
  • der hosts-Trick: das API-Zertifikat kennt nur den Namen api.sno.62.238.124.7.nip.io – der hosts-Eintrag zeigt ihn auf die Tailnet-IP 100.67.147.8, so passt der SAN
  • *.nip.io ist kein DNS-Server – die IP steckt im Namen, darum braucht die SNO keine eigene Zone
  • Jenkins läuft seit 2026-09-01 lokal in der podman-machine (Container shared-jenkins, Compose in shared-infra/) – vorher ein eigener Hetzner-Server
  • Jenkins baut gegen die Host-Podman-Engine (9 Images, Maven im Container) – init.groovy.d spielt die 17 Stolpersteine bei jedem Boot ein
  • ns library: 8 Spring-Dienste + config-server + Frontend + Postgres/Redis/Kafka/Keycloak, deployt als Helm-Releases von der SA pipeline
  • Storage: ein local-path-Provisioner (RWO, am Node) – keine echte HA, passt zur SNO
  • der Deploy-Weg: git pushcurl-Trigger → 9 Image-Builds → podman push an die Registry-Route (80/443) → helm upgrade --install --wait ×9 über den Tunnel
  • Kostenkontrolle: sno.ps1 down/up und jenkins.ps1 down/up per Hetzner-Snapshot – ausgeschaltet baut der Laptop-Jenkins lokal weiter
  • SCOS ist unveränderlich – kein apt/dnf am Node, tailscaled liegt als statisches Binary in /var/usrlocal/bin
  • alles nachprüfbar: oc get ns, hcloud server list, tailscale status – das Diagramm ist der Ist-Zustand, keine Skizze
  • Anleitungen dazu: okd-single-node-hetzner.html, jenkins-lokal-aus-hetzner-snapshot.html, bibliothek-enterprise-via-jenkins-auf-okd.html, tailscale.html
  • Topologie inventarisieren: SNO, CRC und Deploy-Weg – Kube-Kontext, Tailscale, Podman-Jenkins und Deploy-Ergebnis Schicht für Schicht nachweisen

04 Umgebungen & Konfiguration – wo alles liegt, wo man nachschaut

InfrastrukturKonfigurationUmgebungenWeb-Konsole

Die Bausteine – ConfigMap, Secret, Helm-Values, MachineConfig – sind über Block B und G verteilt. Hier die Landkarte: welche Art Konfiguration wo liegt, mit welchem oc-Befehl und über welchen Weg in der Web-Konsole man sie nachschaut – und die drei Schichten, in denen Konfiguration überhaupt passiert.

1 · die Umgebungen Ideal: vier Stufen Entwicklung Test QS Produktion je Stufe: eigener Namespace oder eigener Cluster, eigenes Helm-Values-Overlay, eigene GitOps-Application (E3) Promotion = denselben Chart mit dem naechsten Overlay deployen – der auslösende Wert (Bild-Tag, Werte-Datei) ändert sich in Git dein Projekt: drei Stufen lokal – Spring dev, H2 CRC (02) – volles OCP SNO · ns library eine SNO reicht – die vier Stufen sind auf drei zusammengelegt ein Values-Overlay: infra/helm/values/env/okd-values.yaml die SNO ist die produktionsnahe Stufe (Bitnami-Infra, echte Routes, Keycloak) – kein getrenntes Test/QS, das wäre der nächste Ausbau 2 · die Config-Landkarte – wo man nachschaut was wo es liegt oc-Befehl Weg in der Web-Konsole App-Setting (nicht geheim)ConfigMap in der nsoc get cm -n libraryWorkloads → ConfigMaps DB-Passwort, Client-SecretSecret in der ns (base64, B9)oc extract secret/x -n library --to=-Workloads → Secrets → Reveal values welche DB-URL ein Dienst nutztenv im Deployment (literal / valueFrom)oc set env deploy/x -n library --listDeployment → Reiter Environment interne Service-AdresseService (ClusterIP), DNS <svc>.<ns>.svcoc get svc -n libraryNetworking → Services externe URL (Endpunkt aussen)Route (B4)oc get route -n libraryNetworking → Routes (Spalte Location) gerenderte Helm-WerteRelease-Secret sh.helm.release.v1.* (B11)helm get values x -n libraryDeveloper → Helm → Releases → Values Frontend-Runtime-ConfigConfigMap, als env.js gemountet (J4)oc get cm library-frontend-env -o yamlWorkloads → ConfigMaps Keycloak-Realm / Client-Secretsin Keycloak selbst (Realm-Import / DB)– (Keycloak-eigene API)Keycloak-Route → Admin-Konsole → Realm Cluster: Auth, Ingress, Image, ProxyConfig-API-Objekt <res> clusteroc get authentication cluster -o yamlAdministration → Cluster Settings → Configuration Node: chrony, Kernel-Args, CertsMachineConfig (A7, G4)oc get mc / oc get mcpCompute → MachineConfigs Operator-Konfigurationdie CR des Operators (CRD-Instanz, D5)oc get <cr> -n <ns> -o yamlOperators → Installed Operators → CR-Reiter Vorsicht: die Konsole zeigt Secret-Werte im Klartext für admin/edit (nicht view) – „Reveal“ ist keine Sicherheitsgrenze (B9). Alles auf einmal: Home → Search, der YAML-Reiter. 3 · wo Konfiguration passiert – drei Schichten Quelle – bei dir, in Git Helm-Values: values.yaml + env/okd-values.yaml GitOps-Repo (E3) – die gewünschten Objekte als Text Cluster-Config: die YAML der <res> cluster-Objekte das ist, was ein Mensch schreibt und reviewt Klartext-Secrets gehören hier NICHT rein – Sealed Secrets / ESO (E5) nachschauen: das Repo, helm get values deploy Übersetzung – beim Deploy helm upgrade / oc apply / Argo-Sync rendert die Templates ConfigMap, Secret, Deployment entstehen als echte Objekte Werte aus dem Overlay landen jetzt konkret in den Objekten bei der Bibliothek: der Jenkins-Schritt helm upgrade --wait ×9 Cluster-Config: oc apply auf das cluster-Objekt nachschauen: helm get manifest, oc get -o yaml Start Wirkung – im Cluster etcd hält alle Objekte (G7) der Pod liest sie – auf zwei Wegen (B8): env / envFrom: bei Pod-Start eingefroren → Neustart nötig Mount: aktualisiert sich live (~1 min, ausser subPath) Cluster-Config: der Operator reconcilet den Sollzustand nachschauen: oc rsh in den Pod, oc get co (G6)
Konfiguration hat drei Schichten und viele Ablageorte. Was ein Mensch schreibt, liegt in Git (Helm-Values, GitOps-Repo). Der Deploy übersetzt das in ConfigMap/Secret/Deployment. Im Cluster hält etcd die Objekte, der Pod liest sie per env (eingefroren) oder Mount (live). Zum Nachschauen: die Landkarte oben – je Art ein oc-Befehl und ein Weg in der Web-Konsole. Ausführlich in okd-konfiguration-nachschauen.html.
  • die Bausteine sind verteilt – ConfigMap/Secret (B8), Secret-Details (B9, B10), Helm-Values (B11), MachineConfig (A7, G4), Operator-CR (D5). Dieses Diagramm ist die Landkarte darüber
  • drei Schichten: Quelle (Git: Helm-Values, GitOps-Repo) → Übersetzung (Deploy rendert die Objekte) → Wirkung (etcd hält, Pod liest)
  • vier Stufen im Ideal: Entwicklung, Test, QS, Produktion – je eigener Namespace oder Cluster, eigenes Values-Overlay, eigene GitOps-Application (E3)
  • Promotion = denselben Chart mit dem nächsten Overlay deployen – der auslösende Wert (Bild-Tag, Werte-Datei) ändert sich in Git, nichts wird von Hand kopiert
  • dein Projekt hat drei Stufen: lokal (Spring dev, H2) → CRC (02, volles OpenShift auf dem Laptop) → SNO ns library (produktionsnah) – kein getrenntes Test/QS, das wäre der nächste Ausbau
  • ein Values-Overlay: infra/helm/values/env/okd-values.yaml – setzt Replicas, Ressourcen, Probes, Kafka/Redis-Hosts und den Keycloak-Issuer für die SNO
  • App-Config nachschauen: oc get cm -n library und oc extract secret/<name> -n library --to=-, in der Konsole unter Workloads → ConfigMaps / Secrets
  • welche DB-URL ein Dienst benutzt: oc set env deploy/<dienst> -n library --list, in der Konsole der Reiter Environment am Deployment – zeigt auch, ob der Wert direkt steht oder aus einer ConfigMap/Secret kommt
  • Endpunkte: intern die Service-Adresse <svc>.<ns>.svc (oc get svc), extern die Route-URL (oc get route / Konsole Networking → Routes)
  • gerenderte Helm-Werte (was der Chart wirklich gesetzt hat): helm get values <release> -n library und helm get manifest, in der Konsole Developer → Helm → Releases
  • Keycloak-Realm und Client-Secrets liegen in Keycloak, nicht in OpenShift-Objekten – über die Keycloak-Route zur Admin-Konsole. Keycloaks eigenes DB-Passwort ist ein Secret in der ns
  • Cluster-Config (Auth, Ingress, Image, Proxy): die Config-API-Objekte, oc get authentication cluster -o yaml – in der Konsole Administration → Cluster Settings → Configuration
  • Node- und Operator-Config: MachineConfig (oc get mc / Compute → MachineConfigs) und die Operator-CR (oc get <cr> -o yaml / Installed Operators → CR-Reiter)
  • Vorsicht Secrets: die Konsole zeigt Werte im Klartext für admin/edit (nicht view) – „Reveal values“ ist keine Sicherheitsgrenze (B9). Ausführlich in okd-konfiguration-nachschauen.html

Block A – Fundament

A Was OpenShift zu Kubernetes hinzufügt

OKD/OCP/K8s, Project, oc, Authentifizierung, RBAC, ServiceAccount, SCC – in dieser Reihenfolge. SCC kommt zuletzt, weil es auf ServiceAccount und Project aufbaut.

A1 OKD vs OCP vs Kubernetes – welche Teile kommen obendrauf?

FundamentOKD/OCPOperatorenArchitekturEX280

Kurz: der Kubernetes-Kern bleibt unverändert. Alles, was OpenShift ausmacht, läuft als Operatoren (Controller, die eine Fähigkeit automatisieren) und zusätzliche API-Objekte darauf. OKD (die quelloffene Variante) und OCP (das kommerzielle Produkt von Red Hat) sind zwei Auslieferungen derselben Schicht.

Ein Stapel, kein Fork deine Anwendung Deployment, Service, Ingress, ConfigMap – laufen unveraendert die OpenShift-Schicht Operatoren + zusaetzliche API-Objekte – aendert den Kern nicht Router + Route OAuth-Server + oc login SCC – kein Root im Container interne Registry Web-Konsole OLM + OperatorHub Monitoring-Stack BuildConfig / ImageStream / S2I CVO + ~30 ClusterOperatoren – der Cluster verwaltet und aktualisiert sich als eine Einheit Project + Quota-Defaults · OVN-Kubernetes · DNS- und Ingress-Operator Kubernetes – unveraendert API-Server, etcd, Scheduler, Controller-Manager kubelet, CRI-O (die Container-Runtime), OVN-CNI Node-Betriebssystem SCOS bzw. RHCOS – unveraenderlich, vom Machine Config Operator verwaltet. Kein eigenes Ubuntu/Debian – das gehoert zum Vertrag. Dieselbe Schicht, zwei Auslieferungen OKD · quelloffen OCP · Red Hat Node-OS Kosten Support Operator-Kataloge Pull-Secret Update SCOS / Fedora CoreOSRHCOS kostenlosSubscription je Core Community, ForenRed Hat, SLA community-operators+ redhat- / certified- Dummy reichtechtes, console.redhat.com OKD-ReleasesRed-Hat-Channels Kern und Schicht sind praktisch gleich Im Job bedienst du meistens OCP – die Konzepte hier gelten 1:1. Der Atlas laeuft auf OKD, weil es ohne Subscription geht. Was du lernst, ist trotzdem OpenShift, nicht „nur OKD“. Was noch dazugehoert – und im Gespraech oft gefragt wird oc spricht mit jedem Kubernetes – die Extras schalten sich frei, wenn es OpenShift ist Web-Konsole: zwei Perspektiven – Administrator (Cluster) und Developer (ein Project) kubeadmin = temporaerer Notzugang · system:admin = Zertifikat in der kubeconfig, kein Login oc adm = Admin-Aufgaben: Rechte vergeben, Node leeren, Upgrades fahren oc new-app, oc debug, oc rsh, oc rollout – Abkuerzungen fuer haeufige Ablaeufe OpenShift-Objekte kommen ueber einen aggregierten API-Server – derselbe Port 6443, andere Pfade oc api-resources / oc explain route zeigen, was dieser Cluster kann OpenShift ist kein Fork von Kubernetes. Der Kern ist unveraendert – alles Zusaetzliche laeuft als Operatoren und CRDs darauf. Deployment, Service, Ingress laufen unveraendert. Eine Route kennt nur OpenShift (portabel = Ingress). oc ist ein Superset von kubectl. Die einzelnen Objekte – Project, oc/CRDs, OAuth, RBAC, ServiceAccount, SCC – klappen die Diagramme A2 bis A7 auf.
OpenShift ist Kubernetes plus eine feste Schicht Operatoren. Der Kern – API-Server, etcd, Scheduler, kubelet – ist unverändertes Kubernetes. Darauf sitzen Router, OAuth, SCC, Registry, Konsole, OLM, das Monitoring und die Build-Kette, alle als Operatoren verwaltet. OKD und OCP liefern dieselbe Schicht – sie unterscheiden sich in Node-OS, Kosten, Support und den Operator-Katalogen, nicht in den Konzepten.
  • OpenShift ist unverändertes Kubernetes, darauf eine feste Schicht aus Operatoren und CRDs, kein Fork
  • der Kern bleibt gleich: API-Server, etcd, Scheduler, Controller-Manager, kubelet, CRI-O, OVN-CNI
  • die OpenShift-Schicht sind lauter Operatoren: Router und Route, OAuth-Server, SCC, interne Registry, Web-Konsole, OLM und OperatorHub, Monitoring, dazu BuildConfig, ImageStream und S2I
  • der CVO und rund 30 ClusterOperatoren verwalten den Cluster als eine versionierte Einheit und aktualisieren ihn (G5, G6)
  • ebenfalls Teil der Schicht: Project mit Quota-Vorgaben, OVN-Kubernetes, der DNS- und der Ingress-Operator
  • das Node-Betriebssystem ist SCOS oder RHCOS, unveränderlich und vom MCO verwaltet (G4), kein eigenes Ubuntu oder Debian
  • deine Anwendung (Deployment, Service, Ingress, ConfigMap) läuft unverändert
  • OKD ist die quelloffene Variante: SCOS oder Fedora CoreOS, kostenlos, Community-Support, community-operators, Dummy-Pull-Secret
  • OCP ist das Produkt von Red Hat: RHCOS, Subscription je Core, SLA, dazu redhat- und certified-operators, echtes Pull-Secret von console.redhat.com
  • Kern und Schicht sind praktisch gleich – im Job meist OCP, die Konzepte gelten eins zu eins. Dieser Atlas läuft auf OKD (SNO auf Hetzner), OCP-Unterschiede stehen inline
  • oc ist ein Superset von kubectl – dieselbe Binary redet mit jedem Kubernetes (auch dem k3s aus biblio.yaml), die OpenShift-Extras schalten sich frei, sobald es OpenShift ist
  • oc adm bündelt Admin-Aufgaben, oc new-app, oc debug, oc rsh und oc rollout sind Abkürzungen
  • die OpenShift-Objekte kommen über einen aggregierten API-Server, gleicher Port 6443, andere Pfade – oc api-resources und oc explain route zeigen, was dieser Cluster kann
  • Folge: ein Helm-Chart mit reinen Kubernetes-Objekten läuft auf beiden. Eine Route kennt nur OpenShift – wer portabel bleiben will, schreibt stattdessen ein Ingress
  • die Web-Konsole hat zwei Perspektiven: Administrator für den Cluster, Developer für ein einzelnes Project
  • von Hand durchspielen: kern-und-authentifizierung-openshift.html – live mit oc api-resources die OpenShift-Schicht von Kubernetes trennen

A2 Project vs Namespace – was oc new-project automatisch anlegt

FundamentProjectNamespaceQuotaEX280

Ein Namespace (Kubernetes) ist nur ein Namensraum mit RBAC- und Quota-Grenze. Ein Project ist derselbe Namespace – nur angelegt über einen Weg, der gleich Rollen, ServiceAccounts und SCC-Annotationen mitbringt (eine Annotation ist eine an ein Objekt geheftete Notiz, die Werkzeuge lesen).

Namespace – plain Kubernetes · Namensraum fuer Objektnamen · Grenze fuer RBAC · Grenze fuer ResourceQuota · ein ServiceAccount „default“ · sonst nichts kubectl create namespace foo nur die Huelle – du bist darin nicht automatisch berechtigt oc get project und oc get namespace zeigen fast dasselbe – Project ist ein Namespace mit Annotationen. oc new-project foo Schritt 1 – der Weg, den ein Nicht-Admin gehen darf ProjectRequest das einzige Objekt, das du selbst anlegen darfst project-request-Template liegt in openshift-config, du kannst es aendern Template legt an: Namespace + RoleBinding „admin“ fuer dich Schritt 2 – Cluster-Controller ergaenzen (nicht das Template) · ServiceAccounts default, builder, deployer + RoleBindings (system:image-builder, system:deployer) · der SCC-Allocator schreibt aufs Namespace: openshift.io/sa.scc.uid-range · .mcs · .supplemental-groups → dein Pod bekommt daraus eine zufaellige hohe UID (z. B. 1000680000), keinen festen $HOME · Zugriff auf die SCC restricted-v2 (ueber die Gruppe system:authenticated, clusterweit) · Annotationen openshift.io/description, display-name, requester Das alles ist der Unterschied zwischen kubectl create namespace und oc new-project. NICHT automatisch da – muss ins Template oder pro Projekt · LimitRange → ohne sie starten Pods ohne CPU-/RAM-Requests · ResourceQuota → ohne sie hat das Projekt keinen Deckel · NetworkPolicy → ohne sie erreicht jeder Pod jeden (Diagramm F2) Fuer alle Projekte: ins project-request-Template. Projektuebergreifend: ClusterResourceQuota per Label-Selektor. Befehle oc new-project foo – Projekt anlegen, du wirst admin darin oc project foo – aktuellen Namespace wechseln (schreibt in die kubeconfig) oc projects – zeigt nur, was du sehen darfst · oc get namespace (als Admin) zeigt alle oc delete project foo – Namespace geht auf Terminating, Finalizer raeumen alles darin ab oc adm new-project – als Admin, umgeht Template und Selbstbedienung Im Gespraech oft gefragt · Ein Project ist keine staerkere Isolation als ein Namespace – nur Annotationen plus die automatische admin-RoleBinding. · Netz-Trennung zwischen Projekten kommt separat über NetworkPolicy – ein frisches Project ist nach aussen offen. · Terminating haengt ewig = ein Finalizer klemmt. Nachsehen: oc get ns foo -o yaml, Feld spec.finalizers. · Die Rolle self-provisioner (an system:authenticated:oauth) erlaubt jedem angemeldeten Nutzer oc new-project – Admins entfernen die Bindung oft. · oc get project vs oc get projects.project.openshift.io: dasselbe. Das kurze oc project ohne Argument zeigt den aktuellen.
Ein Project ist ein Namespace mit Startpaket. oc new-project schickt einen ProjectRequest durch ein änderbares Template (Namespace + deine admin-Rolle), danach hängen Controller die drei ServiceAccounts, die SCC-Annotationen und den restricted-v2-Zugriff an. LimitRange, ResourceQuota und NetworkPolicy sind nicht dabei – die gehören ins Template, wenn jedes Projekt sie haben soll.
  • ein Namespace in Kubernetes ist nur ein Namensraum mit RBAC- und Quota-Grenze plus ein default-ServiceAccount, sonst nichts
  • kubectl create namespace foo legt nur die Hülle an, du bist darin nicht automatisch berechtigt
  • oc new-project library erzeugt zuerst einen ProjectRequest, das einzige Objekt, das ein Nicht-Admin anlegen darf
  • daraufhin legt das project-request-Template den Namespace plus eine admin-RoleBinding für dich an (das Template liegt in openshift-config und ist editierbar)
  • danach ergänzen Cluster-Controller (nicht das Template) die ServiceAccounts default, builder, deployer samt ihren RoleBindings system:image-builder und system:deployer (A6)
  • Annotation: ein an ein Objekt geheftetes Schlüssel-Wert-Paar, das Werkzeuge und Controller lesen. Anders als ein label ist es nicht zum Filtern da, sondern zum Mitgeben von Information (darf lang sein, darf JSON enthalten). Merksatz: Labels sagen „wie man es findet“, Annotationen sind „Notizen dran, meist von Maschinen“
  • der SCC-Allocator schreibt drei Annotationen auf den Namespace (openshift.io/sa.scc.uid-range, .mcs, .supplemental-groups) – dein Pod bekommt dadurch eine zufällige hohe UID (etwa 1000680000) und kein festes $HOME (A7)
  • der Namespace erhält Zugriff auf die SCC restricted-v2, über die clusterweite Gruppe system:authenticated
  • weitere Annotationen: openshift.io/description, display-name und requester
  • nicht automatisch da: LimitRange (sonst Pods ohne Requests), ResourceQuota (sonst kein Deckel), NetworkPolicy (sonst erreicht jeder Pod jeden, F2)
  • wer das für alle Projekte will, trägt es ins project-request-Template ein. Über mehrere Projekte hinweg geht eine ClusterResourceQuota per Label-Selektor
  • Befehle: oc new-project library (du wirst admin darauf), oc project library (Namespace wechseln, schreibt die kubeconfig), oc projects, oc delete project library (geht in Terminating), oc adm new-project (als Admin, umgeht Template und Selbstbedienung)
  • ein Project ist keine stärkere Isolation als ein Namespace, nur ein paar Annotationen plus die automatische admin-RoleBinding
  • Netz-Trennung zwischen Projekten kommt separat über NetworkPolicy (F2) – ein frisches Project ist nach aussen offen
  • bleibt der Namespace ewig in Terminating, klemmt ein Finalizer (nachsehen mit oc get ns library -o yaml im Feld spec.finalizers)
  • die Rolle self-provisioner (an system:authenticated:oauth) erlaubt jedem angemeldeten Nutzer oc new-project – Admins entfernen diese Bindung oft
  • im Projekt: oc new-project library ist der erste Schritt des Bibliothek-Deploys – danach hängt Jenkins die pipeline-SA und ihre Bindungen (A5) dran und Helm installiert die Bitnami-Infra
  • von Hand durchspielen: namespace-oc-serviceaccount-openshift.html – oc new-project live verfolgen und sehen, was wirklich automatisch entsteht

A3 oc vs kubectl, API-Ressourcen, CRDs, Web-Konsole

Fundamentoc / kubectlAPIkubeconfigKonsoleEX280

Dieselbe Binary, derselbe Port 6443 – nur redet oc zusätzlich mit den OpenShift-eigenen API-Gruppen und bringt Bequem-Verben mit. Die kubeconfig sagt, mit welchem Cluster und als wer.

dein Laptop oc / kubectl dieselbe REST-Schnittstelle, HTTPS die kubeconfig beantwortet drei Fragen 1 · Wohin? – cluster server: https://api.sno.<ip>.nip.io:6443 certificate-authority: das CA-Zert des Clusters damit oc weiss: das ist wirklich dieser Cluster 2 · Als wer? – user token: sha256~aBcD… – laeuft ab (~24 h), dann neu oc login oder client-certificate – der system:admin-Weg geht bei jeder Anfrage als Bearer-Header mit 3 · Was gilt gerade? – context buendelt cluster + user + namespace: library current-context: zeigt auf einen davon oc project foo aendert nur den Namespace hier eine Datei haelt viele Cluster · oc login legt je Cluster einen Context an getrennte Dateien: $KUBECONFIG / --kubeconfig waehlt (SNO vs k3s) dieselbe Binary redet auch mit reinem k3s (biblio.yaml) – dort fehlen nur die *.openshift.io-Objekte HTTPS 6443 ein API-Server, ein Port 6443 – viele API-Gruppen core/v1 – Kubernetes-Kern Pod, Service, ConfigMap, Secret, Namespace, ServiceAccount · Pfad /api/v1 benannte Gruppen – auch Standard-Kubernetes apps/v1 Deployment · batch/v1 Job/CronJob · rbac.authorization.k8s.io Role · Pfad /apis/<gruppe>/<version> die OpenShift-Gruppen – ueber den aggregierten API-Server, selber Port route.openshift.io → Route · image.openshift.io → ImageStream · build.openshift.io → BuildConfig apps.openshift.io → DeploymentConfig · project.openshift.io → Project · user.openshift.io → User, Group security.openshift.io → SecurityContextConstraints · oauth.openshift.io → OAuthAccessToken diese Gruppen laufen ueber einen Extra-Prozess – fuer oc ist es eine Ressource wie jede andere CRD-gestuetzte Gruppen – was Operatoren mitbringen (Block D) eine CRD registriert einen Typ, dann ist kind: PostgresCluster gleichwertig zu Pod – selbe oc get-Bedienung oc api-resources listet alle Typen · oc explain route.spec.tls holt das Schema aus diesem Cluster Web-Konsole – eine eigene Route console-openshift-console.apps.<cluster> Administrator-Perspektive: Cluster, Operatoren, Nodes, Projekte, Monitoring Developer-Perspektive: ein Project, Topology-Ansicht, Add-Menue, Logs oc whoami --show-console gibt die URL · Login ueber denselben OAuth-Server wie oc login (A4) alles, was die Konsole tut, geht auch per oc – sie schreibt nur dieselben Objekte im Bibliothek-Alltag am haeufigsten oc get pods -n library – Spalte READY lesen, nicht STATUS oc get route -n library → library / keycloak / library-frontend oc logs --previous · oc get endpoints · oc rsh · oc debug node/<n> oc get <typ> -o yaml | -o wide | -o jsonpath='{…}' oc cp: unter Git Bash :// statt :/ (Pfad-Umschreibung), unter PowerShell nicht oc ist ein Superset von kubectl: alles von kubectl, dazu oc new-app, expose, rollout, rsh, debug, adm, login, project, status. Der API-Server prüft, nimmt an, benachrichtigt – er ist der einzige Punkt, an dem Rechte (A5), SCC (A7) und Protokollierung greifen. OCP-Unterschied: keiner – oc, die API-Gruppen und die Konsole sind bei OKD und OCP identisch. Die kubeconfig-Datei – wo sie liegt und wie oc sie findet Wo? Standard: ~/.kube/config (Windows: C:\Users\<du>\.kube\config) – eine Datei, kein .yaml-Ende Projekt: ~/.kube/okd-sno-hetzner.yaml (SNO) · ~/.kube/biblio.yaml (k3s) – benannt, um mehrere auseinanderzuhalten oc login <url> -u kubeadmin -p … schreibt die Datei (clusters / users / contexts + current-context) – von Hand editierst du sie nie Wie oc sie findet (Reihenfolge): 1. oc --kubeconfig <pfad> (Flag gewinnt) → 2. $KUBECONFIG (Umgebungsvariable) → 3. sonst ~/.kube/config · jeder oc-Befehl liest sie automatisch – du „rufst sie nicht auf“
Ein Endpunkt, viele Sprachen. oc und kubectl sprechen dieselbe REST-API auf 6443; die kubeconfig liefert Adresse und Identität. OpenShift hängt seine eigenen Objekttypen (Route, ImageStream, Project, SCC …) über einen aggregierten API-Server an denselben Port, und Operatoren stecken per CRD weitere dazu. oc api-resources und oc explain zeigen, was dieser Cluster kann.
  • oc ist kubectl plus die OpenShift-Verben (new-app, expose, rollout, rsh, debug, adm) und login / project
  • dieselbe Binary redet auch mit reinem Kubernetes (k3s, biblio.yaml), dort fehlen nur die *.openshift.io-Objekte
  • ein Port 6443, viele API-Gruppen: core/v1, die benannten (apps, batch, rbac und weitere), *.openshift.io und die CRD-gestützten
  • die OpenShift-Gruppen laufen über den aggregierten API-Server, einen Extra-Prozess hinter denselben Pfaden
  • eine CRD registriert einen neuen Typ – danach ist kind: PostgresCluster gleichwertig zu Pod und wird genauso mit oc get bedient (D2)
  • oc api-resources listet alle Typen (Kurzname, apiVersion, NAMESPACED, KIND), --namespaced=false filtert auf Cluster-Objekte
  • oc explain route.spec.tls gibt die Schema-Referenz direkt aus diesem Cluster
  • eine kubeconfig besteht aus drei Teilen: Cluster (Adresse und CA), User (Token oder x509-Zertifikat) und Context (welcher Cluster, welcher Namespace)
  • im Projekt: ~/.kube/okd-sno-hetzner.yaml (SNO), ~/.kube/biblio.yaml (k3s), die Variable $KUBECONFIG wählt – oc login schreibt die Datei, oc project setzt den Namespace
  • die SNO-API (api.sno.<ip>.nip.io:6443) ist nur übers Tailnet offen (Ports 22/6443 in der Firewall zu) – ein hosts-Eintrag zeigt den Namen auf die Tailnet-IP, sonst passt der Zert-SAN nicht (Diagramm 03)
  • die Web-Konsole ist eine Route (console-openshift-console.apps.<cluster>) mit zwei Perspektiven, Administrator und Developer – Adresse mit oc whoami --show-console
  • alles, was die Konsole tut, geht auch per oc – sie schreibt nur dieselben Objekte (die Klick-Wege stehen in okd-konfiguration-nachschauen.html)
  • Bibliothek-Alltag: oc get pods -n library (Spalte READY), oc get route -n library, oc logs --previous, oc get endpoints, oc rsh <pod>, oc debug node/<name>
  • oc cp unter Git Bash braucht :// statt :/ (die Umgebung schreibt Pfade um), unter PowerShell nicht
  • OCP-Unterschied: keineroc, die API-Gruppen und die Konsole sind bei OKD und OCP identisch
  • von Hand durchspielen: namespace-oc-serviceaccount-openshift.html – die API mit oc api-resources und oc explain selbst erkunden, ohne Doku

A4 Authentifizierung – Identity Provider, OAuth-Stack, htpasswd, oc login

FundamentAuthOAuthIdPhtpasswdEX280

Kubernetes hat keinen Benutzerspeicher. OpenShift bringt einen eigenen OAuth-Server mit, der gegen einen konfigurierten Identity Provider prüft und ein Bearer-Token ausstellt. kubeadmin und system:admin sind zwei Wege daran vorbei.

du am Laptop oc login -u alice --server=https://api.<cluster>:6443 Passwort interaktiv eingeben oder oc login → Browser Web-Konsole nutzt denselben Server (OAuthClient console) OAuth-Server ClusterOperator authentication · Namespace openshift-authentication eigene Route oauth-openshift.apps.<cluster> liest oauth/cluster – die einzige IdP-Konfig, clusterweit aktiver IdP: HTPasswd Secret htpass-secret in openshift-config · htpasswd -bB users alice pw Änderung → der Operator rollt den OAuth-Server neu weitere Typen: LDAP · GitHub / GitLab / Google · OpenID Connect · Keystone · RequestHeader mehrere gleichzeitig möglich – Login-Auswahl im Browser Token-Ablauf: oauth/clustertokenConfig (Vorgabe ~24 h) API-Server · 6443 jede Anfrage: Authorization: Bearer <token> validiert per TokenReview beim OAuth-Server dann greift RBAC – siehe A5 1 · well-known wo ist der OAuth-Server? 2 · Passwort gegen den IdP 3 · Token 4 · Token in die kubeconfig ↗ User-Eintrag, oc whoami -t beim ersten Login entstehen User · Identity (<idp>:<idp-user>) · UserIdentityMapping – davor kein User in etcd zwei Wege am OAuth-Server vorbei kubeadmin Secret kubeadmin in kube-system (bcrypt-Hash) – kein IdP, Voll-Admin nach eigenem IdP: oc delete secret kubeadmin -n kube-system · Passwort aus cfg/auth/kubeadmin-password system:admin x509-Client-Zertifikat in der Install-kubeconfig (cfg/auth/kubeconfig) – der API-Server prueft es selbst geht am OAuth vorbei → funktioniert auch, wenn der authentication-Operator kaputt ist ServiceAccount – paralleler Auth-Weg ↗ kein OAuth, kein User – ein Token je Pod Diagramm A6 Authentifizierung ≠ Autorisierung ↗ der IdP sagt wer, RBAC sagt was ein frischer User darf erstmal fast nichts – Diagramm A5 oc logout widerruft das Token · oc whoami --show-console · ein abgelaufenes Token sieht aus wie ein falscher (401) HTPasswd liefert keine Gruppen – Gruppen kommen per LDAP-Sync (oc adm groups sync) oder von Hand (oc adm groups) Bibliothek / cloud-host: kein eigener IdP eingerichtet – oc login -u kubeadmin fragt das Passwort interaktiv ab (CRC / SNO) ein eigener IdP (htpasswd/OIDC) wäre der Schritt, sobald mehr als eine Person am Cluster arbeitet OCP-Unterschied: identisch – nur bringt OCP zusätzlich die Möglichkeit, den Cluster gegen Red-Hat-SSO / cluster.redhat.com-Identitäten zu binden.
Ein Server für die Frage „wer bist du“. oc login tauscht Benutzer und Passwort beim OAuth-Server gegen ein Bearer-Token, das dann in der kubeconfig steht und bei jeder Anfrage mitgeht. Welchen Speicher der OAuth-Server befragt, steht in genau einem Objekt (oauth/cluster). kubeadmin ist der Notzugang ohne IdP, system:admin das Zertifikat, das auch dann noch geht, wenn der Auth-Stack streikt.
  • OpenShift bringt einen eigenen OAuth-Server mit, Kubernetes hat keinen
  • bis zum ersten Login gibt es keinen User in etcd – danach entstehen ein User, eine Identity und das Mapping dazwischen
  • IdP-Typen: HTPasswd, LDAP, GitHub, GitLab, Google, OIDC, Keystone, RequestHeader – konfiguriert in genau einem Objekt, oauth/cluster
  • HTPasswd ist ein Secret mit einer htpasswd-Datei, referenziert in oauth/cluster
  • oc login -u <user> --server=https://api.<cluster>:6443 holt ein Bearer-Token vom OAuth-Server – das Passwort wird interaktiv eingegeben, das Token läuft nach einer Weile ab und steht dann in der kubeconfig
  • kubeadmin ist temporär – ein Secret in kube-system, das man nach dem eigenen IdP löscht
  • system:admin ist ein x509-Client-Zertifikat in der Install-kubeconfig, ganz ohne OAuth
  • zwei Token-Arten: ein OAuthAccessToken für die Nutzer-Session, ein ServiceAccount-Token für einen Pod (A6)
  • Gruppen kommen nicht automatisch – per LDAP-Sync (oc adm groups sync) oder von Hand (oc adm groups new <name>)
  • Rechte kommen nicht vom IdP – erst oc adm policy bindet Rollen an einen User oder eine Gruppe (A5)
  • im Projekt ist kein eigener IdP eingerichtet – der Zugang läuft komplett über oc login -u kubeadmin -p $(cat cfg/auth/kubeadmin-password), dasselbe für CRC. Für ein Bewerbungsgespräch wäre HTPasswd der erste Ausbau
  • die SNO-API ist nur übers Tailnet erreichbaroc login von einem Rechner ohne Tailscale und ohne den hosts-Eintrag scheitert am Zertifikat, nicht am Passwort (Diagramm 03)
  • Token weg oder abgelaufen: oc whoami sagt Unauthorized – neu oc login, oder oc whoami -t zeigt das aktuelle Token
  • von Hand durchspielen: kern-und-authentifizierung-openshift.html – einen HTPasswd-IdP live anlegen, einloggen, Rechte binden

A5 RBAC – Role, RoleBinding, oc adm policy, Default-Rollen

FundamentRBACRollenoc adm policyEX280

Nach dem Anmelden (A4) weiß der Cluster wer du bist – aber noch nicht, was du darfst. Das klärt RBAC mit genau zwei Objektarten: einer Rolle (Liste erlaubter Aktionen) und einer Bindung (hängt die Rolle an eine Person, Gruppe oder einen ServiceAccount). Ein frischer User darf zuerst fast nichts.

Zwei Objekte: eine Rolle sagt WAS erlaubt ist, eine Bindung sagt WER sie bekommt. 1 · Was ist erlaubt? – Role / ClusterRole Datei: du legst an – meist schon im Helm-Chart enthalten Role – gilt in genau einem Namespace ClusterRole – clusterweit, oder als Vorlage für viele Namespaces = Liste von Regeln (rules). Jede Regel kombiniert drei Achsen: verbsget · list · watch · create · update · patch · delete resourcespods · services · routes · deployments · secrets · … apiGroups"" (core) · apps · route.openshift.io · … Beispiel-Regel – „darf Pods und ihre Logs ansehen“: - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] 2 · Wer bekommt es? – RoleBinding / ClusterRoleBinding Datei: du legst an – oder oc adm policy erzeugt sie für dich RoleBinding – im Namespace: koppelt roleRefsubjects ClusterRoleBinding – dieselbe Kopplung, clusterweit subjects – wer angesprochen wird: User aus dem IdP (A4)  ·  Group z. B. per LDAP-Sync ServiceAccountsystem:serviceaccount:<ns>:<name>  ↗ A6 so entsteht eine Bindung – ganz ohne YAML: oc adm policy add-role-to-user edit alice -n biblio → RoleBinding, das die ClusterRole edit an alice hängt – nur in biblio … add-role-to-user edit -z pipeline -n biblio -z statt Benutzername = ein ServiceAccount im selben Namespace … add-cluster-role-to-user cluster-reader bob  (→ clusterweit) roleRef Default-ClusterRoles – vom Cluster mitgeliefert (wichtig zu kennen, nicht editieren) viewnur lesen – Secrets ausgenommen editWorkloads anlegen / ändern / löschen – aber nicht RBAC, nicht Quota & LimitRange adminwie edit + RBAC, aber ohne Quota im eigenen Namespace – oc new-project gibt dem Anleger genau das cluster-adminalles, überall – der Vollzugriff (kubeadmin und system:admin haben ihn) self-provisionerdarf neue Projekte anlegen – an die Gruppe system:authenticated:oauth gebunden Maschinen-Rollen für Build & Registry: system:image-builder · system:deployer · registry-editor · registry-viewer Warum admin / edit / view mit der Zeit „mehr können“ Diese drei sind aggregiert: sie sammeln automatisch jede ClusterRole mit dem Label rbac.authorization.k8s.io/aggregate-to-edit: "true". Ein Operator bringt für seine eigenen CRDs so eine markierte ClusterRole mit. → nach der Operator-Installation kann edit plötzlich z. B. Kafka-Topics verwalten – ohne dass jemand eine Bindung angefasst hat. prüfen: oc get clusterrole -l rbac.authorization.k8s.io/aggregate-to-edit  ·  Operatoren & OLM ↗ D1 Bibliothek / cloud-host – der ServiceAccount pipeline Bindungen: wird generiert vom Deploy-Skript scripts/jenkins.ps1 Namespace library · SA pipeline – die Identität, unter der Jenkins baut und ausrollt drei RoleBindings, alle nur im Namespace library: adminHelm-Charts der 8 Dienste ausrollen system:image-builderImages in die interne Registry pushen registry-editorImageStreams und Tags pflegen oc policy add-role-to-user admin system:serviceaccount:library:pipeline -n library prüfen und wieder entziehen oc auth can-i --as alice create deploy -n biblio yes / no – simuliert alice oc adm policy who-can delete route -n biblio → wer darf das im Namespace? oc adm policy remove-role-from-user edit alice -n biblio → löscht die Bindung = Entzug (es gibt kein „deny“) oc describe clusterrole edit → welche Regeln in einer Rolle wirklich stecken Reihenfolge: erst authentifiziert (A4 – „wer bist du“), dann autorisiert (RBAC – „was darfst du“). Beides prüft der API-Server, bevor das Objekt geschrieben wird. ↗ RBAC ist rein additiv – mehrere Bindungen summieren sich, ein „verbieten“ gibt es nicht. Weniger Rechte = Bindung entfernen. SCC (A7) wird ebenfalls über RBAC vergeben: oc adm policy add-scc-to-user bindet die Spezial-ClusterRole system:openshift:scc:<name> OCP-Unterschied: keiner – reines Kubernetes-RBAC. oc adm policy ist nur Komfort über kubectl create rolebinding / clusterrolebinding.
Rolle + Bindung, mehr ist es nicht. Eine Role/ClusterRole zählt erlaubte Aktionen auf (verbs × resources × apiGroups), ein RoleBinding hängt sie an einen User, eine Group oder einen ServiceAccount – im Namespace, oder clusterweit per ClusterRoleBinding. Die Default-Rollen view/edit/admin decken den Alltag ab, oc new-project gibt dir admin auf dein Projekt. RBAC kann nur erlauben, nie verbieten – Entzug heißt: Bindung löschen.
  • es gibt zwei Objektarten: Role und ClusterRole sind die Regelliste, RoleBinding und ClusterRoleBinding sind die Zuordnung
  • eine Regel kreuzt drei Achsen: verbs (get, list, watch, create, update, patch, delete), resources und apiGroups (der leere String steht für die Kern-Gruppe)
  • eine Role gilt in einem Namespace, eine ClusterRole clusterweit oder als wiederverwendbare Vorlage
  • Subjekte sind: ein User (aus dem IdP, A4), eine Group oder eine ServiceAccount (system:serviceaccount:<ns>:<name>, A6)
  • die Default-ClusterRoles: view darf lesen (ohne Secrets), edit darf Workloads ändern und Secrets lesen (ohne RBAC), admin darf zusätzlich RBAC im Namespace (aber nicht Quota), cluster-admin darf alles
  • oc new-project legt eine admin-RoleBinding auf den Anleger an (A2) – darum darfst du im eigenen Projekt sofort fast alle normalen Namespace-Ressourcen verwalten
  • eine Rolle binden: oc adm policy add-role-to-user <role> <user> -n <ns>, -z <sa> statt des Namens für eine ServiceAccount, add-cluster-role-to-user für clusterweit
  • aggregierte Rollen: admin, edit und view sammeln automatisch jede ClusterRole mit dem Label aggregate-to-*, so erweitern Operatoren sie von selbst
  • RBAC ist rein additiv, es gibt kein „deny“ – Entzug geht über remove-role-from-user oder das Löschen der Bindung
  • prüfen mit oc auth can-i --as <user> <verb> <resource>, oc adm policy who-can <verb> <resource> oder oc describe clusterrole edit
  • Bibliothek: die SA pipeline in library bekommt drei Bindungenadmin (Helm-Deploy), system:image-builder und registry-editor (Push in die interne Registry). Sonst nichts (E1, B12)
  • bei einem kaputten Jenkins-Deploy ist oc auth can-i --as=system:serviceaccount:library:pipeline <verb> <resource> -n library die schnellste Antwort auf „darf die pipeline-SA das“
  • auch die SCC wird über dieselbe Mechanik vergeben: add-scc-to-user bindet die ClusterRole system:openshift:scc:<name> (A7)
  • von Hand durchspielen: rbac-und-scc-openshift.html – view/edit/admin live vergleichen, can-i/who-can, RBAC additiv, dann die SCC-Auswahl live gewinnen

A6 ServiceAccount – welche Identität bekommt ein Pod?

FundamentServiceAccountTokenIdentitätEX280

Ein Mensch meldet sich per oc login an (A4). Ein Pod kann das nicht – er braucht trotzdem eine Identität, sobald er mit dem API-Server redet. Diese Identität ist eine ServiceAccount: ein „Benutzer für Maschinen“, an den man genauso Rollen (A5) und SCCs (A7) bindet, nur ohne IdP und ohne Passwort.

Ein Pod kann sich nicht anmelden – er ist eine ServiceAccount und trägt deren Token bei jedem API-Aufruf. UserMenschen, aus dem IdP (A4) ↗ GroupSammlung von Usern ServiceAccount für Prozesse / Pods · system:serviceaccount:<ns>:<name> alle drei können Subjekteiner RoleBinding sein(A5) ↗ Namespace library default/builder/deployer: wird generiert · pipeline: du legst an (oc create sa) default builder deployer pipeline Pod catalog-7f9c… spec.serviceAccountName: default fehlt das Feld → default. Jede App-Pod hat immer eine SA. automountServiceAccountToken: false → kein Token gemountet Deploy-Pods läuft der Cluster als builder / deployer eine SA ist namespaced – sie existiert nur hier, im RBAC steht immer der volle Name system:serviceaccount:library:pipeline anlegen: oc create sa pipeline -n library projiziertes Volume im Pod /var/run/secrets/kubernetes.io/serviceaccount/ token kurzlebiges JWT (Bound Token) ca.crt traut dem API-Server-Zertifikat namespace der eigene Namespace als Text Label: wird generiert vom kubelet – nicht anfassen Client-Libs (client-go) lesen den Pfad von allein token ist ein JWT – sub = die volle SA-Kennung API-Server · 6443 Pod sendet Authorization: Bearer <token> prüft das SA-Token selbst (signiert vom Cluster) dann greift RBAC (A5) ↗ eine frische SA darf nichts – wie ein neuer User Token weg / abgelaufen → 401, wie ein falsches oc create token pipeline holt eins on demand die App ruft die API mit dem Token 1 · RBAC-Rechte (A5) per Default: keine oc adm policy add-role-to-user edit -z pipeline -n library -z = SA in diesem Namespace → RoleBinding mit der SA als Subjekt Bibliothek: pipeline bekommt admin + system:image-builder (aus A5) 2 · SCC (A7) ↗ oc adm policy add-scc-to-user anyuid -z pipeline bestimmt den erlaubten securityContext des Pods nicht die SCC des Users zählt, sondern die der SA, unter der der Pod läuft darum ist die SA der Ort für Root-Ausnahmen 3 · imagePullSecret (B10) ↗ oc secrets link pipeline <secret> --for=pull womit der Pod Images zieht die default-SA hat schon eins für die interne Registry --for=mount macht ein Secret automatisch mountbar 4 · builder / deployer builder: push in die interne Registry (system:image-builder) deployer: steuert DeploymentConfig-Rollouts (B2) beide vom Cluster je Namespace angelegt – OpenShift-eigen reines Deployment (B1) nutzt sie nicht Das Token: früher vs heute früher (bis OCP 4.15) je SA ein Dauer-Secret kubernetes.io/service- account-token in etcd, lief nie ab heute (Bound Token) kubelet holt es über die TokenRequest-API, projiziert es · ~1 h · audience-gebunden · rotiert selbst statischer Token nur für externe Nutzer (CI): oc create token pipeline --duration=8760h extern: Jenkins authentifiziert sich als system:serviceaccount:library:pipeline mit dessen Token (A5 · B11 · E1) ↗ prüfen: oc auth can-i --as=system:serviceaccount:library:pipeline list pods -n library · oc describe sa pipeline SA als OAuth-Client (selten): Annotation serviceaccounts.openshift.io/oauth-redirecturi.<name> – für Apps, die Nutzer-Login weiterreichen OCP-Unterschied: OpenShift ergänzt builder + deployer je Namespace und die OAuth-Client-Annotation – der Rest ist Kubernetes.
Ein Pod ist immer „jemand“. Diese Identität ist eine ServiceAccount. Der Pod bekommt ihr Token als projiziertes Volume (token, ca.crt, namespace), schickt es als Bearer-Header an den API-Server, und ab da entscheidet RBAC (A5). Eine frische SA darf nichts – man bindet ihr Rollen mit -z, eine SCC mit add-scc-to-user, ein Pull-Secret mit oc secrets link. Das Token ist heute kurzlebig und rotiert von selbst, der alte Dauer-Token in etcd ist Geschichte.
  • es gibt drei Identitätsarten: User (aus dem IdP, A4), Group und ServiceAccount (system:serviceaccount:<ns>:<name>). Alle drei sind RBAC-Subjekte (A5)
  • jeder Namespace bekommt automatisch default, builder, deployerbuilder und deployer sind OpenShift-eigen
  • ein Pod ohne serviceAccountName läuft als defaultjeder App-Pod hat immer eine SA
  • der Pod bekommt ein projiziertes Volume unter /var/run/secrets/kubernetes.io/serviceaccount/ mit den Dateien token, ca.crt und namespace
  • das Bound Token ist kurzlebig (etwa eine Stunde) und an eine audience gebunden – der kubelet holt es über die TokenRequest-API und rotiert es. Es wird generiert, nicht anfassen
  • früher (bis OCP 4.15) hatte jede SA ein nie ablaufendes Token-Secret (kubernetes.io/service-account-token) in etcd
  • einen statischen Token braucht man nur für externe Nutzer: oc create token <sa> --duration=<dauer> oder ein Secret von Hand (das du anlegst)
  • im Projekt: genau so ein Secret ist pipeline-token in library (Annotation kubernetes.io/service-account.name: pipeline) – Jenkins nutzt es als podman login-Passwort und in KUBECONFIG_PROD. Es verschwindet nach jedem sno.ps1 up, der Post-up-Hook legt es neu an
  • automountServiceAccountToken: false an SA oder Pod, wenn der Pod die API gar nicht ruft (die meisten App-Pods)
  • eine SA startet mit keinen Rechten – Rollen bindet man mit oc adm policy add-role-to-user edit -z <sa> -n <ns> (-z steht für ServiceAccount)
  • die SCC kommt von der SA, nicht vom Useroc adm policy add-scc-to-user anyuid -z <sa> steuert den erlaubten securityContext (A7)
  • Pull-Secret anhängen mit oc secrets link <sa> <secret> --for=pull – die default-SA hat schon eins für die interne Registry (B10)
  • builder darf in die interne Registry pushen (Rolle system:image-builder), deployer steuert DeploymentConfig-Rollouts
  • von aussen meldet sich Jenkins als system:serviceaccount:library:pipeline mit dessen Token an (A5, B12, E1)
  • prüfen mit oc auth can-i --as=system:serviceaccount:<ns>:<sa>, oc describe sa <sa> oder oc create token <sa>
  • von Hand durchspielen: namespace-oc-serviceaccount-openshift.html – eine eigene ServiceAccount anlegen, binden und mit can-i pruefen

A7 SCC (Security Context Constraints) – warum ein Container nicht als root läuft

FundamentSCCSecurityrootAdmissionEX280

Eine SCC ist eine OpenShift-Admission, die beim Erstellen eines Pods prüft, was sein securityContext darf – und Felder, die der Pod offen lässt, selbst setzt (vor allem die UID). Welche SCC ein Pod bekommt, hängt daran, welche SCCs seine ServiceAccount (A6) per RBAC (A5) nutzen darf. Die Vorgabe restricted-v2 verbietet Root – das ist die häufigste Starthürde bei fremden Images.

Die Frage ist nie „darf der Pod Root?“ – sondern „welche SCC lässt Root zu, und darf die ServiceAccount diese SCC nutzen?“ 1 · Pod wird erstellt von Deployment / DC / oc run – mit einer SA der securityContext ist teils leer 2 · welche SCCs? Admission sammelt alle SCCs, die die ServiceAccount (A6) per RBAC (A5) nutzen darf direkt + über Gruppen 3 · sortieren Priorität absteigend, dann Restriktivität aufsteigend (streng vor locker) 4 · erste passende erste SCC, die den Pod zulässt – notfalls mit Mutation: UID, fsGroup, SELinux-Label setzen 5 · festschreiben Name → Pod-Annotation openshift.io/scc wird generiert nicht anfassen Wenn Schritt 2 sonst nichts findet: jeder authentifizierte Account hat restricted-v2 über die Gruppe system:authenticated – das ist die Vorgabe. SCC prüft den Pod, nicht den Container einzeln – alle Container müssen in dieselbe SCC passen. restricted-v2 – die Vorgabe für alle Pods SCC-Objekt: wichtig zu kennen – vom Cluster verwaltet, eigene SCC nur mit Bedacht runAsUser: MustRunAsRange – zufällige UID, kein Wunsch-UID   aus der Namespace-Annotation openshift.io/sa.scc.uid-range (z. B. 1000700000/10000)   die Annotation entsteht bei oc new-project (A2) privileged: false · allowPrivilegeEscalation: false requiredDropCapabilities: ALL – nur NET_BIND_SERVICE nachrüstbar seccompProfile: RuntimeDefault – v2 verlangt das explizit kein hostPath / hostNetwork / hostPID / hostIPC / hostPort SELinux MustRunAs · fsGroup / supplementalGroups ebenso aus Namespace-Annotationen readOnlyRootFilesystem: nicht erzwungen, aber empfohlen (H2) der Unterschied zu restricted (v1): v2 fordert seccomp + allowPrivilegeEscalation:false ausdrücklich wird seit OCP 4.11 automatisch statt restricted vergeben die eingebauten SCCs – locker ↓ streng privileged alles: Root, host-NS, alle Caps – nur Cluster anyuid UID aus dem Image (auch 0), kein privileged/host hostmount-anyuid + hostPath – Backup / Storage hostnetwork-v2 + hostNetwork / hostPort nonroot-v2 beliebige UID, nur nicht 0 – der häufigste Fix restricted-v2 Vorgabe (siehe links) die Varianten ohne -v2 sind die alten v1-SCCs, für Altlasten Operatoren bringen oft eigene mit (node-exporter usw.) oc get scc listet alle · oc describe scc <name> zeigt die Regeln Reihenfolge = Auswahl in Schritt 3: die strengste passende gewinnt Warum startet mein Container nicht? – die vier Klassiker Image hat USER 0 / schreibt nach /etc /varPermission denied (EACCES) App bindet Port < 1024 → ohne NET_BIND_SERVICE nicht erlaubt – auf 8080 lauschen Image erwartet festen UID + macht chown → scheitert unter zufälliger UID „container has runAsNonRoot and image will run as root“CrashLoopBackOff Fixes in dieser Reihenfolge: 1  Image UID-agnostisch bauen: Dateien gehören Gruppe root, chmod g=u, hoher Port – Red-Hat-UBI ist so 2  Bitnami / Community-Chart: securityContext-Overrides oder global.compatibility.openshift.adaptSecurityContext=auto 3  letzter Ausweg: lockere SCC an genau die eine SAnonroot-v2 reicht meist, anyuid nur bei festem Nicht-Root-UID, nie privileged als Reflex Bibliothek / cloud-host die 8 Spring-Dienste laufen unverändert unter restricted-v2   (die JVM ist die UID egal, lauscht auf 8080) Keycloak / Postgres / Redis / Kafka (Bitnami): adaptSecurityContext   -Flag oder eine lockere SCC an ihre SA was konkret bricht, wenn eine EJB/WAS-App UID 0 erwartet → H1 ↗ der local-path-provisioner-SA bekommt hostmount-anyuid (SNO-Setup) zuweisen + PodSecurity Admission oc adm policy add-scc-to-user nonroot-v2 -z <sa> -n <ns> → RoleBinding auf ClusterRole system:openshift:scc:<name> – das ist RBAC (A5) ↗ besser als add-scc-to-group: nur die eine SA, nicht alle PodSecurity Admission (Kubernetes) läuft parallel: pod-security.kubernetes.io/enforce: restricted|baseline|privileged OpenShift synct das Namespace-Label meist selbst aus den nutzbaren SCCs prüfen: welche SCC ein Pod bekam → oc get pod <p> -o jsonpath='{.metadata.annotations.openshift\\.io/scc}' Merksatz: nicht „darf der Pod X“ – sondern „welche SCC lässt X zu, und darf die SA (A6) diese SCC nutzen (A5)“ OCP-Unterschied: keiner – SCC ist Kern-OpenShift, auch in OKD. SELinux-Typ container_t auf SCOS / RHCOS.
Kein Root, keine Wunsch-UID – per Vorgabe. Die SCC-Admission wählt für jeden Pod die strengste SCC, die seine ServiceAccount nutzen darf und die den Pod (ggf. nach Mutation der UID) zulässt, und schreibt sie in openshift.io/scc. restricted-v2 gilt für alle: zufällige UID, privileged: false, alle Capabilities weg, kein Host-Zugriff. Startet ein Image nicht, ist fast immer das Image schuld (will Root, fester UID, kleiner Port) – der Fix ist das Image, nicht reflexhaft eine lockere SCC.
  • eine SCC ist eine OpenShift-Admission für den securityContext – sie begrenzt ihn und setzt offene Felder selbst. Älter als das Kubernetes-PodSecurity
  • die Auswahl: die Admission nimmt alle SCCs, die die ServiceAccount (A6) per RBAC (A5) nutzen darf, sortiert nach Priorität absteigend und dann Restriktivität aufsteigend, und nimmt die erste, die passt
  • welche SCC gewonnen hat, steht in der Pod-Annotation openshift.io/scc (sie wird generiert)
  • die Vorgabe restricted-v2 gilt für jeden authentifizierten Account, über die Gruppe system:authenticated
  • restricted-v2 setzt eine zufällige UID (runAsUser: MustRunAsRange), privileged: false, wirft alle Capabilities weg, verlangt seccomp RuntimeDefault und erlaubt keinen Host-Zugriff
  • UID, MCS-Label und fsGroup kommen aus den Namespace-Annotationen openshift.io/sa.scc.*, gesetzt bei oc new-project (A2)
  • die eingebauten SCCs von locker nach streng: privileged, anyuid, hostmount-anyuid, hostnetwork-v2, nonroot-v2, restricted-v2
  • Unterschied restricted (v1) zu restricted-v2: v2 verlangt seccomp und allowPrivilegeEscalation:false ausdrücklich
  • typische Startfehler: das Image hat USER 0 und schreibt ins RootFS, bindet einen Port unter 1024, erwartet einen festen UID und macht chown, oder gerät in den runAsNonRoot-Konflikt
  • Fix in dieser Reihenfolge: erstens das Image UID-agnostisch bauen (UBI ist es schon), zweitens ein Chart-Override oder adaptSecurityContext=auto, drittens als letztes eine lockere SCC an genau die eine SA
  • zuweisen mit oc adm policy add-scc-to-user nonroot-v2 -z <sa> -n <ns> – das erzeugt eine RoleBinding auf die ClusterRole system:openshift:scc:<name> (A5)
  • Bibliothek: die 8 Spring-Dienste passen unter restricted-v2, die Bitnami-Images brauchen global.compatibility.openshift.adaptSecurityContext=auto (Keycloak, Postgres, Redis, Kafka)
  • library-frontend läuft als nginx-unprivileged – trotzdem braucht das Image chmod g+w /etc/nginx/conf.d, weil der Entrypoint die Config zur Laufzeit schreibt und die zufällige UID sonst nicht darf
  • von Hand durchspielen: rbac-und-scc-openshift.html – ein Pod scheitert unter restricted-v2 und gewinnt live eine andere SCC
  • Legacy-Vertiefung: was bei einer EJB- oder WAS-App bricht, die UID 0 erwartet, steht in H1

Block B – Workloads laufen lassen, konfigurieren, ausliefern

B1 Pod → ReplicaSet → Deployment – die Kette in einem Bild

WorkloadsPodReplicaSetDeploymentRollout

Du legst ein Objekt an – ein Deployment – und beschreibst darin nur den Sollzustand: welches Image, wie viele Kopien. Drei ineinandergreifende Regelkreise machen daraus laufende Container. Ein Pod wird dabei nie repariert, sondern immer ersetzt.

Ein Objekt beschreibt das Soll – drei Regelkreise erzeugen das Ist. Deployment catalog du legst an – oder ein Helm-Chart (B11) spec.replicas: 2 – wie viele Kopien spec.selector – welche Pods gehören dazu   nach dem Anlegen unveränderlich spec.template – die Pod-Vorlage   Image, Ports, Ressourcen, Probes (B5), env (B8) spec.strategy – RollingUpdate | Recreate declarative: du änderst die Vorlage, nicht die Pods status.readyReplicas zeigt den Ist-Stand Replicas setzt: oc scale, ein HPA (G1) oder das Helm-Value Deployment = Kubernetes · DeploymentConfig = alt (B2) ReplicaSet catalog-6c9fb8d7f4 wird generiert vom Deployment – nicht direkt bearbeiten hält genau N identische Pods – stirbt einer, macht es einen neuen Auswahl über Labels, nicht über Namen der Hash der Pod-Vorlage (pod-template-hash) steht im RS-Namen und als Pod-Label – so bleibt der Selector stabil replicas 2 / 2 ready ReplicaSet catalog-5f7a2b1c (alt) replicas 0 – bleibt für den Rollback stehen revisionHistoryLimit (Vorgabe 10) alte RS werden aufgehoben Pods wird generiert vom RS – flüchtig, nie repariert catalog-6c9fb8d7f4-abc12 catalog-6c9fb8d7f4-xk9p2 je 1 IP · 1+ Container teilen Netzwerk (localhost) + Volumes Pod tot / Node weg → endgültig weg, das RS macht Ersatz verwaltet RS hält N Pods Der Kern von Kubernetes: jeder Controller läuft eine Schleife – Ist beobachten → mit Soll vergleichen → handeln. Deployment schaut auf ReplicaSets, das RS auf Pods, der Scheduler verteilt Pods auf einem Nodes, der kubelet startet die Container. Niemand „deployt“ – alle gleichen nur ab. Rolling Update – wie ein neues Image ausgerollt wird neues Image in spec.template → Deployment legt ein neues RS an → neues hoch, altes runter, Pod für Pod maxSurge 25% – wie viele Pods über Soll erlaubt · maxUnavailable 25% – wie viele unter Soll erlaubt ein neuer Pod zählt erst als „verfügbar“, wenn seine readiness-Probe grün ist (B5) – darum blockiert eine kaputte Probe den Rollout strategy: RollingUpdate (Vorgabe) vs Recreate – erst alle alten weg, dann neue (wenn zwei Versionen nicht parallel laufen dürfen) Ablauf: [alt 2] → [alt 2 + neu 1] → [alt 1 + neu 2] → [alt 0 + neu 2] ✓ Bibliothek: neuer Image-Tag aus dem Helm-Value → neues RS catalog-<neuer-hash>, altes auf 0 Rollout steuern oc rollout status deploy/catalog oc rollout history / undo [--to-revision=N] oc rollout pause / resume deploy/catalog oc rollout restart deploy/catalog restart = neuer Rollout, gleiche Vorlage → liest ConfigMaps/Secrets neu (B8) ↗ undo schaltet zurück auf das alte RS – deshalb bleibt es stehen „der Pod kommt immer wieder“ oc delete pod catalog-6c9fb8d7f4-abc12 → das RS zählt 1/2 und macht sofort einen neuen wirklich weg: oc delete deploy/catalog   kaskadiert über ownerReferences: Deployment → RS → Pod oder oc scale deploy/catalog --replicas=0 (nur pausieren) Geschwister-Controller – gleiche Idee, anderer Zweck DaemonSet ein Pod je Node (Logging, Monitoring) · StatefulSet feste Identität + eigenes PVC je Pod (Datenbanken, B3/Storage) Job läuft einmal bis fertig (Migration) · CronJob Job nach Zeitplan (Backup) · alle nutzen dieselbe Pod-Vorlage-Mechanik Deployment (Kubernetes) vs DeploymentConfig (OpenShift-Altbestand): Trigger, Hooks, wann welches → B2 ↗ merke: einen Pod löschen ändert nie den Sollzustand – nur ein Ändern von Deployment oder RS-replicas tut das OCP-Unterschied: keiner beim Deployment – reines Kubernetes. Nur DeploymentConfig ist OpenShift-eigen (B2).
Drei Regelkreise, ein Sollzustand. Das Deployment hält die Pod-Vorlage und die Wunsch-Zahl. Es erzeugt ein ReplicaSet (Name = Vorlagen-Hash), das genau so viele Pods am Leben hält und jeden Ausfall ersetzt. Bei einer neuen Vorlage legt das Deployment ein zweites RS an und schiebt die Last hinüber (Rolling Update), das alte bleibt für oc rollout undo stehen. Einen Pod zu löschen ändert nichts – er kommt sofort zurück.
  • ein Pod ist die kleinste Einheit: ein oder mehr Container mit gemeinsamer IP, localhost und Volumes – flüchtig, wird nie repariert, nur ersetzt
  • ein ReplicaSet hält genau N Pods am Leben, die Auswahl läuft über einen selector auf Labels (nach dem Anlegen unveränderlich)
  • ein Deployment verwaltet ReplicaSets und gibt dir declarative Updates: du änderst die Vorlage, es macht den Rest
  • eine neue Vorlage führt zu einem neuen ReplicaSet mit neuem pod-template-hash (im RS-Namen und als Pod-Label). Das alte RS bleibt stehen (revisionHistoryLimit, Vorgabe 10)
  • beim Rolling Update steuern maxSurge und maxUnavailable das Tempo (wie viele Pods über bzw. unter Soll, Vorgabe je 25 %)
  • zwei Strategien: RollingUpdate (Vorgabe) oder Recreate (erst alle alten weg, dann die neuen)
  • ein neuer Pod zählt erst als verfügbar, wenn seine readiness-Probe grün ist (B5) – eine kaputte Probe blockiert den Rollout
  • steuern mit oc rollout und seinen Unterbefehlen status, history, undo [--to-revision=N], pause, resume, restart – jeweils mit deploy/<n> als Ziel
  • oc rollout restart deploy/<n> startet einen neuen Rollout mit gleicher Vorlage und liest dabei ConfigMaps und Secrets neu (B8)
  • ein gelöschter Pod kommt sofort zurück – wirklich weg über oc delete deploy/<n> (kaskadiert nach unten) oder oc scale --replicas=0
  • die Besitzkette läuft über ownerReferences: Deployment besitzt ReplicaSet, ReplicaSet besitzt die Pods (die Garbage Collection räumt von oben)
  • die Zahl der Replicas setzt oc scale deploy/<n>, ein HPA (Autoscaler, G1) oder das Image-/Replica-Value im Helm-Chart (B11)
  • verwandte Objekte: DaemonSet (ein Pod je Node – so läuft Vector G2 und ovnkube-node F1), StatefulSet (feste Identität + eigenes PVC je Pod – so laufen Postgres, Redis und Kafka aus den Bitnami-Charts), Job/CronJob
  • Bibliothek: die 8 Fachdienste + api-gateway + config-server sind einfache Deployments. Beim Deploy setzt Jenkins --set image.tag=$GIT_SHA (B11) → neue Pod-Vorlage → neues ReplicaSet mit neuem pod-template-hash, das alte bleibt für oc rollout undo deploy/<dienst> stehen
  • das Deployment ist reines Kubernetes – die OpenShift-Variante DeploymentConfig steht in B2
  • von Build bis Pod durchspielen: Build bis Rollout – Besitzkette und zwei neue ReplicaSet-Versionen live nachweisen

B2 Deployment vs DeploymentConfig – Trigger, Lifecycle-Hooks, wann welches?

WorkloadsDeploymentDeploymentConfigTriggerHooksEX280

Beide machen dasselbe – aus einer Pod-Vorlage laufende, ersetzbare Pods (B1). Der Unterschied: das Deployment ist reines Kubernetes und hält sich zurück, der DeploymentConfig ist OpenShift-eigen, älter und bringt eingebaute Trigger und Hooks mit – wird aber ausgephast. Für alles Neue: Deployment.

Gleiches Ergebnis – der Unterschied ist, wer den Rollout fährt und welche Trigger eingebaut sind. Deployment – für alles Neue DeploymentConfig – Altbestand, wird ausgephast was es ist Kubernetes-Standard · apps/v1 OpenShift-eigen · apps.openshift.io/v1 Kind-Objekt ReplicaSet → Pod (B1) ReplicationController → Pod (das Vor-RS-Objekt) wer fährt den Rollout der kube-controller-manager selbst, in-process – kein extra Pod ein deployer-Pod <dc>-<n>-deploy, läuft als ServiceAccount deployer (A6) ↗ – langsamer, kann klemmen Update-Strategie RollingUpdate (maxSurge/maxUnavailable) · Recreate Rolling · Recreate · Custom (eigenes Deployer-Image) Trigger (eingebaut) keine – du änderst spec.template   oc apply / Helm / oc set image → Rollout   „bei neuem Image neu ausrollen“:   Annotation image.openshift.io/triggers (C6) ↗ ImageChange – beobachtet einen ImageStreamTag (C1),   neues Image → automatischer Rollout ConfigChange – jede Vorlagenänderung löst aus   (beim Deployment implizit, hier muss es deklariert sein) Lifecycle-Hooks keine   stattdessen: Helm-Hooks · initContainers · ein Job pre / mid (nur Recreate) / post   Kommando in einem Pod (DB-Migration, Cache) · failurePolicy manuell / Rollback oc rollout undo / restart / pause · oc scale · HPA oc rollout latest dc/<n> · retry · Zähler latestVersion, RCs <n>-1/-2 Verdikt jedes neue Chart / Manifest – GitOps-tauglich, schnell nur noch anfassen, was schon DC ist Warum DC ausgephast wird kein server-side apply · sperrig mit GitOps (Argo CD, E3) · der deployer-Pod ist langsam und kann bei Fehlern hängen bleiben kein sauberes maxSurge · Skalier-Races zwischen DC-Controller und HPA · ReplicationController kann keine set-based Selektoren seit OCP 4.14 legt oc new-app Deployments an (früher DC) · DC bleibt unterstützt, aber ohne neue Features von DC nach Deployment umschreiben 1  DeploymentConfigDeployment, ReplicationController-Reste weg 2  ImageChange-Trigger → Annotation image.openshift.io/triggers (C6) 3  pre/post-Hooks → Helm-Hook-Jobs oder initContainers 4  Custom-Strategie → Tekton-Pipeline (E2) oder Argo Rollouts Werkzeug: oc hat keinen Auto-Konverter – Community-Skripte, sonst von Hand beide können im selben Cluster / Namespace koexistieren wo dir DC noch begegnet alte Projekte & oc new-app aus OCP < 4.14 manche Red-Hat-Operanden, Samples-Operator-Templates die EX280-Prüfung fragt DC weiterhin ab Bibliothek: alle 8 Dienste sind Deployment (Helm B11)DCs eher im Novaris-Legacy-Block (H6) ↗ oc get dc,deploy -A zeigt, was ein Cluster nutzt gemeinsam: Selector, Pod-Vorlage, die Rolling/Recreate-Idee, ein Versionsverlauf zum Zurückschalten (B1) Kernsatz: Deployment ist passiv (du/Helm löst aus), DeploymentConfig ist aktiv (beobachtet Image + Config selbst) OCP-Unterschied: DeploymentConfig gibt es nur in OpenShift/OKD – Deployment verhält sich überall gleich.
Passiv vs aktiv. Ein Deployment tut nichts von selbst – du (oder Helm) änderst die Vorlage, der Controller gleicht ab. Ein DeploymentConfig beobachtet einen ImageStreamTag und seine eigene Config und rollt selbst aus, über einen eigenen Deployer-Pod, mit pre/mid/post-Hooks. Das war früher praktisch, passt aber schlecht zu GitOps – darum baut man Neues als Deployment plus Trigger-Annotation und ersetzt die Hooks durch Jobs.
  • das Deployment ist Kubernetes-Standard (apps/v1), gefahren vom kube-controller-manager in-process
  • der DeploymentConfig ist OpenShift-eigen und wird ausgephast (apps.openshift.io/v1) – neu anlegen immer ein Deployment
  • das Kind-Objekt: Deployment → ReplicaSet (B1), DC → ReplicationController
  • den Rollout fährt das Deployment im Controller selbst, der DC über einen eigenen deployer-Pod (<dc>-<n>-deploy, SA deployer, A6) – langsamer, klemmt eher
  • eingebaute DC-Trigger: ImageChange (ein neuer ImageStreamTag löst automatisch einen Rollout aus, C1) und ConfigChange
  • das Deployment hat keine eingebauten Trigger – für ImageChange-Verhalten die Annotation image.openshift.io/triggers (C6)
  • DC-Hooks sind pre, mid (nur bei Recreate) und post: ein Kommando in einem Pod, mit einer failurePolicy
  • beim Deployment ersetzt man die Hooks durch Helm-Hook-Jobs, initContainers oder einen eigenen Job
  • DC-Strategien: Rolling, Recreate und Custom (ein eigenes Deployer-Image für Blue-Green oder Canary)
  • manuell: beim DC oc rollout latest dc/<n> und oc rollout retry dc/<n>, beim Deployment oc rollout undo deploy/<n> (dazu restart, pause)
  • warum DC weggeht: kein server-side apply, sperrig mit GitOps (E3), der Deployer-Pod ist langsam, kein sauberes maxSurge
  • seit OCP 4.14 legt oc new-app Deployments an – der DC bleibt unterstützt, bekommt aber keine neuen Features
  • Bibliothek: alle 8 Dienste sind Deployment (aus dem Helm-Chart, B11), einen DC triffst du im Novaris-Legacy-Block (H)
  • Kernsatz: Deployment ist passiv (du oder Helm löst aus), DeploymentConfig ist aktiv (beobachtet Image und Config selbst)
  • von Hand durchspielen: workload-feinschliff-openshift.html – Deployment vs DeploymentConfig live vergleichen

B3 Service, Endpoints, Cluster-DNS – wie ein Name zum Pod wird

NetzwerkServiceEndpointSliceCluster-DNSEX280

Pods kommen und gehen, ihre IPs auch. Ein Service legt einen festen Namen und eine feste IP davor. Ein Controller pflegt im Hintergrund die Liste der bereiten Pods dahinter, das Cluster-DNS löst den Namen auf, und OVN biegt jedes Paket auf eine dieser Pod-IPs um – hinter der Service-IP steckt kein Prozess.

Fester Name, feste IP – dahinter eine lebende Liste bereiter Pods. Pod (api-gateway) die App öffnet connect catalog:8080 nur ein Name, keine IP bekannt Cluster-DNS (CoreDNS) Deployment in openshift-dns, vom DNS-Operator löst catalog.library.svc.cluster.local 172.30.42.7 (die ClusterIP) jeder Pod: /etc/resolv.conf zeigt hierher, mit Suchliste Service catalog du legst an (oder Helm) · Objekt bleibt, Pods wechseln clusterIP: 172.30.42.7 – virtuell, stabil port: 8080targetPort: 8080 selector: app=catalog – wer gehört dazu hinter der ClusterIP läuft kein Prozess die interne API ist auch nur ein Service: kubernetes.default.svc EndpointSlice wird generiert vom endpoints-Controller – nicht anfassen 10.128.2.11:8080 ready 10.128.2.12:8080 ready 10.128.3.9 not ready – raus Pods mit app=catalog und grüner Readiness-Probe (B5) OVN-Kubernetes programmiert die Nodes-Datapath (früher kube-proxy) DNAT: ClusterIP 172.30.42.7 → eine ready Pod-IP round-robin, Zielliste aus der EndpointSlice der Pod merkt nichts – er redet scheinbar mit der ClusterIP Overlay-Details: F1 Pods catalog-* 10.128.2.11 READY – in der Slice 10.128.2.12 READY – in der Slice 10.128.3.9 NOT READY – rausgenommen Pod-IPs sind flüchtig – jeder Neustart eine neue darum nie eine Pod-IP fest verdrahten wird ein Pod not-ready, fällt neuer Verkehr weg, bestehende Verbindungen laufen aus Headless-Service: DNS gibt diese IPs direkt zurück, eine A-Record je Pod (StatefulSet) 1 · Pod fragt das DNS: wo ist catalog? 2 · verbindet zu 172.30.42.7:8080 füllt die Zielliste 3 · DNAT auf eine ready Pod-IP 4 · der Controller beobachtet die Pods und pflegt die Slice Service-Typen ClusterIP (Vorgabe) – nur clusterintern erreichbar NodePort – zusätzlich ein hoher Port (30000–32767) auf jedem Node LoadBalancer – fragt Cloud / MetalLB nach einer externen IP (Hetzner: MetalLB) ExternalName – CNAME auf einen externen Namen, kein Proxying Headless (clusterIP: None) – keine virtuelle IP, DNS gibt die Pod-IPs direkt sessionAffinity: None (round-robin je Verbindung) oder ClientIP (klebt an der Quell-IP) DNS-Kurzformen: catalog (selber Namespace), catalog.library (übergreifend), voll ….svc.cluster.local Named Ports: targetPort: http statt einer Nummer – der Container benennt den Port prüfen und die vier Klassiker oc describe svc catalog → die Zeile Endpoints. Leer = Selector passt nicht ODER kein Pod ready oc get endpointslices -l kubernetes.io/service-name=catalog oc rsh <pod> -- curl catalog:8080 1 · selector ≠ Pod-Labels → Endpoints leer → connection refused / timeout 2 · falscher targetPort → Verbindung hängt oder wird abgewiesen 3 · Pod flappt not-ready → sporadische 502 an der Route (B4) 4 · falscher Namespace → catalog löst ins Leere, catalog.library nehmen oc get svc · oc get endpoints (alte, aggregierte Sicht) Bibliothek: jeder der 8 Dienste ist ein ClusterIP-Service auf 8080. Die Configs verdrahten http://catalog-service:8080fullnameOverride (B11) muss den Namen exakt treffen ↗ von aussen erreichbar erst über eine Route oder ein Ingress (B4) – der Service allein hört nur im Cluster ↗ Merksatz: Service = stabiler Name + IP, EndpointSlice = wer gerade bereit ist, OVN = biegt das Paket um OCP-Unterschied: keiner – der DNS-Operator verwaltet CoreDNS, der Rest ist reines Kubernetes. EndpointSlice löst das alte einzelne Endpoints-Objekt ab.
Ein Name, der immer trifft. Der Service hält einen festen Namen und eine feste clusterIP. Das Cluster-DNS löst den Namen zur ClusterIP auf, der endpoints-Controller schreibt die IPs der bereiten Pods in eine EndpointSlice, und OVN macht bei jedem Paket ein DNAT von der ClusterIP auf eine davon. Hinter der ClusterIP sitzt kein Prozess – sie ist eine Fiktion, die der Node-Datapath umschreibt. Von aussen kommt man erst mit einer Route (B4) rein.
  • ein Service ist ein fester Name plus feste clusterIP vor einer wechselnden Menge Pods – das Objekt bleibt, die Pods dahinter nicht
  • der selector (Labels) sagt, welche Pods dazugehören – der endpoints-Controller pflegt daraus die Liste
  • die EndpointSlice listet die bereiten Pod-IPs – nur Pods mit passendem Label und grüner Readiness-Probe (B5). Sie löst das alte Endpoints-Objekt ab
  • hinter der ClusterIP läuft kein Prozess – OVN-Kubernetes (früher kube-proxy) programmiert die Nodes-Datapath und macht DNAT auf eine ready Pod-IP, round-robin
  • das Cluster-DNS (CoreDNS) löst <svc>.<ns>.svc.cluster.local zur ClusterIP auf, kurz <svc> im selben Namespace, <svc>.<ns> übergreifend
  • Service-Typen: ClusterIP (Vorgabe, clusterintern), NodePort (+ hoher Port je Node), LoadBalancer (externe IP via Cloud/MetalLB), ExternalName (CNAME), Headless (clusterIP: None, DNS gibt Pod-IPs direkt)
  • port ist der Service-Port, targetPort der Container-Port – named ports gehen auch
  • sessionAffinity: None (round-robin je Verbindung) oder ClientIP (klebrig nach Quell-IP, mit Timeout)
  • die interne API ist selbst nur ein Service: kubernetes.default.svc
  • prüfen: oc describe svc catalog (Zeile Endpoints leer = Selector-Mismatch oder kein Pod ready), oc get endpointslices -l kubernetes.io/service-name=catalog
  • Fallen: Selector ≠ Pod-Labels, falscher targetPort, flappender Pod → sporadische 502 an der Route, falscher Namespace angenommen
  • Bibliothek: jeder der 8 Dienste ist ein ClusterIP-Service auf 8080api-gateway ruft die Fachdienste über http://catalog-service:8080 usw. auf. Der Name kommt aus fullnameOverride im Helm-Chart (B11) und muss exakt passen, sonst geht die Auflösung ins Leere
  • von aussen erst über Route/Ingress (B4) – der Service allein hört nur im Cluster
  • OCP-Unterschied: keiner – der DNS-Operator verwaltet CoreDNS, sonst reines Kubernetes
  • von Hand durchspielen: service-route-probes-openshift.html – einen Service anlegen und per DNS auflösen, den Selector live kaputt machen und die Endpoints leer laufen sehen

B4 Route – edge / passthrough / reencrypt, der HAProxy-Router, das Wildcard-Zertifikat

NetzwerkRouteTLSRouterHAProxyEX280

Ein Service hört nur im Cluster (B3). Eine Route macht ihn von aussen erreichbar: sie sagt welcher Hostname auf welchen Service zeigt und wie TLS behandelt wird. Der Router (HAProxy als Pod) hält die öffentlichen Ports 80 und 443 und setzt jede Route in HAProxy-Konfig um.

Ein Hostname, ein Router, drei Arten mit TLS umzugehen. Browser library-frontend .apps.<cluster> HTTPS auf Port 443 Wildcard-DNS *.apps.<cluster> ein A-Record für alle Routes, zeigt auf die Router-Nodes / den LB du legst ihn einmal an Router (HAProxy) Pods router-default im Namespace openshift-ingress hält :80 und :443 auf den Nodes (hostNetwork) liest alle Route-Objekte clusterweit schreibt daraus die HAProxy-Konfig, live bringt ein Default-Wildcard-Zertifikat mit für *.apps.<cluster> (auf OKD/SNO selbstsigniert) → eine edge-Route braucht kein eigenes Zertifikat, kein Let's Encrypt Route library-frontend du legst an (oder Helm) · oc expose / oc create route host + to.service + port + tls.termination + path Status Admitted: True = der Router hat sie angenommen weiter wie B3 Router → Service (ClusterIP) → bereite Pods Service ohne Endpoints → der Router antwortet 503 Pod not-ready → fällt aus der Rotation (B5) Sharding: mehrere IngressController mit routeSelector (intern / extern) der Router liest die Route edge – TLS endet am Router Router entschlüsselt, dahinter HTTP zum Pod Router setzt X-Forwarded-Proto: https, X-Forwarded-For, Forwarded Pfad-Routing möglich (spec.path: /api) Keycloak-Falle: der Pod sieht HTTP und baut http://-Redirects – aktuelle Images: KC_PROXY_HEADERS=xforwarded in der Bibliothek der Normalfall für alle Web-Routes Spring: server.forward-headers-strategy=framework damit die App die echte URL rekonstruiert passthrough – Router fässt TLS nicht an die verschlüsselte Verbindung geht direkt zum Pod der Pod hält das Zertifikat, nicht der Router nötig für Client-Zertifikate / mTLS kein Pfad-Routing, keine Header, nur SNI-Host Router macht Routing rein über den SNI-Hostnamen Beispiel: eine gRPC- oder mTLS-Backend-Route Diagramm F4 (Service Mesh) macht mTLS anders – im Sidecar oc create route passthrough --service=grpc-svc reencrypt – zweimal TLS Router entschlüsselt und baut eine neue TLS-Verbindung zum Pod braucht destinationCACertificate Ende-zu-Ende verschlüsselt und trotzdem L7-Routing der Router kann Header setzen und Pfade routen passt zu einem Pod mit serving-cert (F5) der service-ca-Operator gibt dem Pod ein internes Zertifikat eigenes Public-Zertifikat: route.spec.tls.certificate / .key inline, oder ein Secret via cert-manager Optionen als Annotationen: haproxy.router.openshift.io/timeout: 30s, balance, rate-limit, ip_whitelist, disable_cookies oc expose svc/library-frontend erzeugt eine reine HTTP-Route oc create route edge --service=library-frontend --hostname=... Ingress statt Route: OpenShift übersetzt ein Ingress automatisch in eine Route (portabel bleiben, F3) Bibliothek: nur library-frontend und api-gateway haben eine Route (edge) – die anderen 6 Dienste reden nur clusterintern Merksatz: Route = Hostname + Service + TLS-Modus. Router = ein HAProxy, der alle Routes kennt. OCP-Unterschied: dort kann der Ingress-Operator das Cluster-Wildcard von einer echten CA holen lassen – auf OKD/SNO bleibt es selbstsigniert.
Von aussen rein – über genau einen HAProxy. Das Wildcard-DNS *.apps.<cluster> zeigt auf den Router. Der liest alle Route-Objekte und baut daraus seine HAProxy-Konfig. Die Route sagt: dieser Hostname geht auf diesen Service, und so wird TLS behandelt – edge (endet am Router), passthrough (Pod hält das Zertifikat), reencrypt (zweimal TLS). Dahinter geht es weiter wie in B3.
  • eine Route ist ein OpenShift-Objekt: Hostname → Service, mit Port, TLS-Modus und optional einem Pfad – Kubernetes-Pendant ist Ingress
  • der Router (HAProxy als Pod) hält Port 80 und 443 auf den Nodes, liest alle Routes clusterweit und schreibt daraus die HAProxy-Konfig
  • das Wildcard-DNS *.apps.<cluster> ist ein einziger A-Record, der auf den Router zeigt – du legst ihn einmal an
  • der Router bringt ein Default-Wildcard-Zertifikat für *.apps.<cluster> mit – darum braucht eine edge-Route kein eigenes Zertifikat (auf OKD/SNO selbstsigniert)
  • edge: TLS endet am Router, dahinter HTTP zum Pod – der Router setzt X-Forwarded-*, Pfad-Routing geht
  • passthrough: der Router fässt TLS nicht an, der Pod hält das Zertifikat – nötig für Client-Zertifikate, dafür kein Pfad-Routing und keine Header
  • reencrypt: Router terminiert und baut eine neue TLS-Verbindung zum Pod (mit destinationCACertificate) – Ende-zu-Ende und L7-Routing
  • eigenes Zertifikat: inline in route.spec.tls.certificate/.key, oder ein Secret über cert-manager
  • Keycloak-Falle: bei edge sieht der Pod HTTP und baut http://-Redirects. Aktuelle Images brauchen KC_PROXY_HEADERS=xforwarded plus passende KC_HOSTNAME/KC_HTTP_ENABLED=true, alte Images KC_PROXY=edge. Bei Spring hilft server.forward-headers-strategy
  • anlegen: oc expose svc/library-frontend (reine HTTP-Route), oc create route edge --service=library-frontend --hostname=...
  • Route-Status Admitted: True = der Router hat sie angenommen. HostAlreadyClaimed wenn zwei Routes denselben Host wollen
  • ein Service ohne Endpoints → der Router antwortet 503. Ein flappender Pod → sporadische 502 (B5)
  • OpenShift übersetzt ein Ingress automatisch in eine Route – wer portabel bleiben will, schreibt Ingress (F3)
  • Bibliothek: drei edge-Routes – library-frontend, api-gateway und keycloak unter *.apps.sno.<ip>.nip.io, in der Jenkins-Prod-Stage per oc expose angelegt. Die anderen 6 Dienste sind rein clusterintern
  • von Hand durchspielen: service-route-probes-openshift.html – eine edge-Route anlegen, Wildcard-Zertifikat und Router-Verhalten live sehen

B5 Probes – liveness, readiness, startup: was jede steuert

WorkloadsProbesreadinesslivenessstartupEX280

Drei Sonden, drei Fragen. readiness: soll dieser Pod gerade jetzt Verkehr bekommen? liveness: hängt der Container und braucht einen Neustart? startup: ist er mit dem Hochfahren fertig? Jede greift woanders ein – und eine falsch eingestellte Probe verursacht die meisten „läuft, aber tut nichts“-Fälle.

Drei Sonden: bereit? · hängt? · schon hochgefahren? Container startet startupProbe läuft – readiness & liveness pausiert Start fertig readiness gated Verkehr · liveness bewacht den Prozess – solange der Pod läuft readinessProbe Frage: Verkehr jetzt schicken? bei Fehler: Pod fliegt aus der EndpointSlice (B3), bekommt keinen Verkehr – bleibt aber am Leben wird sie wieder grün → Pod kommt zurück entscheidet allein über Endpoint-Mitgliedschaft soll die Abhängigkeiten prüfen (DB erreichbar, Cache warm) der Rollout ohne Ausfall hängt hier dran: neuer Pod bekommt erst Verkehr, wenn bereit (B1) Spring: /actuator/health/readiness MicroProfile: /health/ready livenessProbe Frage: hängt der Prozess und muss neu? bei Fehler (failureThreshold mal): der kubelet tötet den Container, restartPolicy startet ihn neu zu knapp eingestellt → CrashLoopBackOff soll nur prüfen „antwortet der Prozess überhaupt“ Falle: liveness prüft die DB → DB zuckt → ganze App startet neu Faustregel: liveness flach halten, Abhängigkeiten in die readiness viele Apps brauchen gar keine livenessProbe Spring: /actuator/health/liveness startupProbe Frage: ist das Hochfahren fertig? solange sie läuft: readiness & liveness pausiert bei Fehler: Container wird getötet (wie liveness) für langsame Starter: JVM, Keycloak (5+ min) failureThreshold × periodSeconds = maximale Bootzeit Beispiel: 30 × 10s = 5 min Grace ohne startupProbe auf einer langsamen JVM: liveness tötet den Container mitten im Start → CrashLoop Alternative früher: großes initialDelaySeconds an der liveness – startupProbe ist der sauberere Weg Arten & Parameter Arten: httpGet (200–399 = ok), tcpSocket, exec (exit 0), grpc initialDelaySeconds – wann die erste Prüfung periodSeconds (Vorgabe 10) · successThreshold timeoutSeconds (Vorgabe 1 – oft zu kurz!) failureThreshold (Vorgabe 3) – wie oft daneben, bevor es zählt grün / rot am Endpunkt, nicht per Log – die App muss den Pfad wirklich bedienen „Running, aber 0/1“ – und die anderen Fallen 0/1 Running = Prozess läuft, ist aber nicht ready → in keinem Endpoint → 503 an der Route 1 · timeoutSeconds: 1 auf einem langsamen Endpunkt → falsche Fehlschläge 2 · keine startupProbe auf einer JVM → liveness killt beim Booten 3 · liveness prüft die DB → DB-Blip startet alle Replicas neu 4 · readiness zeigt grün, obwohl noch nicht warm → erste Requests scheitern oc describe pod <p> # Events zeigen die Probe-Fehler Bibliothek: die 8 Spring-Dienste nutzen Actuator-liveness und -readiness, Keycloak braucht eine startupProbe (langer Boot) Merksatz: readiness steuert Verkehr, liveness steuert Neustart, startup hält beide zurück, bis es läuft oc get pod – die Spalte READY (2/2) ist die Summe der readiness-Ergebnisse, nicht der Status OCP-Unterschied: keiner – Probes sind reines Kubernetes. OpenShift zeigt sie zusätzlich in der Topologie-Ansicht der Konsole.
Drei Fragen, drei Eingriffspunkte. Die readinessProbe allein entscheidet, ob der Pod in der EndpointSlice steht – fällt sie, kommt kein Verkehr mehr, der Pod bleibt aber am Leben. Die livenessProbe lässt den Container töten und neu starten – zu scharf eingestellt gibt das eine CrashLoop. Die startupProbe hält die anderen beiden zurück, bis eine langsame JVM oder Keycloak fertig gebootet ist. Ein Rollout ohne Ausfall (B1) hängt komplett an der readiness.
  • readinessProbe: entscheidet allein über die Endpoint-Mitgliedschaft (B3) – fällt sie, kein Verkehr, der Pod bleibt am Leben und kommt zurück, wenn sie wieder grün ist
  • livenessProbe: fällt sie failureThreshold-mal, tötet der kubelet den Container und restartPolicy startet ihn neu – zu knapp = CrashLoop
  • startupProbe: solange sie läuft, sind readiness und liveness pausiert – für langsame Starter, failureThreshold × periodSeconds = maximale Bootzeit
  • readiness prüft Abhängigkeiten (DB erreichbar, Cache warm), liveness bleibt flach („antwortet der Prozess“) – sonst startet ein DB-Blip alle Replicas neu
  • Arten: httpGet (200–399 = ok), tcpSocket, exec (exit 0), grpc
  • Parameter: initialDelaySeconds, periodSeconds (10), timeoutSeconds (1 – oft zu kurz), failureThreshold (3), successThreshold
  • „0/1 Running“ = läuft, aber nicht ready → in keinem Endpoint → 503 an der Route
  • ohne startupProbe auf einer JVM tötet die liveness den Container mitten im Boot → CrashLoop – früher half ein großes initialDelaySeconds
  • feste Health-Pfade: Spring Actuator /actuator/health/liveness und /actuator/health/readiness, MicroProfile Health /health/live und /health/ready
  • der Rollout ohne Ausfall hängt an der readiness (B1) – ein neuer Pod bekommt erst Verkehr, wenn bereit, und maxUnavailable zählt bereite Pods
  • prüfen: oc describe pod <p> (Events), die Spalte READY bei oc get pod ist die Summe der readiness-Ergebnisse
  • Bibliothek-Stolperstein: die liveness mit initialDelaySeconds: 30 tötete catalog-service mitten im Spring-Kaltstart auf der geladenen SNO → hochgesetzt auf 150 s liveness / 90 s readiness. Alle 8 Dienste mit Actuator-Probes, Keycloak zusätzlich mit startupProbe (langer Boot)
  • OCP-Unterschied: keiner – Probes sind reines Kubernetes, die Konsole zeigt sie zusätzlich in der Topologie-Ansicht
  • von Hand durchspielen: service-route-probes-openshift.html – eine readiness-Probe live scheitern lassen und beobachten, wie der Pod aus den Endpoints fliegt, ohne zu sterben

B6 Scheduling – nodeSelector, Taints/Tolerations, Affinity

WorkloadsSchedulingTaintsAffinityEX280

Der Scheduler weist jedem neuen Pod einen Node zu: erst filtern (welche Nodes können ihn tragen?), dann bewerten (welcher ist am besten?), dann binden. Vier Regeln steuern das mit – und die Platzierung hängt an den Requests, nicht am tatsächlichen Verbrauch.

Erst filtern (wer kann?), dann bewerten (wer am besten?), dann binden. neuer Pod noch ohne nodeName der Scheduler fängt ihn 1 · Filtern – wer kann? genug freie CPU/RAM (nach Requests) passt zum nodeSelector nicht getaintet, oder Pod toleriert es erfüllt die harte nodeAffinity Volume-Zone passt, keine Port-Kollision nichts passt → Pod bleibt Pending 0/3 nodes available: 3 Insufficient cpu 2 · Bewerten – wer am besten? am wenigsten belastet (least-allocated) gleichmäßig verteilt über die Nodes weiche Präferenzen (preferred-Affinity) Image schon auf dem Node (Locality) höchster Score gewinnt, bei Gleichstand zufällig Descheduler-Operator kann später umverteilen 3 · Binden Scheduler schreibt spec.nodeName in den Pod der kubelet auf dem Node startet die Container Priorität & Preemption: ein wichtigerer wartender Pod kann niedriger priorisierte verdrängen (PriorityClass) auf SNO ist alles einerlei – ein Node, jeder Pod landet dort ein lokaler 2-Node-kind-Cluster reicht, um es zu sehen nodeSelector – der einfachste Weg nodeSelector: {disktype: ssd} Pod läuft nur auf einem Node mit genau diesem Label Node labeln: oc label node n1 disktype=ssd kein Match → Pending harte Regel, keine Abstufung – dafür nodeAffinity typisch: GPU-Node, Infra-Node, Storage-Node Bibliothek: Postgres auf einen Node mit schnellem Storage Taint (Node) + Toleration (Pod) der Node wehrt ab, wer nicht toleriert Effekte: NoSchedule, PreferNoSchedule, NoExecute (wirft auch laufende Pods raus) Control-Plane-Node: node-role…/master:NoSchedule der Node-Controller taintet automatisch bei disk-pressure, memory-pressure, not-ready oc adm taint nodes n1 gpu=true:NoSchedule Affinity – weich oder hart nodeAffinity: wie nodeSelector, mit Operatoren und required (hart) / preferred (weich) podAffinity: nah bei Pods mit Label X podAntiAffinity: fern von ihnen – spreizt Replicas immer mit einem topologyKey (hostname, zone) Bibliothek: podAntiAffinity auf catalog-service, damit die 2 Replicas auf verschiedenen Nodes liegen topologySpread der moderne Weg, Replicas gleichmäßig zu verteilen maxSkew: wie stark dürfen sich Zonen unterscheiden topologyKey: zone oder hostname whenUnsatisfiable: DoNotSchedule / ScheduleAnyway löst podAntiAffinity für „verteil gleichmäßig“ ab Requests treiben die Platzierung – nicht Limits, nicht der Ist-Verbrauch. Ein Pod ohne Requests: Scheduler rechnet mit ~0 → schlechtes Packing, BestEffort-QoS, wird zuerst evakuiert Pod bleibt Pending: kein Node passt – oc describe pod <p> nennt den Grund in den Events (Insufficient cpu / taint / didn't match node selector) Merksatz: nodeSelector = einfach hart, Taint/Toleration = Node wählt, Affinity/Spread = Pod wählt fein OCP-Unterschied: der Scheduler ist Kubernetes – OpenShift ergänzt Infra-/Worker-Rollen als Node-Labels und einen konfigurierbaren Scheduler-Operator (Profile).
Zwei Phasen, vier Stellschrauben. Der Scheduler filtert die Nodes (genug Platz nach Requests? nodeSelector? Taint toleriert? harte nodeAffinity?), bewertet die übrigen (am wenigsten belastet, gut verteilt) und schreibt nodeName in den Pod. nodeSelector ist die harte Kurzform, Taints lassen den Node wählen, Affinity und topologySpreadConstraints verteilen fein. Passt nichts, bleibt der Pod Pending.
  • der Scheduler arbeitet in zwei Phasen: filtern (welche Nodes können?) dann bewerten (welcher am besten?), dann spec.nodeName setzen
  • Requests treiben die Platzierung – nicht Limits, nicht der Ist-Verbrauch. Ein Pod ohne Requests packt schlecht und ist BestEffort
  • nodeSelector: Pod nur auf einem Node mit passendem Label – harte Kurzform, kein Match bedeutet Pending
  • Taint am Node + Toleration am Pod: der Node weist ab, wer nicht toleriert – Effekte NoSchedule, PreferNoSchedule, NoExecute
  • der Node-Controller taintet automatisch bei disk-pressure, memory-pressure, not-ready – Control-Plane-Nodes sind per Default getaintet
  • nodeAffinity: wie nodeSelector, aber mit Operatoren und required (hart) / preferred (weich)
  • podAffinity / podAntiAffinity: nah bei / fern von anderen Pods, immer mit einem topologyKey (hostname, zone) – Anti-Affinity spreizt Replicas
  • topologySpreadConstraints: der moderne Weg, Replicas gleichmäßig zu verteilen (maxSkew, topologyKey, whenUnsatisfiable)
  • Priorität & Preemption: ein wartender Pod mit höherer PriorityClass kann niedriger priorisierte verdrängen
  • auf SNO ist alles einerlei (ein Node) – ein lokaler 2-Node-kind-Cluster reicht, um die Verteilung zu sehen
  • Pod bleibt Pending = kein Node passt – oc describe pod <p> nennt den Grund in den Events
  • Bibliothek: podAntiAffinity auf catalog-service (2 Replicas auf verschiedene Nodes), Postgres per nodeSelector auf einen Storage-Node
  • OCP-Unterschied: der Scheduler ist Kubernetes – OpenShift ergänzt Infra-/Worker-Rollen als Node-Labels und einen Scheduler-Operator mit Profilen
  • von Hand durchspielen: workload-feinschliff-openshift.html – einen Pod per nodeSelector gezielt (nicht) platzieren, Pending live sehen

B7 ResourceQuota & LimitRange – Ressourcengrenzen je Project

WorkloadsResourceQuotaLimitRangeQoSEX280

Zwei Objekte, die zusammen arbeiten. LimitRange füllt fehlende Requests/Limits je Container auf und setzt Min/Max. ResourceQuota ist der Deckel für den ganzen Namespace. Der Haken: eine Quota auf requests.* lässt nur Pods durch, bei denen jeder Container einen Request hat – ohne LimitRange werden Deploys plötzlich abgelehnt.

Die eine füllt Werte auf, die andere setzt den Deckel – erst zusammen gehen sie auf. LimitRange – je Namespace du legst an (oder im project-request-Template, A2) beim Erstellen jedes Pods: füllt fehlende requests und limits je Container mit Default-Werten setzt min / max je Container (weist ab, was draußen liegt) setzt ein Default-Verhältnis limit : request ein Admission-Plugin mutiert den Pod, bevor er gespeichert wird oc describe limitrange -n library ohne LimitRange: Pods ohne Requests → schlechtes Scheduling (B6), keine QoS-Garantie ResourceQuota – Deckel je Namespace Obergrenze für alles zusammen im Namespace requests.cpu / requests.memory – Summe reserviert limits.cpu / limits.memory – Summe Obergrenzen pods, services, secrets, configmaps – Anzahl persistentvolumeclaims, requests.storage Scopes: BestEffort, NotTerminating, PriorityClass Status wird asynchron nachgeführt (Used gegen Hard) oc describe quota -n library ClusterResourceQuota (OpenShift): eine Quota über mehrere Projekte, per Label- oder Annotation-Selektor der Haken – warum sie zusammen müssen eine Quota auf requests.* oder limits.* lässt nur Pods durch, bei denen jeder Container den entsprechenden Wert gesetzt hat → ohne LimitRange: Deploy schlägt fehl must specify requests.cpu der klassische Moment: Quota dazu → alte Deploys brechen, weil sie nie Requests angegeben haben Reihenfolge: erst LimitRange, dann Quota gehört beides ins project-request-Template (A2) QoS-Klassen – abgeleitet, nicht gesetzt: wer bei Knappheit zuerst stirbt Guaranteed jeder Container: requests == limits für CPU und RAM – wird zuletzt evakuiert Burstable hat Requests, aber nicht überall == Limits – darf über den Request hinaus, bis zum Limit BestEffort gar keine Requests/Limits – wird zuerst evakuiert und zuerst OOM-gekillt request = reserviert (Scheduling, B6) · limit = harte Obergrenze CPU über Limit → gedrosselt (throttling) · Memory über Limit → OOMKilled (Exit 137) Bibliothek / cloud-host Namespace library: eine ResourceQuota (z. B. 6 vCPU / 12 Gi) und eine LimitRange (Default 250m / 512Mi Request, 1 / 1Gi Limit je Container) ohne die LimitRange würden mehrere der 8 Deploys abgelehnt, sobald die Quota existiert CRC-Erfahrung (Diagrammseite): 8 JVMs kalt = Last-Spitze → Requests realistisch setzen prüfen: oc describe quota -n library und oc describe limitrange -n library zeigen Used gegen Hard bzw. die Default-Werte Merksatz: LimitRange füllt je Container auf, ResourceQuota deckelt den ganzen Namespace – und die Quota erzwingt, dass die Werte da sind OCP-Unterschied: ClusterResourceQuota ist OpenShift-eigen – LimitRange und ResourceQuota sind reines Kubernetes.
Auffüllen und Deckeln. Die LimitRange mutiert jeden Pod beim Anlegen: fehlende requests/limits bekommen Default-Werte, Min/Max weisen Ausreißer ab. Die ResourceQuota ist die Summengrenze für den Namespace. Sobald die Quota requests.* deckelt, lässt sie nur noch Pods durch, bei denen jeder Container einen Request hat – darum braucht es beide. Aus requests und limits leitet Kubernetes die QoS-Klasse ab, die entscheidet, wer bei Knappheit zuerst stirbt.
  • LimitRange (je Namespace): füllt fehlende requests/limits je Container mit Default-Werten auf und setzt Min/Max – ein Admission-Plugin mutiert den Pod
  • ResourceQuota (je Namespace): Obergrenze für alles zusammen – Summe requests.cpu/limits.memory, Anzahl pods/services/secrets/pvc, requests.storage
  • der Haken: eine Quota auf requests.* lässt nur Pods durch, bei denen jeder Container einen Request hat – ohne LimitRange werden Deploys abgelehnt (must specify requests.cpu)
  • Reihenfolge: erst die LimitRange, dann die Quota – sonst brechen alte Deploys, die nie Requests angegeben haben
  • QoS-Klassen (abgeleitet, nicht gesetzt): Guaranteed (requests == limits überall) stirbt zuletzt, Burstable dazwischen, BestEffort (nichts gesetzt) stirbt zuerst
  • request = reserviert fürs Scheduling (B6), limit = harte Obergrenze – CPU über Limit wird gedrosselt, Memory über Limit wird OOMKilled (Exit 137)
  • ClusterResourceQuota (OpenShift): eine Quota über mehrere Projekte, per Label- oder Annotation-Selektor
  • gehört ins project-request-Template (A2), wenn jedes Projekt automatisch eine Grenze haben soll
  • Scopes schränken ein, worauf die Quota zählt: BestEffort, NotTerminating, PriorityClass
  • prüfen: oc describe quota -n library (Used gegen Hard), oc describe limitrange -n library
  • Bibliothek: oc new-project library bringt aus dem project-request-Template (A2) schon eine Quota und eine LimitRange mit. Real belegen OKD + die 8 JVMs + Postgres/Redis/Kafka/Keycloak zusammen ~15,6 GB – darum läuft die SNO auf cpx52 (24 GB). Ohne die LimitRange würden mehrere der 8 Deploys abgelehnt, sobald die Quota existiert
  • OCP-Unterschied: ClusterResourceQuota ist OpenShift-eigen, LimitRange und ResourceQuota sind reines Kubernetes
  • von Hand durchspielen: workload-feinschliff-openshift.html – eine Quota live gegen einen Deploy laufen lassen, mit und ohne LimitRange

B8 ConfigMap & Secret – wie ein Pod an Konfiguration und Secrets kommt

KonfigurationConfigMapSecretenv / volumeEX280

Beides sind Schlüssel/Wert-Ablagen im Namespace. ConfigMap für Nicht-Geheimes, Secret für Geheimes (base64-kodiert, separat behandelt – aber nicht verschlüsselt). Ein Pod nimmt sie entweder als Umgebungsvariablen (bei Start eingefroren) oder als Datei-Mount (aktualisiert sich live).

Zwei Ablagen, zwei Wege in den Pod – einer eingefroren, einer live. ConfigMap nicht-geheime Key/Value- oder Datei-Config Klartext in etcd, steht in oc describe du legst an (oder Helm) · max 1 MiB oc create configmap app --from-file=application.yaml Secret dasselbe, aber base64 und separat behandelt base64 ist keine Verschlüsselung nicht in describe, nicht ins Log, tmpfs beim Mount oc extract secret/db --to=- RBAC: view darf keine Secrets lesen; edit darf sie lesen (A5) at-rest: etcd nur base64 – echte Verschlüsselung einschalten (B9) Pod als Umgebungsvariablen envFrom / env[].valueFrom bei Pod-Start eingefroren als Datei-Mount volumes[] + volumeMounts[] aktualisiert sich live (~1 min) ausser bei subPath (dann eingefroren) eine Änderung rollt den Pod nicht es gibt keinen Trigger (anders als DC-ConfigChange, C6) Mount holt neuen Inhalt, aber die App muss ihn neu lesen env ändert sich gar nicht ohne Neustart Muster 1: Hash der Config als Annotation am Deployment checksum/config: {{ ... | sha256sum }} → Helm rollt Muster 2: von Hand oc rollout restart deploy/catalog manche Frameworks lesen Datei-Config selbst nach (Spring Cloud) Secret-Typen – der Typ erzwingt bestimmte Schlüssel Opaque frei · kubernetes.io/tls (tls.crt / tls.key) – Routes, serving-certs (F5) kubernetes.io/dockerconfigjson – Pull-Secrets (B10) · service-account-token (A6) kubernetes.io/basic-auth, kubernetes.io/ssh-auth immutable: true – nur ersetzen statt ändern, schützt vor Versehen und entlastet den apiserver-Watch Bibliothek / cloud-host App-Config (application.yaml-Overrides) als ConfigMap DB-Passwort, Keycloak-Client-Secret, Kafka-SASL als Secret Secrets gemountet unter /etc/secrets/, Config als env Secret-Herkunft, Rotation, raus aus Git: B10 · Verschlüsselung: B9 anlegen: oc create secret generic db --from-literal=password=..., oc create secret tls, oc set env --from, oc set volume projizierte Volumes: mehrere ConfigMaps/Secrets und der SA-Token in einen Verzeichnisbaum, mit eigenen Pfaden je Quelle Merksatz: env = eingefroren, Mount = live, Änderung rollt nichts – und base64 ist nur Kodierung, kein Schutz OCP-Unterschied: keiner – ConfigMap und Secret sind reines Kubernetes. OpenShift ergänzt nur oc extract und oc set als Komfort.
Zwei Ablagen, zwei Konsumwege. ConfigMap für Nicht-Geheimes, Secret für Geheimes – base64-kodiert, nicht verschlüsselt. Als Umgebungsvariablen ist der Wert bei Pod-Start eingefroren, als Datei-Mount aktualisiert er sich im laufenden Pod. Beide Lösen keinen Rollout aus – dafür braucht es eine Config-Hash-Annotation oder oc rollout restart. view darf keine Secrets lesen; edit darf sie lesen, aber keine Rollen oder Bindings verwalten.
  • ConfigMap = nicht-geheime Key/Value- oder Datei-Config, Secret = dasselbe, nur base64-kodiert und separat behandelt (nicht in describe, tmpfs beim Mount)
  • base64 ist keine Verschlüsselung – jeder mit get secrets sieht den Klartext (oc extract secret/x --to=-)
  • zwei Konsumwege: als Umgebungsvariablen (envFrom, env.valueFrom) oder als Datei-Mount (volumeMounts)
  • env ist bei Pod-Start eingefroren, ein Mount aktualisiert sich im laufenden Pod (~1 min) – ausser bei subPath, dann auch eingefroren
  • eine Änderung an ConfigMap/Secret rollt den Pod nicht neu – kein Trigger. Muster: checksum/config-Annotation (Helm rollt) oder oc rollout restart
  • Secret-Typen: Opaque, kubernetes.io/tls, kubernetes.io/dockerconfigjson, service-account-token, basic-auth, ssh-auth – der Typ erzwingt Schlüssel
  • Verschlüsselung at-rest: etcd speichert Secrets per Default nur base64 – echte Verschlüsselung schaltet man ein (B9)
  • RBAC: view darf keine Secrets lesen. edit darf sie lesen, aber keine Rollen oder Bindings verwalten (A5)
  • immutable: truenur ersetzen statt ändern, schützt vor Versehen und entlastet den apiserver-Watch. Größe max 1 MiB
  • anlegen: oc create secret generic --from-literal/--from-file, oc create secret tls, oc set env --from, oc set volume
  • projizierte Volumes: mehrere ConfigMaps/Secrets und der SA-Token in einen Verzeichnisbaum
  • Bibliothek: App-Config je Dienst als ConfigMap, DB-Passwort / Keycloak-Client-Secret / Kafka-Credentials als Secret – das Frontend-env.js ist eine als Datei gemountete ConfigMap (J4)
  • OCP-Unterschied: keiner – ConfigMap und Secret sind reines Kubernetes, oc extract/oc set sind Komfort
  • von Hand durchspielen: workload-feinschliff-openshift.html – env vs Mount live vergleichen, wenn sich eine ConfigMap ändert

B9 Secret-Schwerpunkt – Typen, Konsum, Verschlüsselung at-rest Schwerpunkt

KonfigurationSecretTypenVerschlüsselungEX280

Ein Secret ist kein monolithisches Ding: der Typ legt fest, welche Schlüssel drin sein müssen und wofür es taugt. Was in etcd liegt, ist per Default nur base64 – echte Verschlüsselung ist ein Schalter. Und der Weg zum Klartext ist ein Einzeiler, den jeder Auditor kennt.

Der Typ macht die Regeln. base64 ist Kodierung, kein Schutz. die sechs Typen Typ · Pflicht-Schlüssel · wofür Opaquefrei, kein Schema – alles andere kubernetes.io/tlstls.crt, tls.key – Routes (B4), serving-certs (F5) …/dockerconfigjson.dockerconfigjson – Image-Pull (B10) …/service-account-tokentoken, ca.crt, namespace – alter SA-Token (A6) …/basic-authusername, password …/ssh-authssh-privatekey der Typ erzwingt die Schlüssel – ein tls-Secret ohne tls.key wird abgewiesen immutable: true → nur ersetzen · max 1 MiB · namespaced: ein Pod sieht nur die seines Namespace projiziertes Volume: mehrere Secrets/ConfigMaps + SA-Token in einen Baum, je Quelle ein Pfad was wirklich in etcd liegt per Default: base64 – jedes etcd-Backup = Klartext echte Verschlüsselung einschalten: oc edit apiserver cluster spec.encryption.type: aescbc # oder aesgcm der kube-apiserver-Operator rollt eine EncryptionConfiguration aus, verschlüsselt Secrets, ConfigMaps und Token Schlüssel in openshift-config-managed, Rotation wöchentlich ein Migrations-Job schreibt alle bestehenden Objekte neu prüfen: oc get apiserver cluster -o yaml Feld status.encryption nicht dasselbe wie TLS im Transit – das macht der API-Server sowieso Alternative gegen Backup-Leak: etcd-Backup selbst verschlüsseln (G7) der Klartext-Weg, den jeder Auditor kennt oc get secret db -o jsonpath='{.data.password}' | base64 -d oc extract secret/db --to=./dir jeder mit get secrets im Namespace sieht alles im Klartext oc describe pod zeigt env-Variablen-Namen, nicht die Werte – aber ein Crash-Dump kann sie leaken Vorsicht: oc get secret -o yaml > all.yaml schreibt alle Klartexte in eine Datei Bibliothek / cloud-host jedes Secret gehört genau einem der 8 Dienste (DB, Keycloak, Kafka…) alles im Namespace library – die Trennung pro Namespace begrenzt den Schaden ein kompromittierter Pod kommt nur an die Secrets seines Namespace Herkunft, Pull-Secrets, Rotation, raus aus Git: B10 welche SA welches Secret nutzen darf: über die Pod-Spec, nicht über RBAC am Secret Konsum (aus B8): env = bei Start eingefroren, Volume = live (tmpfs, Dateien 0644, root – ausser fsGroup/defaultMode) Merksatz: der Typ bestimmt die Schlüssel, base64 schützt nichts, at-rest-Verschlüsselung ist ein Cluster-Schalter OCP-Unterschied: die apiserver/cluster-Verschlüsselung mit automatischer Rotation ist OpenShift-Komfort – das Kubernetes-Original ist manuelle EncryptionConfiguration.
Typ, Speicher, Klartext-Weg. Sechs Secret-Typen, jeder mit erzwungenen Schlüsseln (tls.crt/tls.key, .dockerconfigjson, …). In etcd steht per Default nur base64 – ein Backup ist Klartext; echte Verschlüsselung ist der Schalter spec.encryption.type: aescbc am apiserver/cluster. Und der Weg zum Klartext ist oc get secret … -o jsonpath | base64 -d, den jeder mit get secrets gehen kann.
  • der Typ erzwingt die Schlüssel: Opaque (frei), kubernetes.io/tls (tls.crt/tls.key), …/dockerconfigjson, …/service-account-token, …/basic-auth, …/ssh-auth
  • in etcd liegt per Default nur base64 – jedes etcd-Backup und jedes Disk-Image ist Klartext
  • echte Verschlüsselung at-rest einschalten: oc edit apiserver clusterspec.encryption.type: aescbc (oder aesgcm)
  • der kube-apiserver-Operator rollt eine EncryptionConfiguration aus, verschlüsselt Secrets, ConfigMaps und Token, Schlüssel in openshift-config-managed, Rotation wöchentlich
  • ein Migrations-Job schreibt alle bestehenden Objekte neu – prüfen: oc get apiserver cluster -o yaml Feld status.encryption
  • der Klartext-Weg: oc get secret db -o jsonpath='{.data.password}' | base64 -d oder oc extract secret/db --to=./dir
  • oc describe pod zeigt nur env-Namen, nicht die Werte – aber ein Crash-Dump oder oc get secret -o yaml > all.yaml leakt alles
  • immutable: true schützt vor versehentlicher Änderung, Größe max 1 MiB (etcd)
  • Secrets sind namespaced – ein Pod sieht nur die seines Namespace, das begrenzt den Schaden bei einem kompromittierten Pod
  • projiziertes Volume: mehrere Secrets/ConfigMaps und der SA-Token in einen Verzeichnisbaum, je Quelle ein Pfad
  • Konsum (B8): env eingefroren, Volume live (tmpfs, Dateien 0644/root – ausser fsGroup/defaultMode)
  • Bibliothek: jedes Secret gehört genau einem der 8 Dienste, alles im Namespace library – die Trennung begrenzt den Schaden
  • OCP-Unterschied: die apiserver/cluster-Verschlüsselung mit Auto-Rotation ist OpenShift-Komfort – Kubernetes pur ist manuelle EncryptionConfiguration

B10 Secret-Schwerpunkt – Herkunft, Pull-Secrets, Rotation, raus aus Git Schwerpunkt

KonfigurationSecretPull-SecretRotationGitOps

Woher kommt ein Secret? Wie zieht ein Pod ein privates Image? Wie tauscht man einen Wert aus, ohne dass etwas bricht? Und der Kernkonflikt: ein Klartext-Secret gehört nicht in Git, aber die restliche Config schon – drei Werkzeuge lösen das unterschiedlich.

Viele Quellen, ein Secret – und die Frage, wie es nicht in Git landet. Secret im Namespace, von einer dieser Quellen geprüft: oc get secret, Feld type du: oc create secret SA-Token-Controller (A6) service-ca-Operator (F5) cert-manager (ACME/CA) Helm-Chart (Klartext!) CI-Pipeline (Block E) Sealed Secrets / ESO Pull-Secret – damit ein Pod ein privates Image zieht oc create secret docker-registry regcred \ --docker-server=... --docker-username=... --docker-password=... oc secrets link default regcred --for=pull oder global: der Cluster-weite openshift-config/pull-secret die default-SA hat schon eins für die interne Registry (C5) imagePullSecrets steht an der SA oder direkt am Pod Rotation von Hand: Secret ersetzen, dann oc rollout restart env übernimmt sonst nichts, ein Mount holt die Datei, die App muss neu lesen automatisch: tls-Secrets vom service-ca-Operator (F5) cert-manager erneuert und aktualisiert das Secret vor Ablauf ein Reloader (Sidecar/Annotation) löst dann den Rollout aus raus aus Git – ein Klartext-Secret in Git ist base64, also für jeden mit Repo-Zugriff lesbar Sealed Secrets (Bitnami): du verschlüsselst mit dem Public Key des Controllers → SealedSecret-CRD in Git der Controller entschlüsselt im Cluster in ein echtes Secret · clusterspezifisch (der Key liegt im Cluster) External Secrets Operator (ESO): ExternalSecret-CRD referenziert einen Key in Vault / AWS SM / GCP SM / Azure KV der Operator synct ihn in ein Secret · der Wert lebt außerhalb des Clusters SOPS (+ age / PGP): verschlüsselte Werte-Datei in Git, ein GitOps-Plugin (Argo CD, Flux) entschlüsselt beim Apply Vergleich im GitOps-Kontext: E5 · die Vorlage bauen: oc create secret … --dry-run=client -o yaml, dann versiegeln Merksatz: Sealed Secrets = Key im Cluster, ESO = Secret in einem externen Store, SOPS = Key beim Deployer Bibliothek / cloud-host: der konkrete Weg wird im GitOps-Teil (Block E) gebaut – bis dahin Secrets von Hand mit oc create secret OCP-Unterschied: keiner – Sealed Secrets, ESO und SOPS sind Community-Werkzeuge, laufen als Operator/Controller auf jedem Cluster.
Herkunft, Pull, Rotation, Git. Ein Secret kommt von vielen Quellen – von dir, von Operatoren (SA-Token, service-ca, cert-manager), von Helm oder der CI. Ein Pull-Secret hängt man mit oc secrets link … --for=pull an eine SA. Rotation heißt: ersetzen und Pods rollen (bei service-ca-Certs automatisch). Und weil ein Klartext-Secret nicht in Git gehört, gibt es Sealed Secrets (Key im Cluster), den External Secrets Operator (Store außerhalb) und SOPS (Key beim Deployer).
  • Herkunft: du (oc create secret), der SA-Token-Controller (A6), der service-ca-Operator (F5), cert-manager, ein Helm-Chart (Klartext, schlecht), die CI-Pipeline (E), Sealed Secrets / ESO
  • Pull-Secret: oc create secret docker-registry regcred --docker-server=... --docker-username=... --docker-password=...
  • anhängen: oc secrets link default regcred --for=pull (je SA) oder global in openshift-config/pull-secret
  • die default-SA jedes Namespace hat schon ein Pull-Secret für die interne Registry (C5) – imagePullSecrets steht an der SA oder am Pod
  • Rotation von Hand: Secret ersetzen, dann oc rollout restart – env übernimmt sonst nichts, ein Mount holt die Datei, die App muss neu lesen
  • Rotation automatisch: tls-Secrets vom service-ca-Operator (F5), cert-manager erneuert vor Ablauf – ein Reloader löst den Rollout aus
  • warum kein Klartext-Secret in Git: base64 ist lesbar – jeder mit Repo-Zugriff hat die Zugangsdaten
  • Sealed Secrets: du verschlüsselst mit dem Public Key des Controllers → SealedSecret-CRD in Git → Controller entschlüsselt im Cluster (clusterspezifisch)
  • External Secrets Operator: ExternalSecret-CRD referenziert einen Key in Vault / AWS SM / Azure KV → Operator synct ihn in ein Secret (der Wert lebt außerhalb)
  • SOPS (+ age/PGP): verschlüsselte Werte-Datei in Git, ein GitOps-Plugin entschlüsselt beim Apply – Vergleich in E5
  • die Vorlage bauen: oc create secret … --dry-run=client -o yaml, dann versiegeln
  • Merksatz: Sealed Secrets = Key im Cluster, ESO = Store außerhalb, SOPS = Key beim Deployer
  • Bibliothek / cloud-host: der konkrete Weg wird im GitOps-Teil (Block E) gebaut – bis dahin Secrets von Hand
  • von Hand durchspielen: secrets-in-openshift.html – Typen anlegen, env vs Mount live, at-rest einschalten, Pull-Secrets, Rotation, die drei Git-Modelle
  • OCP-Unterschied: keiner – Sealed Secrets, ESO und SOPS sind Community-Werkzeuge, laufen auf jedem Cluster

B11 Helm-Schwerpunkt – Chart, Values, Template, Release: das mentale Modell Schwerpunkt

AuslieferungHelmChartRelease

Helm ist keine magische Deploy-Maschine. Es ist eine Textschablone plus eine Release-Verwaltung für ein Bündel Kubernetes-Manifeste. Kein Server im Cluster, keine laufende Überwachung. Der ganze Ablauf ist: Werte einsetzen → rendern → anwenden → das Ergebnis als Secret merken.

Der ganze Helm-Ablauf: Werterendernfertige YAMLapplyRelease-Secret Chart (Ordner) + Werte Template-Engine fertige YAML apply Release im Namespace Chart – ein Ordner mit Schablonen Datei: du legst an (helm create) oder vom Anbieter Chart.yaml – Name, version (Chart), appVersion     version = Chart-Stand, appVersion = die App dahinter values.yaml – die Vorgabe-Knöpfe templates/ – Manifeste mit {{ Platzhaltern }} templates/_helpers.tpl – Namensbausteine templates/NOTES.txt – Text nach dem Install charts/ – Subcharts (Abhängigkeiten) Chart.lock – gepinnte Dependency-Versionen helm dependency update zieht Subcharts nach charts/ helm show values ./chart listet alle Knöpfe helm lint ./chart prüft Struktur und Templates Werte – Vorrang: später schlägt früher 1  values.yaml im Chart – Vorgabe 2  -f werte-prod.yaml – deine Datei du legst an – mehrere -f möglich, letzte gewinnt 3  --set image.tag=1.4.2 – CLI, gewinnt → alles zu einem Baum gemerged (tiefe Verschmelzung) im Template erreichbar als .Values.image.tag eingebaute Objekte neben .Values: .Release.Name .Release.Namespace .Chart.Name .Chart.Version .Capabilities.KubeVersion daher rendert dasselbe Chart je Release anders Werte-Datei = du legst an, values.schema.json prüft sie Secrets gehören nicht in die Werte-Datei – siehe B10 Template-Engine – Go-Templates, sonst nichts {{ .Values.replicas }}   Wert einsetzen {{ .Release.Name }}-web   Namen bauen (darum fullnameOverride) {{ include "biblio.labels" . }}   Baustein aus _helpers.tpl einfügen {{- if .Values.route.enabled }} … {{- end }}   Block an / aus {{- range .Values.services }} … {{- end }}   Schleife – ein Block je Listeneintrag {{- ... }} / {{ ... -}}   steuern Leerraum und Leerzeilen (YAML ist einrückungssensibel) kein Server, kein Tiller, keine Reconciliation reine Textersetzung – danach wird das Ergebnis angewendet Fehler im Template = Render bricht ab, nichts geht an den Cluster gerenderte YAML – genau die Objekte aus Block B ↗ Deployment · Service · Route · ConfigMap · Secret · HPA · ServiceAccount helm template biblio ./chart -f werte.yaml zeigt alles, ändert nichts – der Blick vor jedem Install Label: wird generiert – nicht hier editieren, den Wert ändern prüfen: helm template … | oc apply --dry-run=server -f - Kustomize-Alternative: oc apply -k patcht fertige YAML statt zu templaten Release – benannte, versionierte Installation helm install biblio ./chart -n library → Revision 1 helm upgrade biblio ./chart -n library → Revision 2 helm rollback biblio 1 → Revision 3 (= Rev 1) gespeichert als Secret sh.helm.release.v1.biblio.v2 im Namespace (base64 + gzip) Label: wird von Helm generiert – der Verlauf, nicht anfassen helm list -n library · helm history biblio · helm get manifest biblio helm uninstall biblio löscht nur, was im Release steht Was Helm NICHT tut – und was im Gespräch oft gefragt wird keine laufende Überwachung: eine manuelle Änderung am Cluster (Drift) wird erst beim nächsten helm upgrade überschrieben – ein Operator (D1) korrigiert sofort ↗ Helm handelt als du: es nutzt dein kubeconfig-Token, kein Serverteil – es kann nur, was deine RBAC erlaubt (A5) ↗ gelöschte Template-Objekte bleiben verwaist · CRDs aus crds/ werden nie ge-upgradet · Rollback berührt keine Daten (PVCs, DB) nützliche Flags: --dry-run (nur rendern) · --atomic (bei Fehler zurück) · upgrade --install (idempotent, CI-tauglich) OpenShift & Bibliothek OpenShift: die helm-CLI ist unverändert · die Web-Konsole hat einen Helm-Katalog HelmChartRepository (clusterweit) / ProjectHelmChartRepository listen Charts im Entwickler-Katalog Bibliothek: ein Werteblock je Dienst (Image-Tag, Replicas, Ressourcen) · fullnameOverride für stabile Namen ↗ Jenkins ruft helm upgrade --install je Dienst auf (Diagramm E1) – kein Cluster-Agent ↗ Alternativen zum Templaten oc apply -k (Kustomize) – Overlays patchen fertige YAML Argo CD (E3) rendert Helm-Charts selbst und hält sie im Soll ↗ oc process (Template) – der ältere OpenShift-eigene Weg Operator (D) – wenn Betriebslogik laufend reagieren muss Merksatz: Helm ist values → template → render → apply → Release-Secret. Alles andere ist Zubehör. Prüfen vor dem Install: immer helm template lesen. Danach: helm get manifest gegen oc get vergleichen. OCP-Unterschied: keiner an der CLI – OCP ergänzt nur den Konsolen-Katalog und zertifizierte Charts von catalog.redhat.com.
Schablone plus Verlauf, mehr ist Helm nicht. Ein Chart ist ein Ordner aus Chart.yaml, values.yaml und templates/. Die --set- und -f-Werte werden über die Vorgaben gemerged, die Go-Template-Engine rendert daraus fertige YAML (das zeigt helm template, ohne etwas anzufassen), und beim install entsteht ein Release – als Secret im Namespace, das den Verlauf trägt. Helm läuft mit deinem Token, hat keinen Cluster-Agenten und korrigiert Drift erst beim nächsten upgrade – genau da fängt ein Operator an.
  • Helm ist eine Textschablone plus Release-Verwaltung für ein Bündel Manifeste – kein Server im Cluster, keine laufende Überwachung
  • ein Chart ist ein Ordner: Chart.yaml (Metadaten, Versionen, Dependencies), values.yaml (die Knöpfe), templates/ (Manifeste mit {{ }}), charts/ (Subcharts)
  • die Werte haben einen Vorrang, später schlägt früher: values.yaml, dann -f meine-werte.yaml, dann --set key=val – alles zu einem Baum verschmolzen, im Template .Values.x.y
  • neben .Values gibt es .Release.Name, .Release.Namespace, .Chart.* und .Capabilities.*darum rendert dasselbe Chart je Release anders
  • beim Rendern werden Chart und Werte zu fertiger YAML (helm template zeigt sie, ohne etwas zu ändern), danach wird sie intern angewendet
  • ein Release ist eine benannte, versionierte Installation in einem Namespace, gespeichert als Secret sh.helm.release.v1.<name>.v<n> (base64 und gzip)
  • Befehle sind helm install, upgrade, rollback, uninstall, list, history – nützliche Flags --dry-run, --atomic, upgrade --install
  • version ist die Chart-Version, appVersion die Version der App dahinter – zwei verschiedene Dinge
  • Helm handelt als du (mit deinem kubeconfig-Token) und kann nur, was deine RBAC erlaubt (A5) – seit Helm 3 kein Tiller mehr
  • Helm ist kein Operator: eine Abweichung am Cluster wird erst beim nächsten helm upgrade korrigiert, ein Rollback rührt keine Daten an (D1)
  • in OpenShift ist die helm-CLI unverändert – die Web-Konsole hat einen Helm-Katalog, die HelmChartRepository-CR listet Quellen
  • Bibliothek: ein Werteblock je Dienst (Image-Tag, Replicas, Ressourcen), fullnameOverride für stabile Namen. Jenkins ruft helm upgrade --install je Dienst auf (E1)
  • Alternativen: Kustomize (oc apply -k, patcht statt zu templaten), Argo CD rendert Charts selbst (E3), oc process für die alten OpenShift-Templates

B12 Helm-Schwerpunkt – Praxis am Bibliothek-Chart, wo Helm aufhört Schwerpunkt

AuslieferungHelmPraxisRollback

B11 war das Modell – hier der Alltag. Immer zuerst helm template lesen. Dann upgrade --install --atomic. helm history und rollback als Sicherheitsnetz. Hooks für DB-Migrationen, Dependencies für Subcharts – und ein paar Stellen, an denen Helm einfach nichts mehr tut.

Erst lesen, dann --atomic ausrollen, immer ein Rollback-Netz. der Ablauf 1 immer zuerst die gerenderte YAML lesen: helm template biblio ./chart -f werte-sno.yaml 2 Erst-Installation: helm install biblio ./chart -n library -f werte-sno.yaml 3 sehen, was ein Upgrade ändern würde: helm diff upgrade biblio ./chart -f werte-sno.yaml # Plugin 4 der CI-taugliche Aufruf (idempotent, rollt bei Fehler zurück): helm upgrade --install --atomic --timeout 10m Rollback-Netz helm history biblio helm rollback biblio 3 ein Rollback wendet die Manifeste von Revision 3 neu an und legt eine neue Revision an berührt keine PVCs oder DB-Daten – nur die getrackten Manifeste Hooks pre-install / post-upgrade als annotierte Jobs Annotation helm.sh/hook, dazu hook-weight (Reihenfolge) und hook-delete-policy (aufräumen) typisch: DB-Migration vor dem Rollout Hooks laufen außerhalb der Release-Manifeste – kein Rollback Dependencies dependencies: in Chart.yaml helm dependency update zieht die Subcharts nach charts/ Chart.lock pinnt die Versionen (wie ein Lockfile) condition / tags schalten einzelne Subcharts an/aus wo Helm aufhört gelöschte Template-Objekte bleiben verwaist – bis --force oder Hand CRDs im crds/-Ordner: einmal installiert, nie ge-upgradet oder gelöscht ein Rollback berührt keine Daten (PVCs, DB-Inhalt) keine Drift-Korrektur – nur beim nächsten helm upgrade keine Ordnungs-Garantie innerhalb eines Release (nur Hooks ordnen) ab hier übernimmt Argo CD (E3) – rendert selbst und hält das Soll Umbrella-Chart vs Einzelchart je Dienst Umbrella: ein Chart, 8 Subcharts, ein Release – ein upgrade für alles alles-oder-nichts (--atomic), aber ein Fehler blockt alle je Dienst: 8 Charts, 8 Releases – Jenkins ruft upgrade --install je Dienst unabhängiger Rollback, aber keine dienstübergreifende Atomarität Bibliothek: je Dienst – die 8 Deploys sind lose gekoppelt (eigene DB, eigenes Tempo) der pipeline-SA (A5/A6) begrenzt, was helm upgrade anfassen darf Merksatz: template lesen → upgrade --install --atomic → bei Bedarf rollback. Hooks für Migrationen, Argo CD für Drift. nicht vergessen: helm get manifest biblio zeigt, was gerade installiert ist – gegen oc get vergleichen OCP-Unterschied: keiner an der CLI – Helm ist Helm. OpenShift GitOps bündelt Argo CD als Operator, damit die Drift-Lücke geschlossen wird (E3).
Der Alltag mit Helm. Immer helm template zuerst, dann upgrade --install --atomic (idempotent, rollt bei Fehler zurück), helm history und rollback als Netz. Hooks (annotierte Jobs) machen DB-Migrationen, Dependencies ziehen Subcharts nach charts/. Wo Helm aufhört: verwaiste Objekte, nie ge-upgradete CRDs, kein Datenschutz beim Rollback, keine Drift-Korrektur – das übernimmt Argo CD (E3).
  • immer zuerst helm template – die gerenderte YAML lesen, bevor man installiert
  • Erst-Installation: helm install biblio ./chart -n library -f werte-sno.yaml – ein Chart, eine Werte-Datei je Umgebung
  • helm diff upgrade (Plugin) zeigt, was sich ändert, bevor es passiert
  • helm upgrade --install --atomic --timeout 10mder CI-taugliche Aufruf (idempotent, rollt bei Fehler zurück)
  • helm history biblio + helm rollback biblio <rev>ein Rollback wendet die Manifeste der alten Revision neu an, als neue Revision
  • Hooks: pre-install / post-upgrade als annotierte Jobs (helm.sh/hook, hook-weight, hook-delete-policy) – z. B. DB-Migration vor dem Rollout
  • Hooks laufen außerhalb der Release-Manifeste – ein Rollback macht sie nicht rückgängig
  • Dependencies: helm dependency update zieht Subcharts nach charts/, Chart.lock pinnt die Versionen, condition/tags schalten sie an/aus
  • wo Helm aufhört: verwaiste Objekte nach Template-Löschung, CRDs aus crds/ ohne Upgrade, kein Schutz der Daten-Volumes, keine Drift-Korrektur
  • Umbrella-Chart (ein Release, alles-oder-nichts) vs Einzelchart je Dienst (8 Releases, unabhängiger Rollback, keine dienstübergreifende Atomarität)
  • Bibliothek: je Dienst getrennt – die 8 Deploys sind lose gekoppelt (eigene DB, eigenes Tempo), Jenkins ruft upgrade --install je Dienst
  • der pipeline-SA (A5/A6) begrenzt, was helm upgrade anfassen darf
  • wo Helm aufhört und ein Operator anfängt (Block D): Betriebslogik, die laufend auf den Cluster-Zustand reagieren muss
  • nicht vergessen: helm get manifest biblio zeigt, was gerade installiert ist – gegen oc get vergleichen
  • einmal von Hand durchspielen: helm-am-bibliothek-chart.html geht den ganzen Ablauf am echten Chart durch – template, install, Release-Secret, upgrade, kaputtes Upgrade, Drift, uninstall

Block C – die Build-Kette (OpenShift-eigen)

C1 ImageStream & ImageStreamTag – Digest statt Tag, und warum das die Tag-Falle löst

Build-KetteImageStreamDigestRegistryEX280

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 ausserhalb 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
  • von Hand durchspielen: build-kette-openshift.html – einen ImageStream live sehen, die Tag-Falle nachstellen, per Um-Taggen freigeben

C2 BuildConfig & Build-Strategien – S2I, Docker, custom

Build-KetteBuildConfigS2IStrategienEX280

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 ausserhalb 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 ausserhalb 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 ausserhalb 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
  • von Hand durchspielen: build-kette-openshift.html – ein BuildConfig anlegen und einen S2I-Build wirklich laufen lassen

C3 Source-to-Image von innen – assemble/run-Skripte, Builder-Image, inkrementell

Build-KetteS2Iassemble/runBuilder-Image

S2I baut ein Image ohne Dockerfile, weil das Builder-Image die Verpackung schon kann. Es bringt zwei Skripte mit: assemble (baut, zur Build-Zeit) und run (startet, im Container). Der Gedanke: wer Java schreibt, soll kein Dockerfile pflegen – das Builder-Image kodiert die Best Practice.

Zwei Skripte im Builder-Image: assemble baut, run startet. Builder-Image z. B. ubi9/openjdk-17, ubi9/nodejs-20 assemble – Build-Zeit: kompilieren, Artefakt ablegen run – Laufzeit: die App starten (wird Entrypoint) save-artifacts – Cache retten (optional) usage – Hilfetext die Skripte kodieren die Best-Practice-Verpackung Red-Hat-Images respektieren beliebige UID (A7) eigene Skripte in .s2i/bin/ gewinnen über die des Images der S2I-Build-Ablauf 1 Builder-Image starten, Quellcode nach /tmp/src injizieren 2 assemble läuft: mvn package / npm ci && npm run build Artefakt an den Ort legen, den run erwartet (/deployments/app.jar) 3 den Container als neues Image committen, run als Entrypoint Umgebung: .s2i/environment oder env im BuildConfig lokal testen: s2i build . ubi9/openjdk-17 catalog (die s2i-CLI) debuggen: S2I_DEBUG=1, oc rsh in den Build-Pod Liberty-s2i: assemble legt das WAR nach /config/apps/ (Liberty-Zeichnung 10) Ergebnis ein gewöhnliches OCI-Image nichts S2I-spezifisches zur Laufzeit läuft überall, wie jedes andere Image Entrypoint ist das run-Skript landet als ImageStreamTag (C1) in der Registry (C5) Größe: enthält das ganze JDK – darum der Chained Build (unten) docker-image://-Referenz auch für ein reines Deployment inkrementeller Build save-artifacts schreibt den Dependency-Cache (~/.m2, node_modules) als tar nach stdout S2I stellt ihn im nächsten Build unter /tmp/artifacts wieder her assemble nutzt den Cache → keine erneuten Downloads aktivieren: strategy.sourceStrategy.incremental: true im BuildConfig spart bei Java-Builds Minuten – füllt aber das Cache-Layer und muss gepflegt werden Chained Build – kleines Laufzeit-Image Build-Stufe: volles JDK + Maven → produziert nur das Artefakt (die JAR) Runtime-Stufe: nur JRE, kopiert das Artefakt hinein oc new-build ... --to catalog-builder, dann oc new-build --source-image=catalog-builder ... --to catalog = dasselbe wie ein Docker-Multi-Stage-Build, nur mit S2I-Bausteinen der Gedanke: wer Java oder Node schreibt, soll kein Dockerfile pflegen – das Builder-Image kodiert Base-Image, Verpackung, Nicht-Root, Health-Defaults Merksatz: assemble = bauen (einmal), run = starten (jeder Pod), Ergebnis = normales Image wann oc new-app das alles automatisch aufsetzt: C4 OCP-Unterschied: identisch – S2I ist ein eigenes Projekt (die s2i-CLI läuft auch ohne Cluster), OpenShift ruft es über die BuildConfig auf.
Das Builder-Image kann die Verpackung schon. Es bringt assemble (baut zur Build-Zeit) und run (startet im Container) mit. S2I injiziert den Quellcode, lässt assemble laufen und committet den Container als neues Image mit run als Entrypoint. save-artifacts rettet den Dependency-Cache in den nächsten Build. Eigene Skripte in .s2i/bin/ gewinnen. Das Ergebnis ist ein ganz normales OCI-Image.
  • das Builder-Image bringt zwei Skripte mit: assemble (baut, zur Build-Zeit) und run (startet, im Container – wird der Entrypoint)
  • assemble: mvn package / npm run build anwerfen und das Ergebnis an den Ort legen, den run erwartet
  • der Ablauf: Builder starten → Quellcode nach /tmp/srcassemble → Container als neues Image committen mit run als Entrypoint
  • inkrementeller Build: save-artifacts hebt den ~/.m2-/node_modules-Cache in die nächste Runde – incremental: true im BuildConfig
  • eigene Skripte im Repo unter .s2i/bin/ überschreiben die des Images, .s2i/environment für Env-Variablen
  • Chained Build: Build-Stufe mit vollem JDK produziert nur das Artefakt, Runtime-Stufe mit nur JRE kopiert es hinein – kleines Laufzeit-Image, wie ein mehrstufiges Dockerfile
  • der Gedanke: wer Java schreibt, soll kein Dockerfile pflegen – das Builder-Image kodiert Base-Image, Verpackung, Nicht-Root
  • Red-Hat-Builder-Images (ubi9/openjdk-17, ubi9/nodejs-20) respektieren beliebige UID (A7)
  • lokal testen mit der s2i-CLI: s2i build . ubi9/openjdk-17 catalog – debuggen mit S2I_DEBUG=1 oder oc rsh
  • Was-wäre-wenn für die Bibliothek: ein nodejs-S2I für library-frontend (Angular-Build → statisches nginx) würde das zweistufige Dockerfile ersparen – heute baut die Pipeline das Frontend selbst (Node 22 im Container)
  • Liberty-s2i: assemble legt das WAR nach /config/apps/ (Liberty-Gedankenspiel Zeichnung 10)
  • das Ergebnis ist ein gewöhnliches OCI-Image – nichts S2I-spezifisches zur Laufzeit, läuft überall
  • OCP-Unterschied: identisch – S2I ist ein eigenes Projekt, OpenShift ruft es über die BuildConfig auf
  • S2I von innen durchspielen: Build bis Rollout – lokale Binärquelle bauen und assemble, run, Build-Pod sowie Image-Digest auseinanderhalten

C4 oc new-app / oc new-build – welche Objekte sie erzeugen

Build-Ketteoc new-appoc new-buildEX280

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 ausserhalb (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
  • von Hand durchspielen: build-kette-openshift.html – oc new-app live starten und sehen, welche Objekte dabei entstehen

C5 Die interne Registry – der image-registry-Operator, die zwei Adressen, Pruning

Build-Ketteinterne Registryimage-registryPruningEX280

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 aussen, muss erst freigeschaltet werden). Pods ziehen ohne Extra-Secret. oc adm prune images hält sie klein.

Eine Registry, zwei Adressen: innen zum Ziehen, aussen (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 aussen – 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 aussen 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 = aussen (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 aussen. 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)
  • aussen 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
  • von Hand durchspielen: build-kette-openshift.html – die interne Registry einschalten, von Hand pushen und pullen, aufräumen mit prune images

C6 Deployment-Trigger – ImageChange und ConfigChange

Build-KetteTriggerImageChangeConfigChange

Reines Kubernetes rollt nur aus, wenn sich die Pod-Vorlage ändert. Ein neuer Push unter demselben Tag ändert sie nicht – also passiert nichts, und du suchst den Fehler im Code. Der OpenShift-Image-Trigger löst das: er trägt den neuen Digest in die Vorlage. ConfigChange meint die Vorlage selbst – nicht referenzierte ConfigMaps.

Ein Rollout passiert nur, wenn sich die Pod-Vorlage ändert. reines Kubernetes – die Falle Pod-Vorlage: image: catalog:latest du pushst catalog:latest neu → die Vorlage ist byte-identisch → kein neues ReplicaSet → nichts passiert der Fix ist gebaut, aber nicht ausgerollt du suchst den Fehler im Code – stundenlang Regel ausserhalb OpenShift: nie :latest, immer ein Digest oder eine Version der OpenShift-Image-Trigger eine Annotation am Deployment: image.openshift.io/triggers: '[{"from":{"kind": "ImageStreamTag","name":"catalog:latest"},"fieldPath": "spec.template.spec.containers[0].image"}]' der openshift-controller-manager beobachtet den Tag neuer Digest → er patcht die Vorlage: image: catalog@sha256:<neuer-digest> → Vorlage ändert sich → neues ReplicaSet → Rollout oc set triggers deploy/catalog --from-image=catalog:latest -c catalog ConfigChange eine Änderung an der Pod-Vorlage selbst löst aus beim Deployment: implizit (jede Vorlagenänderung) beim DeploymentConfig: type: ConfigChange deklarieren (B2) Grenze: meint die Vorlage, nicht referenzierte ConfigMaps oder Secrets gemountete ConfigMap geändert → Pod rollt nicht (B8) Abhilfe: checksum/config-Annotation (Helm) oder ein Reloader-Operator der universelle Weg oc rollout restart deploy/catalog setzt eine restartedAt-Annotation → Vorlagenänderung → Rollout liest dabei ConfigMaps/Secrets neu – der Ersatz für den fehlenden Trigger oc set triggers deploy/catalog --manual pausiert den Image-Trigger nicht verwechseln image.openshift.io/triggers = am Deployment (löst einen Rollout aus) BuildConfig-Trigger = am BuildConfig (löst einen Build aus, C2) Kette: Push → ImageStream-Digest (C1) → Trigger → Rollout (B1) Bibliothek: der Image-Trigger am Deployment je Dienst, gebaut wird ausserhalb Merksatz: ohne Trigger und ohne wechselnden Tag läuft der alte Container weiter – der Image-Trigger schreibt den Digest, dann rollt es ConfigChange = Vorlage, nicht die referenzierten ConfigMaps/Secrets – dafür checksum/config oder oc rollout restart OCP-Unterschied: der Image-Trigger ist OpenShift-eigen – reines Kubernetes hat kein Äquivalent (nur externe Tools wie Keel oder Argo Image Updater).
Der Digest muss in die Vorlage. Reines Kubernetes rollt nur bei einer Änderung der Pod-Vorlage – ein erneuter :latest-Push ändert sie nicht, also passiert nichts. Die Annotation image.openshift.io/triggers lässt den openshift-controller-manager den ImageStreamTag beobachten und bei neuem Digest die Vorlage patchen → Rollout. ConfigChange meint die Vorlage selbst, nicht referenzierte ConfigMaps – dafür checksum/config oder oc rollout restart.
  • reines Kubernetes: die Pod-Vorlage sagt image: catalog:latest – ein erneuter Push unter demselben Tag ändert die Vorlage nicht, nichts passiert
  • der OpenShift-Image-Trigger ist eine Annotation am Deployment: image.openshift.io/triggers mit einem ImageStreamTag und einem fieldPath
  • der openshift-controller-manager beobachtet den Tag – neuer Digest → er patcht die Vorlage auf catalog@sha256:<digest> → neues ReplicaSet → Rollout
  • hinzufügen: oc set triggers deploy/catalog --from-image=catalog:latest -c catalog--manual pausiert ihn
  • DeploymentConfig hat denselben Mechanismus eingebaut als spec.triggers[].type: ImageChange (B2)
  • ConfigChange: eine Änderung an der Pod-Vorlage selbst löst aus – beim Deployment implizit, beim DeploymentConfig muss man type: ConfigChange deklarieren
  • wichtige Grenze: ConfigChange meint die Vorlage, nicht referenzierte ConfigMaps/Secrets – eine gemountete ConfigMap zu ändern rollt den Pod nicht (B8)
  • Abhilfe: checksum/config-Annotation (Helm rollt), oc rollout restart deploy/catalog, oder ein Reloader-Operator
  • oc rollout restart setzt eine restartedAt-Annotation → Vorlagenänderung → Rollout, und liest dabei ConfigMaps/Secrets neu
  • nicht verwechseln: image.openshift.io/triggers am Deployment (löst Rollout) vs BuildConfig-Trigger am BuildConfig (löst Build, C2)
  • die Trigger-Kette: Push → ImageStream-Digest (C1) → Trigger → neues ReplicaSet → Rollout (B1)
  • Bibliothek: kein Image-Trigger. Jenkins ruft helm upgrade --set image.tag=$GIT_SHA je Dienst – der Tag-Wert in der Pod-Vorlage ändert sich, also rollt Kubernetes ganz normal aus. Der Trigger-Weg wäre die Alternative, wenn nicht die CI den Tag setzt
  • OCP-Unterschied: der Image-Trigger ist OpenShift-eigen – reines Kubernetes hat kein Äquivalent (nur externe Tools wie Argo Image Updater)
  • beide Triggerwege durchspielen: Build bis Rollout – erst einen neuen Image-Digest automatisch ausrollen, danach eine Pod-Vorlagenänderung als ConfigChange-Wirkung beweisen

Block D – Operatoren & OLM

D1 Was ist ein Operator? – Controller + CRD + Betriebswissen als Code

OperatorenControllerCRDReconcile

Ein Operator ist ein eigener Controller plus eine CRD. Die CRD führt einen neuen Objekttyp ein (z. B. Kafka), eine Custom Resource davon ist der Sollzustand. Der Operator läuft in einer Reconcile-Schleife: Custom Resources beobachten, mit der Realität vergleichen, handeln – er kodiert das Betriebswissen, das sonst ein Admin von Hand anwendet.

Ein Controller, der einen eigenen Objekttyp verwaltet – und ihn am Leben hält. CRD – führt den Typ ein CustomResourceDefinition registriert kind: Kafka danach ist Kafka gleichwertig zu Pod (A3) oc get crd, oc explain kafka.spec Schema + Versionen + Validierung stecken in der CRD wird generiert von OLM beim Installieren (D2) Custom Resource – der Sollzustand du legst an (oder Helm/GitOps) kind: Kafka spec: {replicas: 3, version: "3.7"} nur was, nicht wie – das Wie kennt der Operator Operator ein Controller-Deployment im Cluster Reconcile-Schleife, endlos: 1 Custom Resources beobachten (watch) 2 Ist gegen Soll vergleichen 3 handeln: anlegen, ändern, löschen reagiert auch auf Pod-Ausfälle, Zeit, externe Events schreibt status.conditions zurück in die CR Go (Operator SDK) · Ansible · Helm-basiert die Realität – was der Operator baut StatefulSet(s) für die Broker (B1) headless Services je Replica (B3) ConfigMaps, Secrets, PVCs (B8/G3) Routes, PodDisruptionBudgets, Rollen (A5) dazu Betriebsaufgaben: rollende Version-Upgrades, Rebalancing, Backups Wiederherstellung, Zertifikat-Rotation alles gehalten per ownerReferences auf die CR CR löschen → der ganze Satz fällt weg (Garbage Collection) ziehst du an einem StatefulSet, dreht es der Operator zurück – die CR ist die einzige Wahrheit beobachtet kennt das Schema gleicht ab Ist-Zustand zurück in die Schleife Operator vs Helm – der Unterschied Helm (B11/B12): rendert, wendet an, hört auf – Drift erst beim nächsten upgrade Operator: läuft im Cluster, gleicht ständig ab, reagiert auf Ausfälle und Zeit Faustregel: einmaliges Deploy → Helm. Etwas, das betrieben werden muss → Operator viele Operatoren nutzen intern selbst ein Helm-Chart als Vorlage – plus die Schleife obendrauf wo Helm aufhört und ein Operator anfängt: B12 Capability Levels I–V – wie viel der Operator abnimmt I Basic Install · II Seamless Upgrades · III Full Lifecycle (Backup/Restore) IV Deep Insights (Metriken, Alerts) · V Auto Pilot (Auto-Scaling, Auto-Tuning) im OperatorHub (D3) steht das Level je Operator – sagt, wie viel du selbst noch machst Bibliothek: Kafka-, Postgres-, Keycloak-Operator verwalten die zustandsbehafteten Teile die 8 zustandslosen Spring-Dienste brauchen keinen Operator – ein Deployment reicht Merksatz: CRD = neuer Typ, Custom Resource = Sollzustand, Operator = die Schleife, die ihn Realität werden lässt und hält wie ein Operator installiert wird (OLM): D2 · hands-on eine Custom Resource anlegen und den Reconcile beobachten: D5 OCP-Unterschied: das Operator-Muster ist Kubernetes – OpenShift liefert OLM (D2) und OperatorHub (D3) mit, in Kubernetes pur muss man beides nachrüsten.
Ein Admin als Code. Eine CRD führt einen neuen Objekttyp ein, eine Custom Resource davon ist der Sollzustand („drei Broker, Version 3.7“). Der Operator ist ein Controller im Cluster, der in einer Reconcile-Schleife die Custom Resources beobachtet, mit der Realität vergleicht und handelt – StatefulSets anlegen, Version-Upgrades rollen, Backups fahren. Anders als Helm (B11) wendet er nicht einmal an, sondern gleicht ständig ab. Die CR ist die einzige Wahrheit – ziehst du direkt am StatefulSet, dreht der Operator es zurück.
  • ein Operator ist ein Controller plus eine CRD – die CRD führt einen neuen Objekttyp ein (kind: Kafka), danach gleichwertig zu Pod (A3)
  • eine Custom Resource dieses Typs ist der Sollzustand – nur was (spec.replicas: 3), nicht wie
  • der Operator läuft in einer Reconcile-Schleife – Custom Resources beobachten, Ist gegen Soll vergleichen, handeln, endlos
  • er reagiert auch auf Pod-Ausfälle, Zeit und externe Events, nicht nur auf CR-Änderungen – das ist der Unterschied zu einem einmaligen Deploy
  • was er baut: StatefulSets, Services, ConfigMaps, PVCs, Rollen – alles per ownerReferences an die CR gebunden, CR weg = alles weg
  • die CR ist die einzige Wahrheit – änderst du direkt am StatefulSet, dreht der Operator es beim nächsten Reconcile zurück
  • Unterschied zu Helm (B11): Helm rendert einmal und hört auf, der Operator gleicht ständig ab und heilt Drift
  • Faustregel: einmaliges Deploy → Helm, etwas das betrieben werden muss → Operator
  • Capability Levels I–V – Basic Install, Seamless Upgrades, Full Lifecycle (Backup/Restore), Deep Insights, Auto Pilot – im OperatorHub je Operator angegeben (D3)
  • Bibliothek heute: Postgres, Redis, Kafka, Keycloak laufen als Bitnami-Helm-Charts, nicht als Operatoren – einmal helm upgrade, danach kein Abgleich und keine Selbstheilung
  • der Operator wäre der nächste Schritt für Kafka oder Postgres, sobald Backup, Failover oder Rebalancing (Capability Level III+) gebraucht werden
  • die 8 Spring-Dienste + config-server sind einfache Deployments – kein Operator, kein StatefulSet, die brauchen nur ein Rolling Update
  • Befehle: oc get crd, oc api-resources, oc explain kafka.spec, oc get kafka -o yaml (Feld status)
  • OCP-Unterschied: das Muster ist Kubernetes – OpenShift liefert OLM (D2) und OperatorHub (D3) mit, reines Kubernetes muss beides nachrüsten
  • von Hand durchspielen: Operator-Reconcile und Service-Zertifikate – Soll/Ist-Schleife am eingebauten Service-CA-Operator sichtbar machen und optional eine echte ArgoCD-CR untersuchen

D2 OLM – CatalogSource, Subscription, InstallPlan, ClusterServiceVersion, OperatorGroup

OperatorenOLMSubscriptionCSVEX280

Der Operator Lifecycle Manager ist die Maschinerie hinter dem Installieren und Aktualisieren von Operatoren. Fünf Objekte spielen zusammen: eine CatalogSource (der Laden), eine Subscription (deine Bestellung), ein InstallPlan (die konkrete Liste, ggf. mit Freigabe), eine ClusterServiceVersion (das Paket-Manifest) und eine OperatorGroup (welche Namespaces).

CatalogSource → Subscription → InstallPlan → CSV → laufender Operator. CatalogSource ein Pod, der einen Katalog-Index serviert aus einem Image (gRPC auf :50051) im Namespace openshift-marketplace was drin ist: oc get packagemanifests Disconnected: eigenen Katalog per oc-mirror Subscription deine Bestellung: welcher Operator, welcher channel, welcher Namespace installPlanApproval: Automatic / Manual du legst an (oder die Konsole tut es, D3) bleibt bestehen – sie treibt auch die Upgrades InstallPlan die konkrete Liste: CRDs, RBAC, das Operator-Deployment, die CSV bei Manual: Zustand RequiresApproval oc patch installplan ... approved:true OLM löst hier auch Abhängigkeiten auf (D4) ClusterServiceVersion das Paket-Manifest des Operators (die „CSV“) Deployment-Spec, RBAC, eigene + benötigte CRDs Beispiel-Custom-Resources, Install-Modi, Version Phasen: Pending→InstallReady→Installing→Succeeded eine CSV je installierter Version, im Ziel-Namespace Operator läuft als Deployment reconciled seine CRs (D1) OperatorGroup – welche Namespaces der Operator sieht OwnNamespace – nur der eigene SingleNamespace – genau ein anderer MultiNamespace – eine Liste AllNamespaces – clusterweit (nur in openshift-operators) jeder Namespace mit installierten Operatoren braucht genau eine OperatorGroup nicht jeder Operator kann jeden Modus – die CSV sagt, welche er unterstützt zwei OperatorGroups im selben Namespace → alle Operatoren dort brechen was OLM sonst noch macht Upgrades: neuer Katalog-Inhalt im Channel → neuer InstallPlan → (Freigabe) → neue CSV ersetzt die alte Replaces-Kette: die CSV nennt, welche Version sie ablöst – OLM geht sie Schritt für Schritt Abhängigkeiten: eine CSV kann required CRDs/APIs deklarieren → OLM installiert den Anbieter mit RBAC: OLM legt die ServiceAccount + Rollen des Operators an (aus der CSV, A5/A6) alles über oc get catalogsource,subscription,installplan,csv,operatorgroup -A was klemmt: oc describe sub <name> – das Feld Conditions OLM selbst läuft als olm-operator + catalog-operator in openshift-operator-lifecycle-manager Merksatz: CatalogSource = Laden, Subscription = Bestellung + Upgrade-Kanal, InstallPlan = die Liste (ggf. Freigabe), CSV = das Paket, OperatorGroup = der Wirkbereich die Konsolen-Sicht darauf (OperatorHub, die vier Kataloge): D3 · hands-on mit Channels und Freigabe: D4 OCP-Unterschied: OLM ist bei OpenShift vorinstalliert – in Kubernetes pur installiert man es selbst (operator-sdk olm install), die Objekte sind identisch.
Fünf Objekte, eine Kette. Die CatalogSource ist ein Pod, der einen Katalog serviert. Deine Subscription sagt „Operator X, Channel Y, Freigabe automatisch oder manuell“. OLM erzeugt daraus einen InstallPlan – die konkrete Objektliste, die bei Manual auf deine Freigabe wartet. Ist er durch, installiert eine ClusterServiceVersion (das Paket-Manifest) den Operator. Die OperatorGroup legt fest, welche Namespaces er beobachtet. Dieselbe Subscription treibt später auch die Upgrades.
  • eine CatalogSource ist ein Pod, der einen Katalog-Index verfügbarer Operatoren aus einem Image serviert (Namespace openshift-marketplace)
  • eine Subscription ist deine Bestellung – welcher Operator, welcher channel, welcher Namespace, installPlanApproval: Automatic|Manual
  • ein InstallPlan ist die konkrete Objektliste, die OLM anlegen wird – CRDs, RBAC, das Operator-Deployment, die CSV
  • bei Manual steht der InstallPlan auf RequiresApproval bis oc patch installplan <n> --type=merge -p '{"spec":{"approved":true}}' oder der Konsolen-Button
  • eine ClusterServiceVersion (CSV) ist das Paket-Manifest – Deployment-Spec, RBAC, eigene und benötigte CRDs, Beispiel-CRs, Install-Modi
  • CSV-Phasen: Pending → InstallReady → Installing → Succeeded oder Failed – eine CSV je installierter Version
  • eine OperatorGroup legt den Wirkbereich fest: OwnNamespace, SingleNamespace, MultiNamespace, AllNamespaces
  • jeder Namespace mit Operatoren braucht genau eine OperatorGroup – zwei brechen alle Operatoren dort
  • auf der SNO ist noch kein Operator installiert – die Bibliothek-Infra kam per Helm, OLM läuft aber bereit (oc get catalogsource -A zeigt die vier Default-Kataloge)
  • der erste echte Kandidat wäre OpenShift GitOps (E3) oder Pipelines (E2) aus redhat-operators
  • AllNamespaces auf einem Single-Node mit Vorsicht – der Operator-Pod und alle Operanden teilen sich die ~11 GB mit OKD und dem library-Stack, SingleNamespace wo möglich
  • wo ein Operator konfiguriert ist: die CSV (oc get csv -n <ns>) und die CR-Instanzen unter Installed Operators in der Konsole
  • Upgrades: neuer Katalog-Inhalt im Channel → neuer InstallPlan → Freigabe → neue CSV ersetzt die alte über die replaces-Kette
  • OCP-Unterschied: OLM ist bei OpenShift vorinstalliert – reines Kubernetes braucht operator-sdk olm install
  • von Hand durchspielen: operator-installieren-olm.html – die OLM-Objekte am Beispiel OpenShift GitOps einmal komplett durchspielen

D3 OperatorHub – die vier Default-Kataloge, Installation über die Konsole

OperatorenOperatorHubKatalogeEX280

OperatorHub ist die Konsolen-Oberfläche über den CatalogSources (D2). Vier Kataloge sind ab Werk da – redhat-operators, certified-operators, community-operators, redhat-marketplace – sie unterscheiden sich in Support. Ein Klick auf „Install“ erzeugt im Hintergrund die Subscription und die OperatorGroup.

Vier Kataloge, ein Unterschied: wer supportet. redhat-operators von Red Hat gebaut und gepflegt voll unterstützt (im OCP-Abo enthalten) z. B. OpenShift GitOps, Pipelines, Serverless auf OKD: die gleichen Kataloge, kein Red-Hat-SLA erste Wahl, wenn es das Benötigte gibt certified-operators von Partnern (MongoDB, Crunchy, Confluent…) zertifiziert – Support beim Partner Red Hat prüft Verpackung + Kompatibilität oft die Produktions-Variante eines Community-Operators z. B. Crunchy Postgres, Confluent Kafka community-operators von der Community (operatorhub.io) kein Support – „wie besehen“ gut zum Lernen und für Nicht-Kritisches z. B. Strimzi (Kafka), CloudNativePG, cert-manager Bibliothek nutzt Strimzi + CloudNativePG (Community) redhat-marketplace kostenpflichtig – Kauf/Abo über marketplace.redhat.com Lizenz + Support beim Anbieter auf OKD in der Regel leer / nicht relevant Installation über die Konsole (Administrator-Perspektive) 1 Operator im OperatorHub wählen (Kachel zeigt Katalog + Capability Level, D1) 2 Update channel wählen (stable…) – das ist der Upgrade-Kanal 3 Installation mode: alle Namespaces oder ein bestimmter 4 Update approval: Automatic oder Manual (Freigabe je Upgrade, D4) im Hintergrund: die Konsole legt Subscription + OperatorGroup an (D2) danach: Operators → Installed Operators, dort die CRs anlegen (D5) gleiches per CLI & Sonderfälle alles listen: oc get packagemanifests -n openshift-marketplace Details eines Operators: oc describe packagemanifest <name> (Channels, CSVs) von Hand installieren: eine Subscription-YAML anlegen (GitOps-tauglich, E3) Disconnected: oc-mirror spiegelt einen Katalog, eine eigene CatalogSource zeigt darauf Default-Kataloge abschalten: OperatorHub-Objekt cluster, Feld disableAllDefaultSources ein eigener interner Katalog: mit dem opm-Tool ein Index-Image bauen Merksatz: OperatorHub = die Konsolen-Sicht auf die CatalogSources. Der Katalog sagt, wer supportet. Install = Subscription + OperatorGroup, im Hintergrund Bibliothek: community-operators für Strimzi/CloudNativePG – für Produktion wären die certified-operators die sichere Wahl OCP-Unterschied: die vier Namen sind auf OCP und OKD gleich – die Inhalte teils, aber redhat-marketplace und volle Zertifizierung sind an ein OCP-Abo gebunden.
Ein Store mit vier Regalen. redhat-operators ist voll unterstützt, certified-operators kommt von Partnern (Support dort), community-operators hat gar keinen Support, redhat-marketplace ist kostenpflichtig. Jeder Katalog ist eine CatalogSource (D2). Im OperatorHub wählst du Operator, Channel, Namespace-Umfang und Freigabe-Modus – die Konsole legt daraus Subscription und OperatorGroup an. Danach findest du ihn unter „Installed Operators“ und legst dort die Custom Resources an (D5).
  • OperatorHub ist die Konsolen-Oberfläche über den CatalogSources (D2) – unter Operators → OperatorHub
  • redhat-operators: von Red Hat, voll unterstützt im OCP-Abo – GitOps, Pipelines, Serverless, Logging
  • certified-operators: von Partnern, zertifiziert, Support beim Partner – Crunchy Postgres, Confluent Kafka
  • community-operators: von der Community, kein Support, gut zum Lernen – Strimzi, CloudNativePG, cert-manager
  • redhat-marketplace: kostenpflichtig, Kauf über marketplace.redhat.com
  • Install-Klick: Operator → Update channelInstallation modeUpdate approval – die Konsole legt im Hintergrund Subscription + OperatorGroup an (D2)
  • danach: Operators → Installed Operators, dort die Custom Resources anlegen (D5)
  • GitOps-tauglich: statt Klick eine Subscription-YAML ins Git legen (E3) – nachvollziehbar und im Rebuild dabei (E4)
  • Bibliothek hat bisher keinen Operator – die zustandsbehaftete Infra kam als Bitnami-Helm-Chart, nicht aus dem Hub
  • wenn doch: community-operators zum Lernen (Strimzi statt Bitnami-Kafka), certified-operators für echten Betrieb
  • auf der SNO kosten alle vier Default-Kataloge RAM – jeder ist ein Pod in openshift-marketplace. disableAllDefaultSources: true am OperatorHub-Objekt cluster plus nur der gebrauchte Katalog spart ~300 MB
  • CLI-Blick in einen Katalog: oc get packagemanifests -n openshift-marketplace, oc describe packagemanifest <name> zeigt Channels und CSVs
  • disconnected: oc-mirror spiegelt einen Katalog, eine eigene CatalogSource zeigt darauf
  • von Hand durchspielen: operator-installieren-olm.html – die vier Default-Kataloge von der CLI aus ansehen

D4 Einen Operator hands-on installieren – Channels, Dependency-Resolution, Approval

OperatorenInstallationChannelsDependenciesEX280

Drei Dinge entscheiden, wie eine Installation läuft. Der Channel ist der Versions-Zug (stable, alpha). Die Dependency-Resolution zieht benötigte Operatoren automatisch mit. Die Approval (Manual) lässt dich jedes Upgrade freigeben – so gatet man Produktion.

Channel = Versions-Zug · Dependencies = automatisch · Manual = du gibst frei. Channels – die Release-Züge stable / stable-v2 / alpha – je Channel eine Kette an CSVs die Subscription nennt einen Channel → OLM hält dich auf dessen Spitze Channel wechseln = ein Upgrade-Pfad (z. B. stablestable-v2) welche es gibt: oc describe packagemanifest <name> startingCSV in der Subscription pinnt eine bestimmte Version jede Version nennt per replaces/skips, was sie ablöst OLM geht die Kette Schritt für Schritt, nicht in einem Sprung Dependency-Resolution eine CSV kann required APIs / CRDs deklarieren OLM sucht im Katalog einen Anbieter und installiert ihn mit Beispiel: Service Mesh (F4) zieht Kiali + Jaeger + Elasticsearch nach der InstallPlan zeigt alle Objekte, auch die der Abhängigkeiten Konflikt: zwei Operatoren wollen dieselbe CRD in anderer Version → Installation blockiert nachsehen: oc get installplan -o yaml, Feld spec.clusterServiceVersionNames Abhängigkeiten deinstallieren sich nicht mit – von Hand aufräumen Approval – Upgrades gaten installPlanApproval: Automatic – OLM zieht sofort mit Manual – jeder neue InstallPlan wartet auf dich Zustand RequiresApproval in der Konsole (Button) oder: oc patch installplan <ip> --type=merge \ -p '{"spec":{"approved":true}}' so testet man erst in einem Cluster, dann gibt man Prod frei „stecken geblieben“ ist meist ein nicht freigegebener InstallPlan der Ablauf beim Beobachten CSV-Phasen: Pending → InstallReady → Installing → Succeeded (oder Failed / Replacing) oc get csv -w -n <ns> oc describe sub <name> -n <ns> das Feld status.conditions der Subscription sagt, was hängt (Katalog weg, InstallPlan offen, Konflikt) Rollback – unschön es gibt kein helm rollback-Äquivalent Weg: Subscription + CSV löschen, ältere Version neu abonnieren CRDs und Custom Resources bleiben – ein CRD-Downgrade kann Daten kosten darum: erst im Test-Cluster, Release Notes des Operators lesen maxOpenShiftVersion in der CSV kann sogar einen Cluster-Upgrade blocken (G5) Merksatz: Channel steuert welche Version, Approval steuert wann, Dependency-Resolution zieht das Benötigte mit Produktion: immer Manual approval, ein Test-Cluster davor, alte CSVs nicht sofort löschen jetzt den Operator wirklich nutzen – eine Custom Resource anlegen und den Reconcile beobachten: D5 OCP-Unterschied: identisch – Channels, Approval und Dependency-Resolution sind reine OLM-Mechanik, in OKD und OCP gleich.
Drei Hebel. Der Channel ist der Versions-Zug – die Subscription hält dich auf seiner Spitze, ein Channel-Wechsel ist ein Upgrade-Pfad. Die Dependency-Resolution liest die required-APIs einer CSV und installiert die Anbieter mit. Die Approval auf Manual lässt jeden neuen InstallPlan auf deine Freigabe warten – so testest du erst und gibst dann Produktion frei. Der häufigste „hängt“-Fall ist ein nicht freigegebener InstallPlan.
  • Channels (stable, latest, alpha) sind Release-Züge – die Subscription nennt einen, OLM hält dich auf dessen Spitze
  • ein Channel-Wechsel ist ein Upgrade-PfadstartingCSV in der Subscription pinnt eine bestimmte Version
  • jede Version nennt per replaces/skips, was sie ablöst – OLM geht die Kette Schritt für Schritt, nicht in einem Sprung
  • Dependency-Resolution: eine CSV deklariert required APIs → OLM findet einen Anbieter im Katalog und installiert ihn mit
  • der InstallPlan zeigt alle Objekte, auch die der Abhängigkeiten – die deinstallieren sich nicht mit
  • Approval: Automatic zieht sofort mit, Manual lässt jeden InstallPlan warten (Zustand RequiresApproval)
  • hands-on auf der SNO: oc apply -f subscription.yaml für OpenShift GitOps – das ist der erste Operator, den das Projekt real braucht (E3)
  • auf einem Single-Node immer Manual – ein Auto-Upgrade mitten in einer Demo ist unschön, und der Reconcile-Sturm eines frischen Operators kostet kurz CPU
  • freigeben: der Konsolen-Button oder oc patch installplan <ip> --type=merge -p '{"spec":{"approved":true}}'
  • zusehen: oc get csv -w -n openshift-gitops-operator – Phasen Pending → InstallReady → Installing → Succeeded
  • was hängt steht in status.conditions der Subscription – oc describe sub <name>
  • Rollback ist unschön: Subscription + CSV löschen, ältere Version neu abonnieren – CRDs und Custom Resources bleiben
  • maxOpenShiftVersion in der CSV kann sogar ein Cluster-Upgrade blocken (G5) – vor einem OKD-Upgrade die Operator-CSVs prüfen
  • OCP-Unterschied: identisch – Channels, Approval und Dependency-Resolution sind reine OLM-Mechanik
  • von Hand durchspielen: operator-installieren-olm.html – eine Subscription mit Manual-Approval abonnieren, den InstallPlan live genehmigen, die CSV-Phasen verfolgen

D5 Operator-verwaltete Last – eine Custom Resource anlegen, Reconcile beobachten

OperatorenCustom ResourceReconcilehands-on

Jetzt der Nutzen aus D1–D4. Du legst eine Custom Resource an (kind: Kafka, spec.replicas: 3) und siehst zu, wie der Operator daraus StatefulSets, Services und ConfigMaps baut. Änderst du die CR, gleicht er nach. Löschst du sie, räumt er alles weg.

Du schreibst die CR – der Operator baut und hält den ganzen Satz. 1 · Custom Resource anlegen apiVersion: kafka.strimzi.io/v1beta2 kind: Kafka metadata: {name: events, namespace: library} spec: {kafka: {replicas: 3, version: "3.7"}} oc apply -f kafka.yaml das Schema prüft die CRD (D1) – Tippfehler in spec = sofort abgewiesen Beispiel-CRs liefert die CSV mit (oc get csv <n> -o jsonpathalm-examples) Operator 2 · reconcile sieht die neue CR, liest spec rechnet aus, was fehlt legt die Objekte an, in Reihenfolge schreibt status.conditions zurück in die CR Ready: True / Reconciling: True 3 · was entsteht (Label des Operators) StatefulSet events-kafka – 3 Broker-Pods mit stabiler Identität (B1) headless Service events-kafka-brokers – ein DNS-Name je Pod (B3) Service events-kafka-bootstrap – der Einstiegspunkt für Clients ConfigMaps (Broker-Config), Secrets (TLS), PVCs je Broker (G3) PodDisruptionBudget, NetworkPolicy, RBAC alles mit ownerReferences auf die Kafka-CR beobachten: oc get kafka,statefulset,svc,pvc -l strimzi.io/cluster=events -w der Operator managt auch die Broker-Rolling-Updates bei Version-Wechsel plus: KafkaTopic- und KafkaUser-CRs für Topics und Zugänge watch apply 4 · ändern und löschen CR auf replicas: 5 patchen → der Operator skaliert das StatefulSet hoch und rebalanced CR löschen → der ganze Satz fällt weg (Garbage Collection über ownerReferences) direkt am StatefulSet ziehen → der Operator dreht es beim nächsten Reconcile zurück (D1) Bibliothek / cloud-host eine Kafka-CR für das Event-Rückgrat, eine PostgresCluster-CR für die DB KafkaTopic-CRs je Fachdomäne – die Topics stehen dann auch im Git (E3) die 8 Spring-Dienste bleiben normale Deployments – kein Operator nötig Merksatz: CR anlegen → Operator reconciled → ein ganzer Objektsatz → CR ändern skaliert, CR löschen räumt weg Debugging: oc describe kafka events (Events + Conditions), oc logs deploy/<operator> -n <ns> OCP-Unterschied: keiner – sobald der Operator (D2/D3) läuft, ist das reine Custom-Resource-Mechanik, in OKD und OCP gleich.
Die CR ist der ganze Auftrag. Du schreibst kind: Kafka, spec.replicas: 3 und oc apply. Der Operator sieht die CR, rechnet aus, was fehlt, und legt StatefulSets, headless Services, ConfigMaps, Secrets und PVCs an – alles per ownerReferences an die CR gebunden. Er schreibt status.conditions zurück. replicas: 5 → er skaliert. CR löschen → alles fällt weg. Direkt am StatefulSet ziehen → er dreht es zurück.
  • du legst eine Custom Resource an (oc apply -f kafka.yamlkind: Kafka, spec.kafka.replicas: 3) – das Schema prüft die CRD (D1)
  • Beispiel-CRs liefert die CSV mit (Annotation alm-examples) – ein guter Startpunkt
  • der Operator reconciled – sieht die CR, rechnet aus was fehlt, legt die Objekte in Reihenfolge an
  • was entsteht: StatefulSets (B1), headless Services (B3), ConfigMaps, Secrets, PVCs (G3), PDB, NetworkPolicy, RBAC
  • alles mit ownerReferences auf die CR – CR löschen räumt den ganzen Satz weg (Garbage Collection)
  • der Operator schreibt status.conditions zurück in die CRReady: True, Reconciling: True
  • wo die Operator-Config liegt: in der CR selbst (oc get kafka events -o yaml), nicht in einer ConfigMap – die ConfigMaps darunter erzeugt und überschreibt der Operator
  • CR ändern (replicas: 5) → der Operator skaliert die StatefulSets hoch und rebalanced – beim Helm-Chart wäre das values.yaml + helm upgrade
  • direkt am StatefulSet ziehen → der Operator dreht es beim nächsten Reconcile zurück – die CR ist die einzige Wahrheit (D1)
  • nur der Operator merkt, wenn ein Broker wegfällt, und ersetzt ihn – das Bitnami-Helm-Chart tut das nicht
  • oft bringt der Operator weitere CRDs mit (KafkaTopic, KafkaUser) – auch die gehören dann ins Git (E3)
  • Bibliothek heute: keine Operator-CRs – Kafka und Postgres kommen aus Bitnami-Helm-Charts. Der Umbau wäre helm uninstall, Strimzi-Operator, eine Kafka-CR – und die Helm-Werte weichen CRD-Feldern
  • Debugging: oc describe kafka events (Events + Conditions), oc logs deploy/<operator> -n <ns>
  • OCP-Unterschied: keiner – sobald der Operator läuft, ist das reine Custom-Resource-Mechanik
  • von Hand durchspielen: Reconcile beobachten – ein verwaltetes TLS-Secret löschen und seine Neuerzeugung beweisen; optional danach eine ArgoCD-Custom-Resource anlegen

Block E – CI/CD auf OpenShift

E1 Jenkins auf OCP vs der OpenShift-Weg – wer die CPU im Cluster verbraucht

CI/CDJenkinsOpenShift-WegRessourcen

Baust du im Cluster, kostet jeder Build CPU und RAM, die deinen Diensten fehlen. Jenkins im Cluster (Master-Pod plus Agent-Pods je Build) ist der teure Weg. Der OpenShift-Weg: entweder Tekton (flüchtige Build-Pods, E2) oder CI ausserhalb, die nur noch über ein ServiceAccount-Token deployt. Die Bibliothek baut ausserhalb.

Wer baut, verbraucht CPU. Die Frage ist nur: im Cluster oder ausserhalb? A · Jenkins im Cluster ein Master-Pod (die alte OpenShift-Jenkins-Vorlage) plus Agent-Pods je Build (Kubernetes-Plugin) jeder Build zieht Maven, kompiliert, testet – im Cluster konkurriert mit den Diensten um CPU und RAM braucht ein PVC für $JENKINS_HOME auf SNO oder dem kleinen Bibliothek-Cluster: keine gute Idee Vorteil: alles an einem Ort, ein OAuth-Login (A4) die Vorlage wird nicht mehr gepflegt – Neuanlage vermeiden B · der OpenShift-Weg Tekton (E2): Build-Pods, aber zweckgebaut und flüchtig kein Master, kein PVC – ein PipelineRun pro Lauf oder CI ausserhalb: GitHub Actions, GitLab CI, Jenkins extern baut ausserhalb, deployt nur über ein SA-Token in den Cluster der Cluster trägt nur die Last der Dienste, nicht der Builds Nachteil: zwei Systeme, das Token muss verwaltet werden Argo CD (E3) macht daraus reinen Pull statt Push Faustregel: baue dort, wo CPU billig ist – deploye dort, wo die App läuft Abwägung Steuerungsebene: A belastet sie stark, B kaum Secrets: A hat alles intern, B muss ein Deploy-Token und Registry-Zugang nach aussen geben Portabilität: B läuft gegen jeden Cluster gleich, A ist an diesen einen Cluster gebunden Wartung: A ein weiterer Dienst im Cluster, B ein CI-System Audit: B loggt jeden Deploy als der pipeline-SA (A6) EX280 kennt beide – der Trend geht klar zu B Bibliothek / cloud-host – der konkrete Weg Jenkins läuft ausserhalb in einem lokalen docker-compose (der Hetzner-Server war weg, Jenkins kam vom Snapshot ins Shared-Infra-Compose) 1 Jenkins baut je Dienst ein Image und pusht nach ghcr.io oc login <api> --token=$PIPELINE_TOKEN 2 Jenkins ruft je Dienst helm upgrade --install catalog ./chart -n library --atomic 3 authentifiziert als system:serviceaccount:library:pipeline – gebunden an admin + system:image-builder + registry-editor (A5) der Cluster sieht nur helm upgrade-Aufrufe – keine Build-Pods, kein Jenkins-Pod, kein PVC der nächste Schritt wäre Tekton im Cluster (E2) oder Argo CD als Pull-Modell (E3) – beide ersetzen den Push von aussen oder Merksatz: im Cluster bauen = Build-Last auf der Steuerungsebene. Ausserhalb bauen und nur deployen = der Cluster trägt nur die Dienste. ein Deploy von aussen braucht genau eins: ein pipeline-SA-Token mit gerade den nötigen Rollen (A5) – nicht cluster-admin OCP-Unterschied: die alte Jenkins-Vorlage ist OpenShift-eigen (Templates, OAuth-Login, S2I-Jenkinsfile). Tekton kommt als OpenShift-Pipelines-Operator, ist aber Standard-Kubernetes-Tekton.
Wer baut, verbraucht CPU. Jenkins im Cluster heisst: ein Master-Pod plus Agent-Pods je Build, die mit deinen Diensten um CPU und RAM konkurrieren, plus ein PVC für $JENKINS_HOME. Der OpenShift-Weg ist Tekton (flüchtige Build-Pods) oder CI ausserhalb, die nur noch über ein ServiceAccount-Token in den Cluster deployt. Die Bibliothek baut ausserhalb: Jenkins (lokales compose) pusht nach ghcr.io und ruft helm upgrade --install je Dienst als system:serviceaccount:library:pipeline – der Cluster sieht keine Build-Last.
  • Jenkins im Cluster: ein Master-Pod plus Agent-Pods je Build (Kubernetes-Plugin) – jeder Build kostet Cluster-CPU, konkurriert mit den Diensten, braucht ein PVC für $JENKINS_HOME
  • die alte OpenShift-Jenkins-Vorlage (Templates, OAuth-Login, S2I-Jenkinsfile) wird nicht mehr gepflegt – keine Neuanlage
  • der OpenShift-Weg, Variante Tekton (E2): Build-Pods, aber zweckgebaut und flüchtig – kein Master, kein PVC, ein PipelineRun je Lauf
  • der OpenShift-Weg, Variante CI ausserhalb: GitHub Actions / GitLab CI / externer Jenkins baut ausserhalb und deployt nur über ein SA-Token in den Cluster
  • Faustregel: baue dort, wo CPU billig ist – deploye dort, wo die App läuft
  • Bibliothek: Jenkins läuft ausserhalb in einem lokalen docker-compose, baut je Dienst ein Image und pusht nach ghcr.io
  • dann ruft Jenkins je Dienst helm upgrade --install catalog ./chart -n library --atomic
  • authentifiziert als system:serviceaccount:library:pipeline, gebunden an admin, system:image-builder und registry-editor in library (A5)
  • der Cluster sieht nur helm upgrade-Aufrufe – keine Build-Pods, kein Jenkins-Pod, kein PVC
  • Abwägung Steuerungsebene: Jenkins-im-Cluster belastet sie stark, extern-bauen kaum
  • Abwägung Secrets: intern hat Jenkins alles, extern muss man ein Deploy-Token und Registry-Zugang nach aussen geben
  • Abwägung Portabilität: externe CI läuft gegen jeden Cluster gleich, Jenkins-im-Cluster ist an diesen einen gebunden
  • ein Deploy von aussen braucht genau ein Token mit gerade den nötigen Rollen – nicht cluster-admin
  • OCP-Unterschied: die Jenkins-Vorlage ist OpenShift-eigen – Tekton kommt als OpenShift-Pipelines-Operator, ist aber Standard-Kubernetes-Tekton
  • von Hand durchspielen: tekton-pipelines-openshift.html – Jenkins-im-Cluster gegen den OpenShift-Weg, den Pipelines-Operator installieren

E2 OpenShift Pipelines (Tekton) – Task, Pipeline, PipelineRun, Workspaces

CI/CDTektonPipelinePipelineRun

Tekton ist CI als Kubernetes-Objekte. Ein Task ist eine Reihe von steps, jeder Step ein Container. Eine Pipeline reiht Tasks. Ein PipelineRun ist ein Lauf und erzeugt Pods. Daten zwischen Tasks fließen über Workspaces. Alles flüchtig – kein Master, kein PVC für einen Server.

TaskPipelinePipelineRun – jeder Step ein Container, jeder Lauf ein Satz Pods. Task eine geordnete Liste von steps jeder Step = ein Container (Image + Kommando) step: git-clone step: mvn package step: buildah bud + push Steps teilen sich einen Pod, laufen nacheinander wiederverwendbar aus dem Tekton Hub / als ClusterTask Pipeline eine geordnete oder DAG-Anordnung von Tasks clone → test → build → deploy runAfter ordnet, sonst laufen Tasks parallel params rein, results raus – so gibt ein Task den Image-Digest an den nächsten weiter finally-Tasks laufen immer (Aufräumen, Melden) die Pipeline selbst startet nichts – sie ist die Vorlage when-Ausdrücke: Tasks bedingt überspringen PipelineRun eine Ausführung der Pipeline erzeugt je Task einen TaskRun → je TaskRun einen Pod bindet konkrete params und workspaces läuft als der pipeline-SA (A6) – dessen RBAC begrenzt ihn nach dem Lauf werden Pods bereinigt – per Cleanup/Retention Status + Logs: tkn pr logs -f, die Konsole (Pipelines-Tab) bleibt als Objekt für die Historie (bis aufgeräumt) weiterhin Cluster-CPU – aber nur während des Laufs Workspace geteilter Speicher zwischen Tasks ein PVC (bleibt), emptyDir (nur der Run) oder eine ConfigMap / ein Secret so kommt der geklonte Code von clone zu build der Maven-Cache als PVC-Workspace → schnellere Builds volumeClaimTemplate: ein frisches PVC je Run ohne Workspace kann ein Task nichts an den nächsten geben Secret-Workspace: die Registry-Credentials für den Push Triggers – ein Git-Webhook wird zum PipelineRun EventListener – ein Pod mit einer Route, nimmt den Webhook an TriggerBinding – zieht Werte aus dem Payload (Branch, Commit, Repo) TriggerTemplate – die Vorlage für den PipelineRun, die entsteht Interceptors prüfen die Webhook-Signatur (GitHub, GitLab) und filtern (nur main) Alternative: tkn pipeline start von Hand, oder ein Cron-PipelineRun Pipelines as Code (PAC): der .tekton/-Ordner im Repo definiert alles Werkzeuge & Bibliothek tkn-CLI: tkn pipeline start, tkn pr ls, tkn pr logs -f der OpenShift-Pipelines-Operator bringt Tekton + die Konsolen-Ansicht Bibliothek: git-clone → mvn test → buildah → helm upgrade je Dienst das wäre der Weg weg von Jenkins (E1) und rein in den Cluster Deploy per Argo CD (E3) statt helm upgrade macht die Pipeline noch schlanker Konsole zeigt den DAG live, grün/rot je Task Merksatz: Task = Steps (Container), Pipeline = Tasks (Reihenfolge), PipelineRun = ein Lauf (Pods), Workspace = wie Daten zwischen Tasks fließen alles laufbezogen – Pods werden per Cleanup/Retention bereinigt, kein dauerhafter Server-Prozess wie bei Jenkins (E1) OCP-Unterschied: der Operator heißt OpenShift Pipelines und bringt die Konsolen-Integration und ein paar Extra-ClusterTasks – darunter ist Standard-Tekton.
CI als Kubernetes-Objekte. Ein Task ist eine Reihe von steps, jeder ein Container. Eine Pipeline ordnet Tasks (Reihenfolge oder DAG). Ein PipelineRun ist ein Lauf und erzeugt je Task einen Pod – diese laufbezogenen Pods und Objekte werden nach dem Lauf durch konfigurierte Cleanup-/Retention-Regeln bereinigt; kein Master, kein PVC für einen Server. Daten zwischen Tasks fließen über Workspaces (ein PVC, emptyDir, ein Secret). Ein EventListener mit TriggerBinding und TriggerTemplate macht aus einem Git-Webhook einen PipelineRun.
  • ein Task ist eine geordnete Liste von stepsjeder Step ist ein Container (Image + Kommando), die Steps teilen sich einen Pod
  • eine Pipeline ist eine geordnete oder DAG-Anordnung von TasksrunAfter ordnet, sonst laufen sie parallel, finally-Tasks laufen immer
  • params gehen rein, results kommen raus – so gibt ein Task den Image-Digest an den nächsten weiter
  • ein PipelineRun ist eine Ausführung – er erzeugt je Task einen TaskRun und je TaskRun einen Pod
  • der PipelineRun läuft als der pipeline-SA (A6) – dessen RBAC begrenzt, was die Pipeline anfassen darf
  • Cleanup/Retention bereinigt die laufbezogenen Pods und TaskRuns nach dem Lauf – kein dauerhafter Server-Prozess wie bei Jenkins (E1), Cluster-CPU fällt nach dem Lauf wieder ab
  • ein Workspace ist geteilter Speicher zwischen Tasks: ein PVC (bleibt), emptyDir (nur der Run), oder eine ConfigMap / ein Secret
  • so kommt der geklonte Code von clone zu build, und der Maven-Cache als PVC-Workspace macht Builds schneller
  • Triggers: ein EventListener (Pod + Route) nimmt den Webhook an, TriggerBinding zieht Werte aus dem Payload, TriggerTemplate ist die PipelineRun-Vorlage
  • Interceptors prüfen die Webhook-Signatur und filtern (nur main) – Alternative: tkn pipeline start oder ein Cron-PipelineRun
  • wiederverwendbare Tasks aus dem Tekton Hub oder als ClusterTask: git-clone, buildah, s2i
  • Werkzeuge: die tkn-CLI (tkn pr logs -f), der OpenShift-Pipelines-Operator mit Konsolen-Ansicht (DAG live, grün/rot je Task)
  • Bibliothek: git-clone → mvn test → buildah → helm upgrade je Dienst wäre der Weg weg von Jenkins (E1) und rein in den Cluster
  • OCP-Unterschied: der Operator heißt OpenShift Pipelines und bringt Konsolen-Integration und Extra-ClusterTasks – darunter ist Standard-Tekton
  • von Hand durchspielen: tekton-pipelines-openshift.html – einen echten Task, eine Pipeline mit Workspace und einen PipelineRun live bauen

E3 OpenShift GitOps (Argo CD) – Application, Sync-Waves, das App-of-Apps-Muster

CI/CDArgo CDGitOpsSync-Waves

Argo CD vergleicht dauernd, was in Git steht (das Soll) mit dem, was im Cluster läuft (das Ist), zeigt die Abweichung und gleicht ab – von Hand oder automatisch. Mit selfHeal schließt es die Lücke, die Helm lässt (B12): eine manuelle Änderung am Cluster wird zurückgedreht. Eine Application zeigt auf {Repo, Pfad} und {Cluster, Namespace}.

Git = Soll, Cluster = Ist – Argo vergleicht dauernd und gleicht ab. Git-Repo Manifeste / Helm-Charts / Kustomize der einzige Ort der Wahrheit jede Änderung ist ein Commit – reviewbar, nachvollziehbar, umkehrbar (git revert) Secrets: nicht im Klartext – E5 Argo CD vergleicht Git (Soll) mit dem Cluster (Ist), dauernd Status je App: Synced / OutOfSync + ein Diff Sync: manual (du drückst) oder automated automated.prune: aus Git gelöscht → auch im Cluster weg automated.selfHeal: manuelle Cluster-Änderung → zurückgedreht → genau die Drift-Lücke, die Helm lässt (B12), ist zu Health je Resource (Healthy/Progressing/Degraded) Pull-Modell: Argo läuft im Ziel-Cluster und zieht – kein Push von aussen (E1) Rendering: Helm/Kustomize rendert Argo selbst, dann apply Cluster (Ist) Namespaces, Deployments, Services, Routes, Operatoren was Argo hier findet, gleicht es gegen Git ab Argo darf nur, was seine SA erlaubt (A5) – sonst bleibt die App OutOfSync mit einem RBAC-Fehler mehrere Ziel-Cluster möglich (ein Argo, viele Cluster) liest applyt / heilt Application – das eine Objekt, das alles verbindet source: repoURL + path + targetRevision (Branch/Tag/SHA) destination: server (Cluster) + namespace syncPolicy: automated / prune / selfHeal / CreateNamespace liegt selbst in einem Namespace (meist openshift-gitops) ignoreDifferences: Felder, die ein Operator setzt, nicht als Drift werten argocd app get, argocd app sync, argocd app diff · die UI zeigt den Baum Reihenfolge & Skalierung sync-wave-Annotation ordnet: Namespace (-2) < CRD (-1) < Config (0) < Workload (1) App-of-Apps: eine Wurzel-Application, deren Pfad weitere Applications enthält → ein Einstiegspunkt verwaltet den ganzen Cluster ApplicationSet: erzeugt Applications aus einer Vorlage – pro Cluster, pro Umgebung, pro Verzeichnis Resource-Hooks (PreSync/PostSync): DB-Migration wie Helm-Hooks (B12) der GitOps-Operator liefert Argo + Dex + eine openshift-gitops-Instanz + die Route Merksatz: eine Application = {Repo, Pfad} → {Cluster, Namespace}. Argo vergleicht dauernd und synct – selfHeal hält das Soll durch. Bibliothek: eine Wurzel-App, je Dienst eine Application auf das Helm-Chart – Jenkins baut nur noch das Image, den Rest zieht Argo OCP-Unterschied: OpenShift GitOps ist Argo CD als Operator mit Konsolen-Integration und OpenShift-Login – die Argo-Objekte sind Standard.
Git ist das Soll, der Cluster das Ist. Eine Application verbindet {Repo, Pfad, Revision} mit {Cluster, Namespace}. Argo CD vergleicht beide dauernd, zeigt OutOfSync plus einen Diff und synct – von Hand oder automated. prune löscht, was aus Git verschwindet, selfHeal dreht manuelle Cluster-Änderungen zurück – genau die Drift-Lücke, die Helm lässt (B12). sync-wave ordnet die Reihenfolge, App-of-Apps macht eine Wurzel-App zum Einstiegspunkt für den ganzen Cluster.
  • eine Application (ein CR) zeigt auf {Repo + Pfad + Revision} und {Ziel-Cluster + Namespace}
  • Argo CD vergleicht dauernd Git (Soll) mit dem Cluster (Ist) – Status Synced/OutOfSync plus ein Diff je Resource
  • Sync: manual (du drückst den Knopf) oder automated
  • automated.prune: aus Git gelöscht → auch im Cluster weg
  • automated.selfHeal: eine manuelle Cluster-Änderung wird zurückgedreht – die Drift-Lücke von Helm (B12) ist damit zu
  • Pull-Modell: Argo läuft im Ziel-Cluster und zieht aus Git – kein Push von aussen (E1), kein Deploy-Token nach draussen
  • Argo rendert Helm/Kustomize selbst und macht dann apply – kein helm im Spiel
  • Argo darf nur, was seine SA erlaubt (A5) – sonst bleibt die App OutOfSync mit einem RBAC-Fehler
  • sync-wave-Annotation ordnet: Namespace (-2) < CRD (-1) < Config (0) < Workload (1)
  • App-of-Apps: eine Wurzel-Application, deren Pfad weitere Application-Manifeste enthält – ein Einstiegspunkt für den ganzen Cluster
  • ApplicationSet: erzeugt Applications aus einer Vorlage – pro Cluster, pro Umgebung, pro Verzeichnis
  • Resource-Hooks (PreSync/PostSync): DB-Migration, wie die Helm-Hooks (B12)
  • Werkzeuge: argocd app sync, argocd app diff, die Argo-UI als Route, der OpenShift-GitOps-Operator (Argo + Dex + OpenShift-Login)
  • Bibliothek: eine Wurzel-App, je Dienst eine Application auf das Helm-Chart – Jenkins baut nur noch das Image, den Rest zieht Argo
  • von Hand durchspielen: gitops-argocd-openshift.html – eine Application anlegen, Drift erzeugen und selfHeal live zusehen

E4 GitOps-Rebuild – den Cluster aus Git wiederherstellen (Disaster-Recovery-Übung)

CI/CDGitOpsDisaster-RecoveryRebuild

Wenn alles in Git steht, ist ein Cluster wegwerfbar. Die Übung: zerstören, neu installieren, Argo CD bootstrappen – und Argo zieht den ganzen Rest aus Git zurück. Was nicht in Git liegt (Klartext-Secrets, PVC-Daten, Installer-Config) muss separat gelöst sein. „Wiederhergestellt“ heißt: Workloads ja, Daten nur bei separatem Backup.

Cluster weg – und in einer Stunde wieder da, bis auf die Daten. 1 · zerstören sno.ps1 destroy Server, Snapshot, IP, Firewall 2 · neu installieren sno.ps1 install frischer OKD-Single-Node (~35 min) 3 · Argo bootstrappen GitOps-Operator + Wurzel-Application ein oc apply -f bootstrap/ 4 · Argo zieht alles App-of-Apps → je Dienst eine App (E3) reconcile bis alles Synced & Healthy 5 · grün Workloads laufen, Routes antworten Daten: nur wenn separat gesichert was in Git liegt – kommt automatisch zurück Namespaces, RBAC (Roles, RoleBindings), NetworkPolicy (F2) Deployments, Services, Routes, ConfigMaps, HPAs Helm-Werte-Overrides je Umgebung Operator-Subscriptions + OperatorGroups (D2) die Custom Resources der Operatoren (Kafka, PostgresCluster, D5) die Argo-Application-Objekte selbst (App-of-Apps) Reihenfolge per sync-wave: Secrets-Maschinerie (-1) → Config (0) → Workloads (1) Cluster-Config (Ingress, OAuth-IdP, image-registry): auch als Manifeste in Git der zurückgestellte --rebuild-Flag = genau diese fünf Schritte in einem Kommando was nicht in Git liegt – muss separat gelöst sein Klartext-Secrets → Sealed Secrets in Git (Controller entschlüsselt) oder ESO zieht aus einem externen Store – muss in Wave -1 kommen (E5) bei Sealed Secrets: der Sealing-Key ist clusterspezifisch → sichern PVC-Daten (DB-Inhalt, Uploads) → ein echtes Backup: Velero oder Volume-Snapshots (G3) – ein Rebuild stellt sie nicht her Installer-Config: install-config.yaml, das echte Pull-Secret, die reservierte IP – liegen bei sno.ps1, nicht im App-Repo etcd-Backup (G7) ist die andere DR-Strategie: den Cluster-Zustand wiederherstellen statt neu bauen – für Multi-Node, nicht für SNO-Wegwerf Merke: GitOps rettet die Struktur, nicht den Inhalt Merksatz: destroy → install → Argo bootstrap → Argo zieht alles. Workloads sind in Minuten zurück, Daten nur mit eigenem Backup. der --rebuild-Flag im cloud-host-Skript war bis Block E zurückgestellt, weil er GitOps voraussetzt – ab hier baubar Time-to-green: SNO-Install ~35 min, Argo-Sync ~10 min, Operatoren-Reconcile ~10 min – grob eine Stunde bis alles läuft OCP-Unterschied: identisch – das Muster ist Argo CD + IaC, unabhängig von OKD oder OCP. Nur der Installer-Teil unterscheidet sich (Assisted vs kexec).
Der Cluster ist wegwerfbar – die Daten nicht. Die Übung: sno.ps1 destroysno.ps1 install → den GitOps-Operator und die Wurzel-Application anwenden → Argo zieht Namespaces, RBAC, Workloads, Operator-Subscriptions und die Argo-Apps selbst aus Git zurück. Was nicht in Git liegt, muss separat gelöst sein: Klartext-Secrets (Sealed Secrets / ESO, frühe Sync-Wave), PVC-Daten (ein echtes Backup, Velero / Snapshots), die Installer-Config. GitOps rettet die Struktur, nicht den Inhalt.
  • der Ablauf: sno.ps1 destroysno.ps1 installArgo CD bootstrappen (Operator + Wurzel-Application) → Argo zieht alles aus Git
  • was in Git liegt und automatisch zurückkommt: Namespaces, RBAC, NetworkPolicy, Deployments/Services/Routes, ConfigMaps, HPAs
  • auch in Git: Helm-Werte-Overrides je Umgebung, Operator-Subscriptions und OperatorGroups (D2), die Custom Resources der Operatoren (D5)
  • und in Git: die Argo-Application-Objekte selbst (App-of-Apps), sowie die Cluster-Config als Manifeste (Ingress, OAuth-IdP, image-registry)
  • Reihenfolge per sync-wave: die Secrets-Maschinerie in Wave -1, Config in 0, Workloads in 1
  • was nicht in Git liegt – Klartext-Secrets: über Sealed Secrets in Git (Controller entschlüsselt) oder ESO aus einem externen Store, muss in Wave -1 (E5)
  • bei Sealed Secrets ist der Sealing-Key clusterspezifisch – er muss separat gesichert werden, sonst sind die Secrets nach dem Rebuild unlesbar
  • PVC-Daten (DB-Inhalt, Uploads) brauchen ein echtes Backup: Velero oder Volume-Snapshots (G3) – ein Rebuild stellt sie nicht her
  • Installer-Config (install-config.yaml, das echte Pull-Secret, die reservierte IP) liegt bei sno.ps1, nicht im App-Repo
  • etcd-Backup (G7) ist die andere DR-Strategie: den Cluster-Zustand wiederherstellen statt neu bauen – für Multi-Node, nicht für den SNO-Wegwerf
  • der zurückgestellte --rebuild-Flag im cloud-host-Skript ist genau diese fünf Schritte in einem Kommando – ab Block E baubar
  • Time-to-green: SNO-Install ~35 min, Argo-Sync ~10 min, Operatoren-Reconcile ~10 min – grob eine Stunde
  • „Wiederhergestellt“ heißt ehrlich: Workloads ja, Daten nur bei separatem Backup – GitOps rettet die Struktur, nicht den Inhalt
  • OCP-Unterschied: identisch – das Muster ist Argo CD plus Infrastructure-as-Code, nur der Installer-Teil unterscheidet sich
  • von Hand durchspielen: gitops-argocd-openshift.html – der Rebuild-Ablauf als Checkliste, ohne den echten Cluster zu zerstören

E5 Secrets in Git – Sealed Secrets bzw. External Secrets

CI/CDSecrets in GitSealed SecretsExternal Secrets

Ein Klartext-Secret in Git ist base64 – also lesbar. In GitOps (E3) braucht es einen Umweg. Drei Wege: Sealed Secrets (Key im Cluster), External Secrets Operator (Store außerhalb), SOPS (Key beim Deployer). Die Secret-Maschinerie muss vor den Workloads syncen – Sync-Wave -1.

Klartext-Secret in Git = lesbar. Drei Wege, es trotzdem zu versionieren. Sealed Secrets (Bitnami) 1 Controller hat ein Schlüsselpaar im Cluster 2 du: kubeseal verschlüsselt mit dem Public Key 3 ein SealedSecret-CR ist sicher zu committen 4 der Controller entschlüsselt im Cluster → echtes Secret Key: im Cluster · Secret-Wert: nur verschlüsselt in Git Namespace-gebunden (strict) – nicht in einen anderen NS kopierbar der Key ist clusterspezifisch: neuer Cluster → neu sealen oder den Key sichern – die DR-Sorge aus E4 Rotation: der Controller erneuert den Key, alte bleiben gültig am einfachsten – ein Controller, keine externe Abhängigkeit gut für kleine Setups, ein Cluster, wenige Secrets External Secrets Operator (ESO) 1 ein SecretStore / ClusterSecretStore zeigt auf Vault / AWS SM / Azure KV / GCP SM 2 ein ExternalSecret-CR referenziert einen Key-Pfad 3 der Operator holt den Wert → schreibt ein echtes Secret 4 aktualisiert in einem Intervall (refreshInterval) Secret-Wert: nie in Git · lebt im externen Store eine Quelle der Wahrheit für alle Cluster und Umgebungen Rotation im Store → ESO zieht nach, der Reloader rollt (B10) mehr bewegliche Teile: Operator + Store + Zugang zum Store der Store-Zugang ist selbst ein Bootstrap-Secret gut, wenn es schon einen Vault gibt oder viele Cluster Cluster wegwerfbar ohne Key-Verlust (E4) SOPS (+ age / PGP) 1 eine Werte-Datei wird mit sops verschlüsselt (nur die Werte, die Schlüssel bleiben lesbar) 2 die Datei liegt verschlüsselt in Git 3 ein GitOps-Plugin entschlüsselt beim Rendern (Argo CD SOPS-Plugin, argocd-vault-plugin, Flux) Key: beim Deployer (age-Key als Cluster-Secret oder KMS) passt zu jedem GitOps-Tool, kein eigener Controller Diff im Git zeigt key: ENC[...] – welcher Key sich änderte, sieht man Key-Verwaltung liegt bei dir (KMS, age, PGP-Ring) gut, wenn Werte-Dateien schon der Weg sind (Helm/Kustomize) der Renderer braucht den Key im Cluster oder eine KMS-Rolle Sync-Wave-Reihenfolge – das entscheidet, ob es beim Rebuild klappt Wave -2: der Secret-Controller / -Operator (oder er ist ein clusterweiter Operator, immer da) Wave -1: die SealedSecret- / ExternalSecret-CRs → echte Secrets entstehen Wave 0+: die Workloads, die das Secret mounten (E3) – sonst starten sie vor dem Secret Entscheidungsmatrix Sealed: einfachste, Key im Cluster, DR-Risiko am Key ESO: eine Quelle ausserhalb, mehr Teile, cluster-wegwerfbar SOPS: tool-unabhängig, Key-Verwaltung bei dir Merksatz: Sealed = Key im Cluster, ESO = Secret in einem externen Store, SOPS = Key beim Deployer – und die Maschinerie synct vor den Workloads Bibliothek / cloud-host: bis der GitOps-Teil steht, Secrets von Hand mit oc create secret (B10) – danach Sealed Secrets im selben Repo wie die Charts OCP-Unterschied: keiner – alle drei sind Community-Werkzeuge. OpenShift GitOps liefert nur Argo CD, nicht die Secret-Schicht.
Drei Wege, ein Secret zu versionieren. Sealed Secrets: kubeseal verschlüsselt mit dem Public Key des in-cluster Controllers → das SealedSecret ist sicher in Git → der Controller entschlüsselt es im Cluster. External Secrets Operator: ein ExternalSecret referenziert einen Key in Vault / AWS SM / Azure KV → der Operator synct ihn in ein Secret, der Wert berührt Git nie. SOPS: eine verschlüsselte Werte-Datei in Git, ein GitOps-Plugin entschlüsselt beim Rendern. Die Secret-Maschinerie muss in einer frühen Sync-Wave laufen, sonst starten die Workloads ohne ihr Secret.
  • ein Klartext-Secret-YAML in Git ist base64, also lesbar – jeder mit Repo-Zugriff hat die Zugangsdaten (B10)
  • Sealed Secrets: kubeseal verschlüsselt mit dem Public Key des in-cluster Controllers → ein SealedSecret-CR ist sicher zu committen
  • der Sealed-Secrets-Controller entschlüsselt im Cluster in ein echtes Secret – der Key liegt im Cluster, der Wert nur verschlüsselt in Git
  • ein SealedSecret ist Namespace-gebunden (strict) – nicht in einen anderen NS kopierbar
  • der Sealing-Key ist clusterspezifisch: neuer Cluster → neu sealen, oder den Key sichern (die DR-Sorge aus E4)
  • External Secrets Operator: ein ExternalSecret-CR referenziert einen Key-Pfad in Vault / AWS SM / Azure KV / GCP SM
  • der Operator holt den Wert und schreibt ein echtes Secret, aktualisiert in einem refreshInterval – der Wert berührt Git nie
  • ESO braucht einen SecretStore/ClusterSecretStore mit Zugang zum Backend – der Store-Zugang ist selbst ein Bootstrap-Secret
  • SOPS (+ age / PGP): eine Werte-Datei wird verschlüsselt (nur die Werte), liegt so in Git, ein GitOps-Plugin entschlüsselt beim Rendern
  • bei SOPS liegt der Key beim Deployer (age-Key als Cluster-Secret oder eine KMS-Rolle) – passt zu jedem GitOps-Tool, kein eigener Controller
  • Sync-Wave-Reihenfolge: der Secret-Controller in Wave -2, die SealedSecret/ExternalSecret-CRs in -1, die Workloads in 0+ (E3)
  • Entscheidung: Sealed Secrets ist am einfachsten (Key im Cluster, DR-Risiko am Key), ESO ist eine Quelle ausserhalb (mehr Teile, Cluster wegwerfbar), SOPS ist tool-unabhängig (Key-Verwaltung bei dir)
  • Bibliothek: bis der GitOps-Teil steht, Secrets von Hand mit oc create secretdanach Sealed Secrets im selben Repo wie die Charts
  • OCP-Unterschied: keiner – alle drei sind Community-Werkzeuge, OpenShift GitOps liefert nur Argo CD, nicht die Secret-Schicht
  • von Hand durchspielen: gitops-argocd-openshift.html – ein Secret mit kubeseal statt im Klartext versionieren

Block F – Netzwerk & Sicherheit tiefer

F1 OVN-Kubernetes – wie ein Paket von Pod zu Pod kommt (Geneve-Overlay)

NetzwerkOVN-KubernetesOverlayGeneve

Das CNI-Plugin. Jeder Node führt OVN lokal, darüber liegt eine logische Netz-Topologie (Switches, ein verteilter Router) auf dem physischen Netz. Pods auf demselben Node reden über die lokale OVS-Bridge, Pods auf verschiedenen Nodes über einen Geneve-Tunnel. Services (B3) sind OVN-Load-Balancer – kein kube-proxy.

Gleicher Node: die lokale Bridge. Anderer Node: ein Geneve-Tunnel. Node A Pod-CIDR-Ausschnitt 10.128.2.0/23 · ovnkube-node-Pod Pod api-gateway 10.128.2.11 Pod catalog 10.128.2.12 OVS-Bridge br-int – ein logischer Switch verbindet alle Pods des Nodes, kennt ihre MAC/IP 1 · api-gateway → catalog auf demselben Node: das Paket geht nur durch br-int, verlässt den Node nie der Pod hat ein veth-Paar in die Bridge, sonst nichts Besonderes Node B Pod-CIDR-Ausschnitt 10.129.0.0/23 Pod keycloak 10.129.0.7 OVS-Bridge br-int packt das Tunnel-Paket aus und stellt es lokal zu 2 · api-gateway → keycloak auf Node B: OVS auf einem Node A kapselt in Geneve (UDP 6081) über das physische Node-Netz zu Node B Node B packt aus → br-int → Pod Geneve-Tunnel · UDP 6081 die logische Topologie über dem physischen Netz liegt ein Modell aus logischen Switches (einer je Node) und einem verteilten logischen Router, der sie verbindet der Pod sieht ein flaches Netz – jede Pod-IP ist von jeder anderen erreichbar (ohne NetworkPolicy, F2) Steuerung: ovnkube-control-plane schreibt die OVN-Datenbank, ovn-controller je Node setzt sie um alles in openshift-ovn-kubernetes was OVN sonst noch macht Services (B3): als OVN-Load-Balancer, DNAT direkt in der Datapath – kein kube-proxy, keine iptables-Regeln NetworkPolicy (F2): OVN setzt die Allow/Deny-Regeln EgressIP: ausgehender Verkehr bekommt eine feste Quell-IP EgressFirewall: Ziel-Regeln je Namespace OVN-Kubernetes hat OpenShift SDN abgelöst (SDN ab 4.17 raus) Tunnel-Overhead: das MTU je Pod ist ~50 Byte kleiner als das Node-MTU nachsehen oc get pods -n openshift-ovn-kubernetes oc get node -o jsonpath='{.items[*].spec.podCIDR}' die Nodes-Annotation k8s.ovn.org/node-subnets Cluster-Netze im Network-Objekt (clusterNetwork, serviceNetwork) oc get co network zeigt den Zustand des CNI (G6) auf SNO läuft alles auf einem Node – nie ein Geneve-Tunnel Bibliothek: die 8 Dienste im selben Namespace, Verkehr bleibt im Overlay OCP-Unterschied: OVN-Kubernetes ist in OKD und OCP gleich – SDN als älteres Plugin ist entfernt.
Ein flaches Netz auf zwei Ebenen. Jeder Node führt OVN lokal (ovnkube-node), die Datenbank schreibt ovnkube-control-plane. Pods bekommen eine IP aus dem Ausschnitt ihres Nodes. Reden zwei Pods auf demselben Node, geht das Paket nur durch die OVS-Bridge br-int. Sind sie auf verschiedenen Node, kapselt OVN es in einen Geneve-Tunnel (UDP 6081) über das Node-Netz. Services sind OVN-Load-Balancer, NetworkPolicy (F2) OVN-Regeln – kein kube-proxy.
  • das CNI-Plugin: jeder Node führt OVN lokal (ovnkube-node-Pod + ovn-controller), die Datenbank schreibt ovnkube-control-plane in openshift-ovn-kubernetes
  • jeder Node bekommt einen Ausschnitt des Cluster-Pod-CIDR, ein Pod bekommt eine IP daraus über ein veth-Paar in die OVS-Bridge br-int
  • Pod zu Pod auf demselben Node: das Paket geht nur durch br-int und verlässt den Node nie
  • Pod zu Pod auf verschiedenen Nodes: OVS kapselt in einen Geneve-Tunnel (UDP 6081) über das physische Node-Netz, der Ziel-Node packt aus
  • über dem physischen Netz liegt eine logische Topologie aus Switches (einer je Node) und einem verteilten logischen Router
  • der Pod sieht ein flaches Netz – jede Pod-IP ist von jeder anderen erreichbar, solange keine NetworkPolicy (F2) greift
  • Services (B3) sind OVN-Load-Balancer: ruft api-gateway den catalog-service, macht OVN das DNAT direkt in der Datapath – kein kube-proxy, keine iptables-Regeln
  • OVN erzwingt auch NetworkPolicy (F2), EgressIP (feste Quell-IP nach aussen) und EgressFirewall (Ziel-Regeln je Namespace)
  • OVN-Kubernetes hat OpenShift SDN abgelöst – SDN ist ab OCP 4.17 entfernt
  • Tunnel-Overhead: das Pod-MTU ist rund 50 Byte kleiner als das Node-MTU – wichtig bei Jumbo Frames oder VPN darunter
  • auf der SNO: die 8 Dienste + Postgres/Redis/Kafka/Keycloak sind alle Pods auf einem Node – jeder Hop bleibt in br-int, nie ein Geneve-Tunnel
  • nachsehen: oc get pods -n openshift-ovn-kubernetes zeigt auf der SNO genau einen ovnkube-node-Pod, dazu die Nodes-Annotation k8s.ovn.org/node-subnets und das Network-Objekt
  • ein lokaler 2-Node-kind-Cluster zeigt das Kapselungs-Prinzip – für echtes OVN-K mit Geneve das ovn-kubernetes-kind-Setup
  • OCP-Unterschied: identisch – OVN-Kubernetes ist in OKD und OCP gleich, SDN als älteres Plugin ist weg
  • von Hand verfolgen: Service-DNAT und Geneve – ClusterIP, EndpointSlice und Pod-IP zusammenführen und den berechneten OVN-Pfad optional mit ovnkube-trace simulieren

F2 NetworkPolicy – default-allow vs default-deny, Namespace-Isolation

NetzwerkNetworkPolicyIsolationdefault-denyEX280

Ab Werk darf jeder Pod jeden Pod erreichen, clusterweit (A2). Eine NetworkPolicy, die eine Pod-Menge auswählt, schaltet genau diese Pods auf default-deny für die genannte Richtung – danach erlaubst du gezielt einzelne Verbindungen wieder. OVN (F1) setzt die Regeln um.

Ohne Policy: alles offen. Mit Policy: die ausgewählten Pods sind zu, bis du öffnest. Ausgangslage: default-allow ein frischer Namespace hat keine NetworkPolicy → jeder Pod erreicht jeden Pod, clusterweit auch aus anderen Namespaces ein kompromittierter Pod kann alles scannen Isolation ist nicht automatisch (A2) Namespace = RBAC- und Quota-Grenze, aber keine Netz-Grenze die Netz-Trennung kommt separat – hier Bibliothek: heisst, DB und Kafka sind erstmal von jedem Pod erreichbar eine NetworkPolicy wählt aus und schliesst spec.podSelector: {matchLabels: {app: catalog}} nur die catalog-Pods sind betroffen policyTypes: [Ingress] und/oder [Egress] → diese Richtung wird default-deny für diese Pods dann Regeln, die wieder erlauben: ingress: [{from: [{podSelector: {app: api-gateway}}], ports: [{port: 8080}]}] Selektoren: podSelector, namespaceSelector (mit and kombiniert), ipBlock (CIDR + except), ports mehrere Policies auf denselben Pods: die Erlaubnisse summieren sich es gibt kein Deny in einer NetworkPolicy – nur Auswahl + Allow (wie RBAC, A5) AdminNetworkPolicy (clusterweit, vor den Namespace-Policies) kann hart verbieten Bibliothek: der Plan 1 · deny-all-ingress im Namespace library (leerer podSelector: {}, policyTypes: [Ingress]) 2 · api-gateway, library-frontend: Ingress nur aus openshift-ingress (der Router, B4) 3 · die 6 internen Dienste: Ingress nur aus api-gateway 4 · Postgres, Redis, Kafka: nur aus ihren Konsumenten 5 · Egress überall auf openshift-dns erlauben Ergebnis: die DB ist nur noch von den Diensten erreichbar, die sie wirklich brauchen liefert das Helm-Chart mit (B11) – ein NetworkPolicy-Manifest je Dienst die Fallen 1 · DNS vergessen: eine Egress-Policy ohne Regel für openshift-dns (UDP/TCP 53) → die Namensauflösung bricht, alles läuft in Timeouts 2 · Ingress-Policy ohne Deny-Basis: eine reine Allow-Policy schliesst nichts – erst eine Policy, die den Pod auswählt, macht ihn default-deny 3 · namespaceSelector braucht ein Label am Quell-Namespace – nicht jeder hat eins 4 · Health-Probes (B5) kommen vom kubelet, nicht von einem Pod – separat erlauben nachsehen & testen oc get netpol -n library oc describe netpol deny-all-ingress -n library testen: aus einem Pod oc rsh und curl gegen das Ziel wirkt sofort (OVN, F1) – kein Pod-Neustart nötig AdminNetworkPolicy: oc get anp (clusterweit) Console: Netzwerk-Ansicht zeigt die erlaubten Verbindungen als Graph Merksatz: eine Policy, die einen Pod auswählt, macht ihn für die genannte Richtung default-deny – danach zählt nur, was du erlaubst kein Deny in einer NetworkPolicy – für hartes Verbieten über Namespaces hinweg: AdminNetworkPolicy OCP-Unterschied: NetworkPolicy ist reines Kubernetes, OVN setzt sie um – EgressFirewall (Ziel-Regeln je Namespace) und AdminNetworkPolicy sind die OpenShift-/OVN-Erweiterungen.
Auswahl macht zu, Regeln machen auf. Ohne NetworkPolicy erreicht jeder Pod jeden – ein Namespace ist keine Netz-Grenze (A2). Eine Policy, die per podSelector eine Pod-Menge auswählt, schaltet genau diese Pods für die in policyTypes genannte Richtung auf default-deny; die ingress/egress-Regeln erlauben dann gezielt wieder. Es gibt kein Deny – mehrere Policies summieren ihre Erlaubnisse. Klassische Falle: die DNS-Egress-Regel vergessen, dann bricht die Namensauflösung.
  • ab Werk gilt default-allow: jeder Pod erreicht jeden Pod clusterweit – ein frischer Namespace ist offen (A2)
  • ein Namespace ist eine RBAC- und Quota-Grenze, aber keine Netz-Grenze – die kommt separat mit NetworkPolicy
  • eine Policy, die per podSelector eine Pod-Menge auswählt, schaltet genau diese Pods auf default-deny für die Richtungen in policyTypes (Ingress, Egress)
  • danach fügt man ingress/egress-Regeln hinzu, die bestimmten Verkehr wieder erlauben
  • Selektoren: podSelector, namespaceSelector, ipBlock (CIDR + except), ports
  • es gibt kein Deny in einer NetworkPolicy – nur Auswahl und Allow, und mehrere Policies auf denselben Pods summieren ihre Erlaubnisse (wie RBAC, A5)
  • OVN (F1) erzwingt die Regeln – sie wirken sofort, ohne Pod-Neustart
  • AdminNetworkPolicy / BaselineAdminNetworkPolicy: clusterweit, admin-owned, werden vor den Namespace-Policies ausgewertet und können hart verbieten
  • Falle DNS: eine Egress-Policy ohne Regel für openshift-dns (Port 53) → die Namensauflösung bricht, alles läuft in Timeouts
  • Falle Probes: Health-Probes (B5) kommen vom kubelet, nicht von einem Pod – separat erlauben
  • Bibliothek: eine deny-all-ingress-Basis in library, dann Ingress für api-gateway und library-frontend nur aus openshift-ingress, für die 6 internen Dienste nur aus api-gateway, Postgres und Kafka nur aus ihren Konsumenten
  • nachsehen: oc get netpol -n library, oc describe netpol deny-all-ingress -n library, testen mit oc rsh deploy/api-gateway -n library plus curl
  • von Hand durchspielen: networkpolicy-openshift.html – deny-all anlegen, den Traffic wirklich brechen sehen, gezielt wieder erlauben, die DNS- und Probes-Falle
  • OCP-Unterschied: NetworkPolicy ist reines Kubernetes – EgressFirewall und AdminNetworkPolicy sind die OVN-Erweiterungen

F3 Router-interna – die HAProxy-Konfig, Sharding, wie eine Route zum Backend wird

NetzwerkRouter-internaHAProxySharding

B4 hat den Router als Kasten gezeigt – hier von innen. Ein Controller im Router-Pod beobachtet Route und Endpoints, rendert daraus die haproxy.config neu und lädt HAProxy neu. Jede Route wird zu einem Frontend-Match plus einem Backend mit einer server-Zeile je bereitem Pod. Mehrere IngressController teilen die Routes per Selektor auf.

Route + Endpoints → haproxy.config gerendert → Reload. Eingaben, die der Controller beobachtet Route-Objekte (B4): Host, Pfad, TLS-Modus, Service EndpointSlice (B3): die bereiten Pod-IPs der IngressController-Spec (global) jede Änderung → neu rendern Route wird Admitted, wenn ihr Host frei ist HostAlreadyClaimed: zwei Routes, ein Host Router-Pod (router-default, openshift-ingress) 1 · Controller rendert aus einer Go-Template die Datei /var/lib/haproxy/conf/haproxy.config 2 · HAProxy wird graceful neu geladen (alte Verbindungen laufen aus) frontend fe_sni # matcht den Host (SNI / Host-Header) use_backend be_library_frontend if host_frontend backend be_library_frontend server pod:10.128.2.11:8080 ... # je bereitem Endpoint server pod:10.128.2.12:8080 ... # 10.128.3.9 not ready -> keine server-Zeile 3 · das Paket geht dann an den Service (B3), der zum Pod DNAT-et edge/reencrypt: Host-Header lesbar, Pfad-Routing geht · passthrough: nur SNI oc rsh <router-pod> cat /var/lib/haproxy/conf/haproxy.config Sharding mehrere IngressController-Objekte, je einer: eigene domain, eigene Node (nodePlacement) routeSelector und/oder namespaceSelector Beispiel: ein interner Router (nur im Firmennetz) und ein internetseitiger Router eine Route wird von den Controllern Admitted, deren Selektoren sie matcht (kann auch mehrere sein) Skalierung: replicas am IngressController – mehr HAProxy-Pods TLS-Vorgabe, Ciphers, HTTP/2: spec.tlsSecurityProfile Logging: spec.logging.access (aus, sidecar, oder syslog) oc get ingresscontroller -n openshift-ingress-operator oc get route -A -o wide zeigt die Admitted-Spalte Feintuning je Route – Annotationen haproxy.router.openshift.io/… timeout: 30s · balance: roundrobin (auch leastconn, source = sticky) disable_cookies · rate-limit-connections · ip_whitelist set-forwarded-headers steuert, was der Router an X-Forwarded-* setzt (B4) global im IngressController: tuningOptions, httpErrorCodePages, httpHeaders der Reload-Sturm jede Route-/Endpoint-Änderung rendert die ganze Datei neu bei tausenden Routes: ständige Reloads, hohe CPU Abhilfe: RELOAD_INTERVAL bündelt, maxConnections deckelt bei Rollouts (B1) flackern server-Zeilen – sporadische 502/503 Merksatz: der Controller macht aus Route + Endpoints eine haproxy.config, lädt neu, und HAProxy verteilt auf die server-Zeilen Sharding = mehrere IngressController mit Selektoren – intern/extern trennen, oder Last auf mehr Router-Pods verteilen Bibliothek: ein Default-Router reicht – bei einem internen und einem externen Zugang wäre ein zweiter IngressController der Weg OCP-Unterschied: identisch – der HAProxy-Router und der IngressController-Operator sind Kern-OpenShift, in OKD und OCP gleich.
Aus Objekten wird HAProxy-Konfig. Ein Controller im router-default-Pod beobachtet Route und Endpoints, rendert daraus die haproxy.config aus einer Go-Template und lädt HAProxy graceful neu. Jede Route wird zu einem Frontend-Match (SNI bei passthrough, Host-Header sonst) plus einem Backend mit einer server-Zeile je bereitem Endpoint. Mit mehreren IngressController-Objekten und routeSelector teilt man die Routes auf – intern vs extern, oder mehr Router-Pods gegen Last.
  • ein Controller im Router-Pod beobachtet Route und EndpointSlice (B3) und rendert daraus die /var/lib/haproxy/conf/haproxy.config aus einer Go-Template
  • nach jedem Rendern wird HAProxy graceful neu geladen – bestehende Verbindungen laufen aus
  • jede der 3 Bibliothek-Routes (library-frontend, keycloak, api-gateway) wird zu einem Frontend-Match auf den Host-Header (bei edge) plus einem Backend
  • im Backend steht eine server-Zeile je bereitem Endpoint – ein not-ready catalog-service-Pod (B5) bekommt keine Zeile und damit keinen Verkehr
  • eine Route wird Admitted, wenn ihr Host frei ist – HostAlreadyClaimed bei zwei Routes auf denselben Host
  • Sharding: mehrere IngressController-Objekte, je einer mit eigener domain, eigenen Nodes und einem routeSelector oder namespaceSelector
  • typisch: ein interner und ein internetseitiger Router – auf der SNO mit einem Node und einem router-default-Pod ist das gegenstandslos, es gibt genau einen
  • Feintuning je Route: Annotationen haproxy.router.openshift.io/timeout, balance, disable_cookies, rate-limit-connections, ip_whitelist
  • global im IngressController: tuningOptions, logging.access, tlsSecurityProfile, httpErrorCodePages, replicas
  • der Reload-Sturm: bei tausenden Routes rendert jede Änderung die ganze Datei neu → ständige Reloads, die RELOAD_INTERVAL bündelt – bei 3 Routes nie ein Thema
  • bei Rollouts (B1) flackern die server-Zeilen – das erklärt die sporadischen 502/503 aus B4
  • nachsehen auf der SNO: oc rsh -n openshift-ingress deploy/router-default cat /var/lib/haproxy/conf/haproxy.config, oc get route -n library -o wide
  • Bibliothek: der Default-Router reicht – seine selbstsignierte Wildcard bedient alle 3 edge-Routes, ein zweiter IngressController wäre erst bei getrenntem internem und externem Zugang nötig
  • OCP-Unterschied: identisch – HAProxy-Router und IngressController-Operator sind Kern-OpenShift
  • von Hand nachsehen: Route bis HAProxy verfolgen – eine eigene Lab-Route in der generierten Konfiguration finden, TLS-Termination umstellen und Sharding-Felder lesen

F4 Service Mesh (OpenShift Service Mesh / Istio) – Sidecar, mTLS, wann man es braucht

NetzwerkService MeshIstioSidecarmTLS

OpenShift Service Mesh ist Istio als Operator. In jeden Pod wird ein Envoy-Sidecar eingespritzt, das allen Verkehr abfängt. Der Sidecar bringt automatisches mTLS, feingranulares Routing (Canary, Retries, Timeouts) und L7-Telemetrie – kostet aber Latenz, Speicher und Betrieb. Für 8 Dienste ist das oft zu viel.

Ein Envoy-Sidecar je Pod – fängt allen Verkehr ab, macht mTLS, meldet alles. der Pod mit Sidecar App-Container redet mit localhost Envoy-Sidecar fängt in + out ab Einspritzung: Namespace-Label + Pod-Annotation sidecar.istio.io/inject: "true" – ein initContainer setzt iptables-Regeln, alles läuft durch Envoy Kosten: ~50–100 MiB und etwas Latenz je Pod die App ändert sich nicht – sie merkt vom Mesh nichts istiod – die Steuerungsebene konfiguriert alle Sidecars: Routing, Zertifikate, Policy verteilt kurzlebige mTLS-Zertifikate an jeden Sidecar automatisches mTLS zwischen den Sidecars PeerAuthentication: STRICT → nur verschlüsselter Verkehr Zero-Trust ohne App-Änderung konfiguriert über CRDs (kein App-Code): Deployment via ServiceMeshControlPlane / Istio-CR ServiceMeshMemberRoll listet die Member-Namespaces oc get smcp,smmr -n istio-system Traffic-Management & Observability VirtualService: Routing, Canary-%, Retries, Timeouts, Fault-Injection DestinationRule: Subsets, Load-Balancing, Circuit-Breaking Gateway: Mesh-Ingress – eine Alternative zur Route (B4) Kiali: Topologie-Graph, wer redet mit wem Tracing: Jaeger / Tempo · Metriken: Prometheus / Grafana (G1) Retry/Timeout im Mesh statt im App-Code – über Dienste hinweg gleich Canary: 5 % auf v2, Rest auf v1 – ohne zwei Deployments zu jonglieren Mesh-Traffic kann trotzdem durch NetworkPolicy (F2) begrenzt sein wann es sich lohnt viele Dienste, die viel untereinander reden Bedarf an feinem Traffic-Shifting (Canary, A/B, Blue-Green je Request) mTLS überall als Compliance-Vorgabe, ohne jede App anzufassen einheitliche Retries / Timeouts / Circuit-Breaker zentral reiche L7-Telemetrie und ein Abhängigkeitsgraph (Kiali) typisch ab ~15–20 Diensten oder bei strengen Sicherheitsanforderungen Alternativen für einzelne Aspekte: NetworkPolicy (F2), serving-certs (F5), Gateway API, Resilience-Libraries in der App wann Overkill eine Handvoll Dienste, klare Aufrufwege NetworkPolicy (F2) + Routes (B4) + App-Retries reichen der Sidecar kostet Latenz und ~50–100 MiB je Pod istiod, Kiali, Jaeger sind eigene Betriebsobjekte mit Upgrades Debugging wird schwerer: ein Hop mehr, Envoy-Konfig lesen Bibliothek: wahrscheinlich nicht nötig für 8 Dienste notiert als Option, falls die App auf viele Dienste wächst mTLS-Bedarf allein → erst serving-certs (F5) prüfen Merksatz: Sidecar fängt den Verkehr ab, istiod konfiguriert ihn – mTLS, Routing und Telemetrie ohne App-Code, aber mit Overhead je Pod Entscheidung: Mesh erst, wenn NetworkPolicy + Routes + App-Retries nicht mehr reichen istioctl proxy-config zeigt die effektive Envoy-Konfig eines Pods · oc get vs,dr,gw -A OCP-Unterschied: OSSM ist Istio als unterstützter Operator (früher mit ServiceMeshControlPlane, neu die Istio-CR / Sail-Operator) – das Kubernetes-Original ist Upstream-Istio von Hand.
Ein Proxy je Pod. In jeden Pod eines Member-Namespace wird ein Envoy-Sidecar eingespritzt, das allen Verkehr abfängt; istiod konfiguriert alle Sidecars und verteilt kurzlebige Zertifikate. Zwischen den Sidecars läuft automatisch mTLS. Über CRDs (VirtualService, DestinationRule, Gateway) steuert man Routing, Canary und Retries ohne App-Code; Kiali zeigt den Graphen. Der Preis: Latenz, ~50–100 MiB je Pod und ein weiterer Betriebs-Stack – für 8 Dienste meist zu viel.
  • OpenShift Service Mesh = Istio als Operator-Paket – ein Envoy-Sidecar wird in jeden Pod eines Member-Namespace eingespritzt (Namespace-Label + Pod-Annotation)
  • der Sidecar fängt allen ein- und ausgehenden Verkehr ab – die App redet nur mit localhost und merkt vom Mesh nichts
  • istiod ist die Steuerungsebene: konfiguriert alle Sidecars und verteilt kurzlebige mTLS-Zertifikate
  • automatisches mTLS zwischen den Sidecars (PeerAuthentication STRICT) – Zero-Trust ohne App-Änderung
  • Traffic-Management über CRDs: VirtualService (Routing, Canary-Anteil, Retries, Timeouts, Fault-Injection), DestinationRule (Subsets, Circuit-Breaking), Gateway (Mesh-Ingress statt Route)
  • Observability: Kiali (Topologie-Graph), Tracing (Jaeger/Tempo), Metriken (Prometheus/Grafana, G1)
  • Retries und Timeouts im Mesh statt im App-Code – in der Bibliothek macht das heute das api-gateway selbst (Spring), das Mesh würde es über alle Dienste zentralisieren
  • wann es sich lohnt: viele Dienste, feines Traffic-Shifting, mTLS als Compliance-Vorgabe, reiche L7-Telemetrie – typisch ab rund 15 bis 20 Diensten
  • wann Overkill: eine Handvoll Dienste, wo NetworkPolicy (F2) plus Routes (B4) plus App-Retries reichen – genau der Bibliothek-Fall
  • der Preis: Latenz, ~50–100 MiB je Pod – bei einem guten Dutzend Bibliothek-Pods fast 1 GB nur für Sidecars, plus istiod/Kiali/Jaeger als eigene Betriebsobjekte
  • Debugging wird schwerer: ein Hop mehr, die Envoy-Konfig lesen (istioctl proxy-config)
  • Bibliothek: nicht nötig für das gute Dutzend Pods – notiert als Option, wenn die App wächst. Reiner mTLS-Bedarf zwischen zwei Diensten → erst serving-certs (F5)
  • OCP-Unterschied: OSSM ist Istio als unterstützter Operator (ServiceMeshControlPlane, neu die Istio-CR) – Kubernetes pur ist Upstream-Istio von Hand
  • Generation und Bedarf prüfen: Service Mesh erkennen – Sail- und Maistra-Ressourcen unterscheiden, Sidecars im Pod nachweisen und gegen Service-CA oder NetworkPolicy abwägen

F5 serving-certs (Service-CA-Operator) – TLS für den Verkehr im Cluster

Netzwerkserving-certsService-CAZertifikate

Eine Annotation am Service genügt: der Service-CA-Operator legt ein kubernetes.io/tls-Secret an, mit einem Zertifikat für <svc>.<ns>.svc, signiert von der cluster-internen Service-CA und automatisch erneuert. Der Gegenüber vertraut ihr über service-ca.crt. Das ist für Pod-zu-Pod-TLS, reencrypt-Routes (B4) und Webhooks – nicht für den Browser.

Eine Annotation am Service → ein fertiges TLS-Secret, von der Cluster-CA signiert, selbsterneuernd. 1 · du annotierst den Service kind: Service metadata: name: catalog annotations: service.beta.openshift.io/ serving-cert-secret-name: catalog-tls mehr ist auf deiner Seite nicht zu tun kein CSR, kein Key erzeugen, kein Ablaufdatum pflegen Bibliothek: am catalog- und am keycloak-Service 2 · der Service-CA-Operator legt an Secret catalog-tls type: kubernetes.io/tls tls.crt tls.key SAN: catalog.bib.svc und catalog.bib.svc.cluster.local signiert von der cluster-internen Service-CA gültig ~2 Jahre, vor Ablauf automatisch erneuert löschst du das Secret, wird es neu erzeugt 3 · der Gegenüber vertraut der CA ConfigMap annotieren: service.beta.openshift.io/inject-cabundle: "true" → Operator schreibt service-ca.crt hinein Client-Pod mountet die ConfigMap als CA-Bundle CA rotiert ~alle 13 Monate – das neue Bundle kommt automatisch bei den Consumern an gleiche Annotation geht auf ein ValidatingWebhook, APIService, CRD-conversion-Feld wofür man es nimmt reencrypt-Route (B4): der Router spricht per HTTPS zum Pod, destinationCACertificate = Service-CA Webhook-Server (Validating/Mutating) – die API verlangt HTTPS mit vertrauter CA Prometheus-Scrape über HTTPS, Operator-Metriken, andere In-Cluster-Endpunkte Pod-zu-Pod: App → DB, App → interner Dienst mit echtem TLS statt Klartext Kurz: überall, wo zwei Sachen innerhalb des Clusters TLS sprechen sollen leichter als cert-manager (B4), wenn dir die interne CA als Vertrauensanker reicht wofür nicht nicht für Browser-TLS an der Route kein Browser kennt die Service-CA → Warnung öffentliches TLS = Ingress-Cert / cert-manager (B4) SAN ist nur *.svc – kein externer DNS-Name nicht zwischen Clustern nutzbar (CA je Cluster) CA-Neustart-Rotation: Pods ggf. neu rollen Merksatz: serving-cert-secret-name erzeugt den Server-Cert, inject-cabundle verteilt die CA zum Vertrauen – beide vom selben Operator, beide selbsterneuernd nachsehen: oc get secret catalog-tls -o yaml · oc get co service-ca · CA in openshift-service-ca / openshift-config-managed oc get configmap <cm> -o jsonpath='{.data.service-ca\.crt}' zeigt das injizierte Bundle OCP-Unterschied: der Service-CA-Operator ist OpenShift-eigen – unter Kubernetes pur nimmt man cert-manager mit einem internen Issuer oder rollt eine eigene CA.
Eine Annotation, ein Secret. service.beta.openshift.io/serving-cert-secret-name am Service lässt den Service-CA-Operator ein kubernetes.io/tls-Secret anlegen – Zertifikat für <svc>.<ns>.svc, signiert von der Cluster-Service-CA, vor Ablauf automatisch erneuert. Der Client vertraut über eine mit inject-cabundle annotierte ConfigMap (service-ca.crt). Gedacht für reencrypt-Routes (B4), Webhooks, Prometheus-Scrapes und Pod-zu-Pod-TLS – nicht für den Browser, der die interne CA nicht kennt.
  • Ziel: TLS für Verkehr innerhalb des Clusters, ohne selbst CSRs, Keys oder Ablaufdaten zu verwalten
  • du annotierst den Service mit service.beta.openshift.io/serving-cert-secret-name und dem gewünschten Secret-Namen – mehr nicht
  • der Service-CA-Operator legt daraufhin ein kubernetes.io/tls-Secret an (tls.crt, tls.key)
  • das Zertifikat gilt für <svc>.<ns>.svc und <svc>.<ns>.svc.cluster.localkeine externen DNS-Namen
  • signiert von der cluster-internen Service-CA, gültig rund 2 Jahre und vor Ablauf automatisch erneuert
  • der Gegenüber vertraut der CA über eine mit service.beta.openshift.io/inject-cabundle annotierte ConfigMap → der Operator schreibt service-ca.crt hinein
  • die CA rotiert etwa alle 13 Monate – das neue Bundle kommt automatisch bei allen Consumern an
  • Verwendung: reencrypt-Route (B4, destinationCACertificate = Service-CA), Webhook-Server, Prometheus-Scrape, Operator-Metriken, Pod-zu-Pod-DB-TLS
  • Bibliothek heute: die 8 Spring-Dienste reden intern per HTTP über die Service-Namen – im Overlay auf einem Node vertretbar, aber unverschlüsselt
  • der Service-CA-Weg dorthin: catalog-service bekommt die Annotation, mountet das TLS-Secret, Spring läuft auf server.sslapi-gateway vertraut über das injizierte service-ca.crt. Kein Let's Encrypt, keine Route beteiligt
  • nicht für Browser-TLS an der Route – kein Browser kennt die Service-CA, das gibt eine Zertifikatswarnung. Die edge-Routes der Bibliothek bleiben Sache der Router-Wildcard (B4)
  • die inject-cabundle-Annotation geht auch auf ValidatingWebhookConfiguration, APIService und CRD-conversion-Felder
  • nachsehen: oc get secret <name> -n library -o yaml, oc get co service-ca – die CA-Objekte liegen in openshift-service-ca und openshift-config-managed
  • OCP-Unterschied: der Service-CA-Operator ist OpenShift-eigen – unter Kubernetes pur nimmt man cert-manager mit internem Issuer oder eine selbstgebaute CA
  • von Hand durchspielen: Service-Zertifikat und CA-Bundle – Annotationen anwenden, Secret und ConfigMap prüfen, manuell rotieren und den Controller diagnostizieren

Block G – Betrieb (Admin-Tiefe, meist auf SNO)

G1 Der Monitoring-Stack – Prometheus, Thanos, Alertmanager, user-workload monitoring

BetriebMonitoringPrometheusAlertmanagerEX280

OpenShift bringt zwei getrennte Monitoring-Stacks mit. Der Plattform-Stack läuft immer und überwacht die Steuerungsebene, die Nodes und die Operatoren. Der User-Workload-Stack ist opt-in und überwacht deine Apps – du meldest sie mit einem ServiceMonitor an. Beide zeigen im Observe-Tab und alarmieren über Alertmanager.

Zwei Stacks: Plattform (immer) und deine Apps (opt-in). Plattform-Monitoring – openshift-monitoring läuft immer, kann man nicht abschalten, nur tunen (cluster-monitoring-config) Prometheus (Paar, HA) scrapt: kube-apiserver, etcd, kubelet, node-exporter (DaemonSet), kube-state-metrics, jeder Operator Thanos-Querier – fächert eine Abfrage über alle Prometheus-Instanzen Thanos-Ruler – wertet PrometheusRule-Regeln aus Alertmanager – entprellt, gruppiert, leitet Alarme an receivers PagerDuty · Slack · Webhook · E-Mail · OpsGenie Retention lokal ~15 Tage – länger nur mit externem Speicher (Thanos + Objektspeicher) Alarme wie ClusterOperatorDegraded (G6), KubeletDown, TargetDown oc get prometheus,alertmanager -n openshift-monitoring User-Workload-Monitoring – openshift-user-workload-monitoring opt-in: die ConfigMap cluster-monitoring-config setzen data.config.yaml: enableUserWorkload: true ein eigener Prometheus (getrennt vom Plattform-Stack) du meldest deine App an mit einem ServiceMonitor / PodMonitor-CR: Label-Selektor + Pfad + Port + Intervall eigene Alarme: PrometheusRule-CR im Namespace deiner App Bibliothek: ein ServiceMonitor je Spring-Dienst, scrapt /actuator/prometheus Rechte: die Rolle monitoring-edit im Namespace erlaubt eigene Monitore oc get servicemonitor,prometheusrule -n library deine Alarme laufen über denselben Alertmanager wie die Plattform der Observe-Tab der Konsole Metrics – PromQL-Abfragen & Graphen (über den Thanos-Querier) Alerts – was gerade feuert, Silences setzen Dashboards (mitgelieferte Grafana-artige Ansichten) · Targets (wer wird gescrapt, up/down) API auch für oc adm top pods/nodes und den HPA (B6) was schiefgeht TargetDown: Selektor passt nicht, falscher Port, App bietet den Pfad nicht an Prometheus frisst RAM – hohe Kardinalität (zu viele Label-Werte) begrenzen User-Workload-Prometheus ohne PVC → Daten weg beim Neustart Alertmanager-receiver nicht konfiguriert → Alarme feuern ins Leere Merksatz: Plattform-Stack = der Cluster überwacht sich, User-Workload-Stack = du überwachst deine App über einen ServiceMonitor ServiceMonitor = „scrap diese Pods hinter diesem Service“, PrometheusRule = „alarmiere, wenn dieser Ausdruck wahr wird“ Logging ist ein anderer Stack (G2) · Storage für die Metrik-PVCs: G3 OCP-Unterschied: der Stack ist Kern-OpenShift (bei OKD identisch). Reines Kubernetes installiert man den kube-prometheus-Stack von Hand.
Zwei Stacks, ein Alertmanager. Das Plattform-Monitoring (openshift-monitoring) läuft immer und scrapt Steuerungsebene, Nodes und Operatoren; Thanos vereinheitlicht die Abfrage, Alertmanager leitet die Alarme. Das User-Workload-Monitoring ist opt-in (enableUserWorkload: true) und gibt dir einen eigenen Prometheus – du meldest deine App mit einem ServiceMonitor an und schreibst eigene PrometheusRule-Alarme. Beides erscheint im Observe-Tab.
  • OpenShift hat zwei getrennte Stacks: Plattform-Monitoring (immer an) und User-Workload-Monitoring (opt-in)
  • Plattform (openshift-monitoring): Prometheus-Paar scrapt Steuerungsebene, node-exporter-DaemonSet, kube-state-metrics und jeden Operator – nicht abschaltbar, nur tunen
  • Thanos-Querier fächert eine Abfrage über alle Prometheus-Instanzen, Thanos-Ruler wertet die Regeln aus
  • Alertmanager entprellt, gruppiert und leitet Alarme an receivers: PagerDuty, Slack, Webhook, E-Mail
  • User-Workload aktivieren: die ConfigMap cluster-monitoring-config mit enableUserWorkload: true
  • du meldest deine App an mit einem ServiceMonitor (oder PodMonitor): Label-Selektor, Pfad, Port, Intervall
  • eigene Alarme: ein PrometheusRule-CR im Namespace library – etwa up{job="catalog-service"} == 0 für 5 min, über denselben Alertmanager wie die Plattform
  • Rechte: die Rolle monitoring-edit im Namespace erlaubt eigene Monitore und Regeln
  • der Observe-Tab der Konsole: Metrics (PromQL), Alerts (+ Silences), Dashboards, Targets (up/down)
  • auf der SNO: der User-Workload-Prometheus bekommt per Default keine PVC – nach einem sno.ps1 up sind die App-Metriken von vor dem Snapshot weg, eine volumeClaimTemplate im user-workload-monitoring-config hält dagegen
  • was schiefgeht: TargetDown bei falschem Selektor oder Port, hohe Kardinalität frisst RAM, ein unkonfigurierter receiver schluckt Alarme
  • Bibliothek: ein ServiceMonitor je Spring-Dienst auf /actuator/prometheus (Micrometer) – die Bitnami-Charts von Postgres, Kafka und Redis bringen je einen metrics-Exporter mit, den ein eigener ServiceMonitor abholt
  • Merksatz: ServiceMonitor = „scrap diese Pods“, PrometheusRule = „alarmiere bei diesem Ausdruck“
  • auf SNO ohne HA: der ganze Stack läuft (ServiceMonitor, Observe-Tab, Alertmanager) – nur das Prometheus-Paar und echte Ausfallsicherheit fehlen
  • OCP-Unterschied: identisch – der Stack ist Kern-OpenShift. Reines Kubernetes braucht den kube-prometheus-Stack von Hand
  • von Hand durchspielen: cluster-betrieb-openshift.html – einen ServiceMonitor und eine PrometheusRule live anlegen

G2 Logging – der Loki/Vector-Stack, ClusterLogForwarder

BetriebLoggingLokiVector

Container schreiben nach stdout/stderr, der Node sammelt das. OpenShift-Logging bündelt drei Teile: Vector sammelt auf jedem Node, LokiStack speichert (nach Labels indiziert, nicht Volltext), und der ClusterLogForwarder lenkt die Ströme – in die interne LokiStack und/oder an externe Ziele wie Splunk oder ein SIEM.

Sammeln (Vector) → Speichern (Loki) → Weiterleiten (ClusterLogForwarder). Vector – DaemonSet ein Pod je Node, liest: Container stdout/stderr journald (der Node selbst) Audit-Logs (kube-apiserver, OVN) parst, reichert an (Namespace, Pod, Container, Node als Labels) löste Fluentd ab (leichter, schneller) LokiStack verwaltet vom Loki-Operator Backend: Objektspeicher (S3, ODF, MinIO) indiziert nach Labels, nicht Volltext → billiger als Elasticsearch, aber Query anders drei Tenants: application / infrastructure / audit RBAC und Retention je Tenant getrennt sizing: 1x.demo / 1x.small / … Observe → Logs in der Web-Konsole Abfragesprache: LogQL nach Namespace / Pod / Level filtern Live-Tail, Zeitfenster, Kontext der Entwickler sieht nur den application-Tenant seiner Projekte Alternative: Grafana gegen Loki externe Ziele Splunk · Elasticsearch syslog · Kafka Cloudwatch · GCP Logging Azure Monitor · HTTP typisch: Audit-Logs ans SIEM, App-Logs nach Splunk TLS + ein Secret je Ziel der ClusterLogForwarder – das Lenkobjekt ein CR mit inputs (application / infrastructure / audit, oder ein Label-Selektor), outputs (die Ziele) und pipelines (input → filter → output) filters: Felder droppen, PII maskieren, Labels/Struktur umschreiben, Rate-Limit je Pipeline ohne Forwarder-CR: alles geht in die Default-LokiStack. Mit CR: du entscheidest, was wohin geht Falle: ein Output mit falschem Zertifikat/Secret → die ganze Pipeline staut, Logs gehen verloren (kein unendlicher Puffer) oc get clusterlogforwarder,lokistack -n openshift-logging · Status-Conditions am CR Merksatz: Vector sammelt, Loki speichert nach Labels, der ClusterLogForwarder lenkt – ersetzt das alte EFK (Elasticsearch/Fluentd/Kibana) Metriken sind ein anderer Stack (G1) · Storage für die Log-PVCs / das Objektspeicher-Backend: G3 Bibliothek: die 8 Dienste loggen strukturiert (JSON) nach stdout → LokiStack, Audit-Logs an ein SIEM weitergeleitet OCP-Unterschied: identisch (die Operatoren sind in OKD verfügbar) – frisch keine Logging-Komponente, man installiert die Operatoren.
Sammeln, speichern, lenken. Vector läuft als DaemonSet und liest auf jedem Node die Container-Ausgabe, journald und die Audit-Logs. Die LokiStack speichert sie in Objektspeicher, nach Labels indiziert (nicht Volltext), aufgeteilt in die Tenants application / infrastructure / audit. Der ClusterLogForwarder-CR entscheidet mit pipelines, was in die interne Loki und was an externe Ziele (Splunk, SIEM, Kafka) geht. Das löste den alten EFK-Stack ab.
  • drei Teile: Vector sammelt (DaemonSet), LokiStack speichert, der ClusterLogForwarder lenkt
  • Vector liest je Node: Container-stdout/stderr, journald des Nodes, Audit-Logs – und reichert mit Namespace, Pod und Container als Labels an
  • LokiStack indiziert nach Labels, nicht Volltext – billiger als Elasticsearch, Backend ist Objektspeicher (S3, ODF, MinIO)
  • drei Tenants: application, infrastructure, audit – RBAC und Retention je Tenant getrennt
  • der Observe → Logs-View der Konsole mit LogQL – ein Entwickler sieht nur den application-Tenant seiner Projekte
  • der ClusterLogForwarder-CR: inputs, outputs (die Ziele), pipelines (input → filter → output)
  • filters: Felder droppen, PII maskieren, Labels umschreiben, Rate-Limit je Pipeline
  • externe Ziele: Splunk, Elasticsearch, syslog, Kafka, Cloudwatch, GCP Logging, Azure Monitor, HTTP – jeweils mit TLS und einem Secret
  • ohne Forwarder-CR geht alles in die Default-LokiStack. Mit CR entscheidest du, was wohin geht
  • Falle: ein Output mit falschem Zertifikat staut die ganze Pipeline – Logs gehen verloren, der Puffer ist nicht unendlich
  • Bibliothek: die 8 Spring-Dienste loggen strukturiert als JSON nach stdout (Logback-Encoder), Vector labelt mit namespace=library und dem Pod-Namen – alles im application-Tenant
  • ohne die Logging-Operatoren (auf der SNO nicht per Default installiert) ist oc logs -f deploy/catalog-service -n library alles was du hast – nach einem sno.ps1 up fehlen die Logs von vor dem Snapshot
  • Metriken sind ein anderer Stack (G1) – Kafka-Consumer-Lag und langsame DB-Queries stehen nicht in den Logs, das ist Metrik oder der Postgres-Exporter
  • auf SNO ohne HA: Vector und ClusterLogForwarder laufen – die LokiStack braucht Objektspeicher, also ein MinIO daneben
  • OCP-Unterschied: identisch – frisch ist keine Logging-Komponente da, man installiert die Operatoren (auch in OKD verfügbar)
  • von Hand untersuchen: Logpfad und Logging-Stack – Container-, Kubelet- und CRI-O-Logs lesen und Logging 6 von der früheren Architektur unterscheiden

G3 Storage – CSI, StorageClass, PV/PVC-Bindung, RWO vs RWX

BetriebStorageCSIPV / PVCEX280

Ein Pod will einen Datenträger – er stellt einen PersistentVolumeClaim (den Antrag). Eine StorageClass ist die Vorlage, nach der ein PersistentVolume dynamisch entsteht und an den PVC gebunden wird. Der Zugriffsmodus entscheidet, ob mehrere Pods gleichzeitig schreiben dürfen – und die meiste Blockspeicherung kann das nicht.

PVC (Antrag) → StorageClass (Vorlage) → PV (der Datenträger), gebunden. PersistentVolumeClaim du legst an (oder Helm / ein StatefulSet) verlangt: storage: 20Gi accessModes: [ReadWriteOnce] storageClassName: (optional) namespaced – gehört zum Projekt StorageClass – die Vorlage clusterweit · eine ist Default (Annotation) provisioner – welcher CSI-Treiber parameters – Disk-Typ, fsType, Verschlüsselung reclaimPolicyDelete / Retain volumeBindingMode   Immediate vs WaitForFirstConsumer (PV landet in der Zone des Pods) allowVolumeExpansion: true – PVC nachträglich vergrößern CSI-Treiber = ein Operator je Speichersystem (*-csi-driver) PersistentVolume wird generiert (dynamisch) oder statisch angelegt clusterweit, nicht namespaced an genau einen PVC gebunden (Bound) bei Retain bleibt es nach PVC-Löschung (Released) der Pod mountet es über den PVC-Namen nach welcher Class? provisioniert + bindet Zugriffsmodi – wer darf gleichzeitig schreiben RWO (ReadWriteOnce) – ein Node zur Zeit, die meiste Blockspeicherung RWOP (ReadWriteOncePod) – genau ein Pod ROX (ReadOnlyMany) – viele Nodes lesend RWX (ReadWriteMany) – viele Nodes schreibend – braucht ein Netzwerk-FS (NFS, CephFS, ODF) die meiste Block-CSI (AWS EBS, Hetzner Cloud Volume, iSCSI) kann kein RWX ein Deployment mit >1 Replica und einem RWO-PVC → nur ein Pod läuft, der Rest bleibt Pending Snapshots, Erweitern, Atlas-Praxis VolumeSnapshot + VolumeSnapshotClass – Point-in-Time-Kopie, Basis für Backups (Velero) PVC vergrößern: den Wert unter spec.resources.requests.storage hochsetzen (oc edit pvc) nur größer, nie kleiner · nur wenn allowVolumeExpansion Atlas: auf SNO ein local-path-Provisioner (Daten am Node, keine echte HA) der local-path-provisioner-SA bekommt hostmount-anyuid (A7) – echte Cloud-CSI nur an einem echten Cluster oc get sc,pv,pvc · oc describe pvc <n> (Events nennen den Grund für Pending) Merksatz: PVC = Antrag, StorageClass = Vorlage, PV = der Datenträger – und RWX braucht ein Netzwerk-Dateisystem WaitForFirstConsumer ist fast immer richtig – sonst landet das PV in der falschen Zone und der Pod scheduled nie (B6) Bibliothek: Postgres, Kafka, Redis wollen je ein RWO-Block-PVC über ihre StatefulSets (B1) · das Log-Objektspeicher-Backend (G2) ist S3, kein PVC OCP-Unterschied: identisch – CSI ist Kubernetes. OpenShift Data Foundation (ODF/Ceph) ist ein zusätzliches Produkt für RWX + Objektspeicher im Cluster.
Antrag, Vorlage, Datenträger. Ein PVC verlangt Größe und Zugriffsmodus. Die StorageClass sagt, welcher CSI-Treiber mit welchen Parametern ein PV dynamisch erzeugt und daran bindet. volumeBindingMode: WaitForFirstConsumer wartet, bis der Pod geschedult ist, damit das PV in dessen Zone landet. Der Zugriffsmodus entscheidet: RWO = ein Node, RWX = viele – und RWX braucht ein Netzwerk-Dateisystem, das die meiste Blockspeicherung nicht bietet.
  • ein PersistentVolumeClaim ist der Antrag (Größe, accessModes, optional storageClassName) – namespaced
  • eine StorageClass ist die clusterweite Vorlage: provisioner (der CSI-Treiber, kommt als Operator), reclaimPolicy, volumeBindingMode, allowVolumeExpansion
  • dynamisches Provisioning erzeugt ein PersistentVolume und bindet es an den PVC (Bound) – oder es bindet an ein passendes statisches PV
  • volumeBindingMode: WaitForFirstConsumer wartet, bis der Pod geschedult ist – sonst landet das PV in der falschen Zone und der Pod scheduled nie (B6)
  • reclaimPolicy: Delete löscht das PV mit dem PVC, Retain behält es (Released) für manuelle Rettung
  • RWO (ein Node zur Zeit) – die meiste Blockspeicherung. RWOP = genau ein Pod
  • RWX (viele Nodes schreibend) braucht ein Netzwerk-Dateisystem (NFS, CephFS, ODF) – AWS EBS, Hetzner Volume, iSCSI können es nicht
  • ein Deployment mit >1 Replica an einem RWO-PVC → nur ein Pod läuft, der Rest bleibt Pending
  • VolumeSnapshot + VolumeSnapshotClass für Point-in-Time-Kopien (Basis für Velero-Backups, E4)
  • PVC vergrößern geht nur größer, nie kleiner, und nur bei allowVolumeExpansion
  • auf der SNO: ein local-path-Provisioner statt echtem CSI (Daten am Node, keine HA). Sein SA braucht die SCC hostmount-anyuid (A7), und das Basis-Verzeichnis muss chcon -t container_file_t bekommen – SCOS-SELinux lässt sonst auch root dort nicht schreiben
  • Bibliothek: Postgres, Kafka und Redis wollen je ein RWO-Block-PVC über ihre StatefulSets (B1)
  • Stolperstein: der image-registry-Operator will eine RWX-PVC, local-path kann nur RWO – die image-registry-storage-PVC von Hand als RWO vorab anlegen, sonst startet die Registry nicht
  • ein geteiltes Verzeichnis (etwa Buchcover für alle catalog-service-Replicas) bräuchte RWX – mit local-path unmöglich, also Objekt-Storage (MinIO, S3) oder ein NFS-Volume
  • OCP-Unterschied: identisch – CSI ist Kubernetes. OpenShift Data Foundation (Ceph) ist ein Zusatzprodukt für RWX und Objektspeicher im Cluster
  • von Hand durchspielen: cluster-betrieb-openshift.html – ein PVC anlegen und die Bindung live beobachten

G4 MachineConfig & der MCO – wie RHCOS-Nodes konfiguriert werden

BetriebMachineConfigMCORHCOS

RHCOS/SCOS ist ein unveränderliches Betriebssystem – du gehst nicht per SSH rein und editierst Dateien. Jede Änderung am Node ist ein MachineConfig-Objekt (ein Ignition-Schnipsel). Der Machine Config Operator mischt alle MachineConfigs eines Pools zu einer Konfig und rollt sie Node für Node aus: cordon, drain, schreiben, reboot, uncordon.

Kein SSH-Editieren – alles über MachineConfig, ausgerollt Node für Node. MachineConfig (Objekt) ein Ignition-Schnipsel: Dateien (/etc/…), systemd-Units Kernel-Argumente, kernelType Label …/role: worker ordnet es einem Pool zu höhere CRs: KubeletConfig, ContainerRuntimeConfig → MCO macht MachineConfigs draus MachineConfigPool master, worker, oder ein eigener Pool der MCO mischt alle MachineConfigs des Pools zu einer gerenderten Konfig (rendered-worker-<hash>) maxUnavailable: wie viele Nodes gleichzeitig paused: true → Rollout einfrieren (Wartungsfenster) oc get mcp – UPDATED / UPDATING / DEGRADED, Maschinenzahlen DEGRADED = ein Node kam nicht durch (SSH rein, journalctl) der MCO rollt aus – Node für Node 1 cordon – keine neuen Pods auf diesen Node 2 drain – laufende Pods verdrängen (PodDisruptionBudget beachtet) 3 die neue Konfig auf die Platte schreiben (der machine-config-daemon je Node) 4 reboot – der Node startet mit der neuen Konfig 5 uncordon – Node nimmt wieder Pods, dann der nächste auch ein einzelner SNO-Node macht das → kurze Downtime der Steuerungsebene manche Datei-Änderungen ohne Reboot (der Daemon erkennt das) wofür man MachineConfigs nimmt chrony / NTP-Server · Registry-Mirror (registries.conf, ImageDigestMirrorSet) Kernel-Argumente (hugepages, nosmt), Realtime-Kernel zusätzliche CA-Zertifikate ins System-Trust, eine systemd-Unit SSH-Keys für core · crio-Einstellungen (via ContainerRuntimeConfig) eine kaputte MachineConfig kann alle Nodes eines Pools lahmlegen – erst an einem Custom-Pool testen Beziehung zu Upgrades & Nodes ein Cluster-Upgrade (G5) liefert oft eine neue Basis-MachineConfig → der MCO rollt die Nodes darum dauert ein Upgrade so lang – jeder Node cordon/drain/reboot, nacheinander Pool pausieren vor einem EUS-Hop, danach entpausen (G5) oc debug node/<name>chroot /host zum Nachsehen (nicht zum Ändern) oc get machineconfig · oc get mcp -w · oc get nodes (SchedulingDisabled = mitten im Rollout) Merksatz: MachineConfig = eine Node-Änderung als Objekt, MachineConfigPool = die Gruppe, MCO = rollt sie nacheinander mit Reboot aus niemals per SSH Dateien ändern – beim nächsten Reconcile oder Reboot ist es weg, und der Pool wird Degraded baut auf dem unveränderlichen Node-OS aus Diagramm 01 (SCOS/RHCOS) auf · Node-Auswahl für Pods: B6 OCP-Unterschied: identisch – MCO und MachineConfig sind Kern-OpenShift, in OKD und OCP gleich (RHCOS vs SCOS als OS).
Der Node ist unveränderlich – die Konfig ist ein Objekt. Eine MachineConfig trägt einen Ignition-Schnipsel (Dateien, systemd-Units, Kernel-Args). Alle MachineConfigs eines MachineConfigPool (master/worker) werden vom MCO zu einer gerenderten Konfig gemischt und dann Node für Node ausgerollt: cordon → drain → schreiben → reboot → uncordon. Darum dauert ein Upgrade so lang. Per SSH editieren macht den Pool Degraded.
  • RHCOS/SCOS ist unveränderlich – jede Node-Änderung ist ein MachineConfig-Objekt (ein Ignition-Schnipsel: Dateien, systemd-Units, Kernel-Args)
  • ein Label machineconfiguration.openshift.io/role ordnet die MachineConfig einem MachineConfigPool zu – der MCO mischt alle eines Pools zu einer rendered-<pool>-<hash>-Konfig
  • der Rollout geht Node für Node: cordondrain (PodDisruptionBudget beachtet) → schreiben → rebootuncordon
  • maxUnavailable am Pool steuert die Parallelität, paused: true friert den Rollout ein (Wartungsfenster)
  • oc get mcp zeigt UPDATED / UPDATING / DEGRADED und die Maschinenzahlen – DEGRADED = ein Node kam nicht durch
  • höhere CRs: KubeletConfig und ContainerRuntimeConfig – der MCO übersetzt sie in MachineConfigs
  • typische Nutzung: chrony/NTP, Registry-Mirror (registries.conf), Kernel-Args (hugepages), extra CA-Zertifikate, eine systemd-Unit, SSH-Keys für core
  • ein Cluster-Upgrade (G5) liefert oft eine neue Basis-MachineConfig → darum rollt jedes Upgrade alle Nodes mit Reboot
  • niemals per SSH Dateien ändern – beim nächsten Reconcile oder Reboot ist es weg und der Pool wird Degraded. Nur nachsehen: oc debug node/<name> dann chroot /host
  • Falle: eine kaputte MachineConfig kann alle Nodes eines Pools lahmlegen – erst an einem Custom-Pool testen
  • real auf der SNO: tailscaled wurde von Hand per SSH als statisches Binary und systemd-Unit installiert – überlebt einen Reboot, aber ein Cluster-Upgrade (G5) rollt den Node neu aus und es ist weg. Sauber wäre eine MachineConfig mit der Unit
  • der SSH-Key für core kam über install-config.yaml (sshKey:) in die Ignition – das ist schon eine MachineConfig, die einzige, die wir bewusst gesetzt haben
  • auf SNO ohne HA: die Rollout-Mechanik sieht man am einen Node (cordon/drain/reboot, kurze Control-Plane-Downtime) – nur maxUnavailable über einen Pool zeigt sich erst mit Workern
  • OCP-Unterschied: identisch – MCO und MachineConfig sind Kern-OpenShift (RHCOS vs SCOS als OS)
  • sicher von Hand proben: MachineConfig und MCO – gerenderte Konfiguration bis zum Node zurückverfolgen und ein eigenes Manifest nur per serverseitigem Dry-Run prüfen

G5 Cluster-Upgrades – der Cluster Version Operator, Upgrade-Channels, oc adm upgrade

BetriebUpgradesCVOChannelsEX280

Ein OpenShift-Upgrade ist ein Ziel: ein Release-Image, das die Manifeste aller Komponenten enthält. Der Cluster Version Operator (CVO) gleicht den ganzen Cluster darauf ab – er geht die ClusterOperatoren der Reihe nach durch, dann rollt der MCO die Nodes. Du wählst nur den Channel und das Ziel, den Rest macht der CVO. Ein Rollback gibt es nicht.

Ein Release-Image, ein CVO, der alles der Reihe nach abgleicht – nur vorwärts. Channel wählen stable-4.16 – getestet, empfohlen fast-4.16 – früher, weniger Vorlauf candidate-4.16 – Vorschau, nicht Prod eus-4.x – gerade Minors, längerer Support der Channel = ein Graph erlaubter Upgrade-Pfade oc adm upgrade channel stable-4.16 Cluster Version Operator reconciled den Cluster auf ein Release-Image (ein Digest: alle Component-Manifeste + Operator-Images) geht die ClusterOperatoren in Abhängigkeitsreihenfolge durch jeder Operator hebt sein Operand-Deployment (G6) dann rollt der MCO die Nodes (G4) – darum dauert es oc get clusterversion zeigt Fortschritt in Prozent Vorbedingungen & Regeln kein ClusterOperator Degraded (G6) – sonst blockiert der CVO alle PodDisruptionBudgets erfüllbar, genug Kapazität für drain admin-acks bestätigen: bekannte Breaking Changes zwischen Minors deprecated APIs prüfen (APIRemovedInNextReleaseInUse-Alarm) kein Rollback – nur vorwärts, in einer niedrigeren Umgebung testen immer nur ein Minor auf einmal (4.15 → 4.16, nicht 4.15 → 4.17) der Ablauf in Befehlen oc adm upgrade zeigt aktuelle Version + verfügbare Ziele oc adm upgrade --to=4.16.7 oder --to-latest Fortschritt: oc get clusterversion -w, oc get co (was ist noch Progressing) Console: Administration → Cluster Settings → Update (mit Fortschrittsbalken) nach Start: nicht abbrechen – ein halb-upgradeter Cluster ist der schlechteste Zustand EUS-zu-EUS (4.14 → 4.16) Worker-Pools paused (G4) → die Steuerungsebene über 4.15 auf 4.16 hopsen → dann Worker-Pools entpausen → sie springen direkt auf 4.16 spart einen kompletten Node-Rollout-Zyklus EUS = Extended Update Support, gerade Minors (4.14, 4.16…), ~24 Monate für Cluster, die selten upgraden dürfen – nicht der Normalfall Merksatz: du wählst Channel + Ziel, der CVO gleicht alles ab (Operatoren zuerst, dann Nodes), Rollback gibt es nicht ein Degraded-ClusterOperator (G6) blockiert das Upgrade – darum ist oc get co der erste Schritt der Node-Teil ist G4 (MCO) · die Health-Ampel ist G6 · etcd-Backup vor dem Upgrade: G7 OCP-Unterschied: OCP hat Subscription-gebundene Channels und OpenShift Update Service (OSUS) für disconnected. OKD nutzt eigene, community-gepflegte Update-Graphen.
Ein Ziel, ein Operator, eine Richtung. Der CVO gleicht den ganzen Cluster auf ein Release-Image ab – er hebt die ClusterOperatoren in Reihenfolge, dann rollt der MCO die Nodes (G4). Du wählst den Channel (stable-4.16, eus-4.x…) und das Ziel mit oc adm upgrade --to=. Vorbedingung: kein Degraded-ClusterOperator (G6), admin-acks bestätigt. Ein Rollback wird nicht unterstützt – man testet in einer niedrigeren Umgebung.
  • ein Upgrade ist ein Release-Image – ein Digest mit den Manifesten jeder Komponente und den Operator-Images
  • der Cluster Version Operator (CVO) gleicht den Cluster darauf ab: erst die ClusterOperatoren in Abhängigkeitsreihenfolge, dann rollt der MCO die Nodes (G4)
  • Channels: stable-4.16 (empfohlen), fast, candidate, dazu eus-4.x – jeder Channel ist ein Graph erlaubter Pfade, immer nur ein Minor auf einmal
  • oc adm upgrade zeigt aktuelle Version und verfügbare Ziele, oc adm upgrade --to=4.16.7 startet
  • Vorbedingung: kein ClusterOperator Degraded (G6) – sonst blockiert der CVO
  • admin-acks bestätigen für bekannte Breaking Changes zwischen Minors, deprecated APIs prüfen (APIRemovedInNextReleaseInUse-Alarm)
  • PodDisruptionBudgets müssen erfüllbar sein und genug Kapazität für drain vorhanden
  • kein Rollback – nur vorwärts, in einer niedrigeren Umgebung testen
  • nach dem Start nicht abbrechen – ein halb-upgradeter Cluster ist der schlechteste Zustand
  • Fortschritt: oc get clusterversion -w, oc get co (was ist noch Progressing), oder die Console unter Cluster Settings
  • etcd-Backup vor dem Upgrade (G7) · der Node-Teil ist G4 · die Health-Ampel ist G6
  • real auf der SNO: OKD-SCOS-Update-Graphen sind community-gepflegt und dünn – oft ist der Weg nicht oc adm upgrade, sondern sno.ps1 destroy und dann sno.ps1 install mit der neueren Version (der Rebuild aus dem Plan)
  • wenn doch ein Upgrade: erst sno.ps1 down als Sicherung, dann oc adm upgrade --to, rund 40 min – der eine Node rebootet, die API ist während des Control-Plane-Reboots kurz weg
  • auf SNO ohne HA: ein oc adm upgrade läuft und der CVO geht die COs durch – nur der EUS-Worker-Pool-Tanz braucht Worker
  • OCP-Unterschied: OCP hat Subscription-Channels und OpenShift Update Service (disconnected). OKD nutzt community-gepflegte Update-Graphen
  • von Hand durchspielen: cluster-betrieb-openshift.html – den Upgrade-Weg auf OKD-SNO verstehen, warum es oft eher ein Rebuild als ein oc adm upgrade ist

G6 ClusterOperators – was oc get co sagt: Available vs Degraded

BetriebClusterOperatorsoc get coDegradedEX280

Jede Kern-Fähigkeit des Clusters – Auth, Ingress, DNS, Registry, Monitoring, Netzwerk, etcd, kube-apiserver – ist ein ClusterOperator-Objekt, verwaltet von seinem Operator. Jedes meldet drei Zustände: Available (nutzbar?), Degraded (läuft, aber ungesund?), Progressing (mitten in einer Änderung?). oc get co ist die Gesundheits-Ampel des Clusters.

Die Gesundheits-Ampel: Available · Degraded · Progressing. Available – ist die Funktion nutzbar? True = die Fähigkeit funktioniert (mindestens teilweise) False = Ausfall – z. B. ingress False → keine Route erreichbar das ist der wichtigste der drei Beispiele: authentication, kube-apiserver, etcd, dns, image-registry, monitoring, network, storage rund 30 Stück – oc get co listet alle Degraded – läuft, aber ungesund? True = die Funktion geht (noch), ist aber nicht im Sollzustand z. B. 2 von 3 Registry-Repliken laufen, ein PVC fehlt = untersuchen, nicht ignorieren – wird oft zu Available: False der CVO (G5) blockiert Upgrades, solange irgendein CO Degraded ist die message am CO nennt fast immer den Grund ClusterOperatorDegraded-Alarm feuert an Alertmanager (G1) Progressing – mitten in einer Änderung? True = der Operator rollt gerade etwas aus normal während eines Upgrades (G5) oder einer Konfig-Änderung bleibt es stundenlang True → es hängt, nachsehen gesund im Ruhezustand: Available=True, Degraded=False, Progressing=False dazu Upgradeable=False heisst: erst was tun, dann upgraden einen kaputten ClusterOperator lesen oc get co → welcher ist nicht True/False/False? oc describe co image-registry → die message + Related Objects 2 · in den genannten Namespace: oc get pods -n openshift-image-registry 3 · oc logs des Operator-Pods, oc get events im Operand-Namespace hier fängt jede „der Cluster fühlt sich komisch an“-Untersuchung an die Operator-Ebenen der CVO verwaltet die ~30 Second-Level-Operatoren jeder Second-Level-Operator verwaltet seine Operand-Pods und meldet Status über sein ClusterOperator-Objekt OLM-Operatoren (Block D) sind eine separate Ebene – nicht in oc get co die Konsole: Home → Overview zeigt dieselbe Ampel grafisch Merksatz: Available=False = Ausfall, Degraded=True = untersuchen, Progressing=True = normal beim Upgrade – alle drei falsch/wahr richtig = gesund ein Degraded-CO blockiert das Upgrade (G5) – darum ist oc get co der erste Schritt vor jeder größeren Aktion Alarme dazu: ClusterOperatorDown / ClusterOperatorDegraded an Alertmanager (G1) OCP-Unterschied: identisch – ClusterOperators sind Kern-OpenShift, die Menge ist in OKD und OCP fast gleich.
Die Ampel des Clusters. Jede Kern-Fähigkeit ist ein ClusterOperator-Objekt mit drei Conditions: Available (nutzbar? False = Ausfall), Degraded (läuft, aber nicht im Soll? True = untersuchen), Progressing (mitten in einer Änderung? normal beim Upgrade). oc get co zeigt sie alle – alles True/False/False = gesund. Ein Degraded-Operator blockiert Upgrades. Man liest ihn über die message → den Operand-Namespace → die Pod-Logs.
Die rund 30 Default-ClusterOperators – Operand-Namespace und typische Pods
ClusterOperatorOperand-Namespacetypische Pods/Objekte
Control Plane
etcdopenshift-etcdetcd-<node>
kube-apiserveropenshift-kube-apiserverkube-apiserver-<node>
kube-controller-manageropenshift-kube-controller-managerkube-controller-manager-<node>
kube-scheduleropenshift-kube-scheduleropenshift-kube-scheduler-<node>
kube-storage-version-migratoropenshift-kube-storage-version-migratormigrator-*
openshift-apiserveropenshift-apiserverapiserver-*
openshift-controller-manageropenshift-controller-managercontroller-manager-*
Plattform-Dienste
authenticationopenshift-authenticationoauth-openshift-*
consoleopenshift-consoleconsole-*, downloads-*
dnsopenshift-dnsdns-default-* (DaemonSet), node-resolver-* (DaemonSet)
ingressopenshift-ingressrouter-default-*
image-registryopenshift-image-registryimage-registry-* – auf SNO/CRC per Default Removed (C5)
monitoringopenshift-monitoringprometheus-k8s-*, alertmanager-main-*, thanos-querier-*, node-exporter-* (DaemonSet)
networkopenshift-ovn-kubernetesovnkube-node-* (DaemonSet), ovnkube-control-plane-*
node-tuningopenshift-cluster-node-tuning-operatortuned-* (DaemonSet)
service-caopenshift-service-caservice-ca-*
config-operatoropenshift-config-operatoropenshift-config-operator-*
insightsopenshift-insightsinsights-operator-*
Operator-Unterbau (OLM selbst, D2)
operator-lifecycle-manageropenshift-operator-lifecycle-managerolm-operator-*
operator-lifecycle-manager-catalogopenshift-operator-lifecycle-managercatalog-operator-*
operator-lifecycle-manager-packageserveropenshift-operator-lifecycle-managerpackageserver-*
marketplaceopenshift-marketplaceredhat-operators-*, community-operators-*… (D3)
Maschinen & Infrastruktur – auf einer UPI-SNO wie dieser meist inert
machine-apiopenshift-machine-apimachine-api-operator-*
machine-approveropenshift-cluster-machine-approvermachine-approver-*
machine-configopenshift-machine-config-operatormachine-config-daemon-* (DaemonSet), machine-config-controller-*, machine-config-server-*
cloud-controller-manageropenshift-cloud-controller-managerje Provider – auf Bare-Metal meist leer
cloud-credentialopenshift-cloud-credential-operatorcloud-credential-operator-*
control-plane-machine-setopenshift-machine-apicontrol-plane-machine-set-operator-*
baremetalopenshift-machine-apinur aktiv bei Baremetal-IPI-Installern
Storage
storageopenshift-cluster-storage-operatorcluster-storage-operator-*
csi-snapshot-controlleropenshift-cluster-storage-operatorcsi-snapshot-controller-*
  • jede Kern-Fähigkeit (auth, ingress, dns, image-registry, monitoring, network, etcd, kube-apiserver…) ist ein ClusterOperator-Objekt – rund 30 Stück, verwaltet vom CVO
  • die volle Liste mit Operand-Namespace und typischen Pods steht in der Tabelle oben – das sind zugleich die Default-Pods, die oc get pods -A auf jedem frischen Cluster zeigt, bevor auch nur eine eigene App läuft
  • auf einer Bare-Metal-SNO wie dieser sind machine-api, machine-approver, cloud-controller-manager, cloud-credential, control-plane-machine-set und baremetal zwar Available, tun aber praktisch nichts – sie sind für Cloud- oder IPI-Installer gedacht, die hier nicht laufen
  • Available: ist die Funktion nutzbar? False = Ausfall (z. B. ingress False → keine Route erreichbar)
  • Degraded: die Funktion geht noch, ist aber nicht im Sollzustand – True = untersuchen, wird oft zu Available: False
  • Progressing: der Operator rollt gerade etwas aus – normal beim Upgrade, aber stundenlang True = es hängt
  • gesund im Ruhezustand: Available=True, Degraded=False, Progressing=False. Upgradeable=False = erst etwas tun, dann upgraden
  • der CVO (G5) blockiert Upgrades, solange irgendein CO Degraded ist
  • einen kaputten CO lesen: oc get co → wer ist nicht True/False/False? → oc describe co <name> für die message
  • dann in den Related Objects-Namespace: oc get pods -n <operand-ns>, oc logs des Operator-Pods, oc get events
  • hier fängt jede „der Cluster fühlt sich komisch an“-Untersuchung an
  • OLM-Operatoren (Block D) sind eine separate Ebene – sie erscheinen nicht in oc get co
  • Alarme dazu: ClusterOperatorDown / ClusterOperatorDegraded an Alertmanager (G1)
  • nach einem sno.ps1 up steht image-registry oft auf Progressing oder Degraded – der Operator hängt an einer Leader-Lease von vor dem Snapshot. sno-wsl.sh remediate (im up eingebaut) räumt das
  • Bibliothek: schlägt ein helm upgrade mit wirren Fehlern fehl, ist der erste Blick oc get co – ist ingress oder authentication Degraded, weil der Cluster nach dem Restore noch nicht durch ist?
  • OCP-Unterschied: identisch – ClusterOperators sind Kern-OpenShift, die Menge ist in OKD und OCP fast gleich
  • von Hand durchspielen: cluster-betrieb-openshift.html – oc get co lesen und einen kaputten ClusterOperator debuggen

G7 etcd – Quorum, Backup und Restore, die SNO-Besonderheiten

BetriebetcdQuorumBackup / Restore

etcd ist die Schlüssel-Wert-Datenbank des Clusters – jedes API-Objekt liegt darin. Sie repliziert per Raft und braucht deshalb ein Quorum (3 Member überstehen 1 Ausfall, 5 Member 2). Auf jedem Control-Plane-Node läuft ein etcd-Static-Pod, verwaltet vom etcd-Operator. Ein Snapshot ist die einzige Absicherung – und es gibt zwei Wege dazu (der etcd-Snapshot und der Hetzner-Disk-Snapshot).

Die Datenbank hinter dem API-Server – Raft, Quorum, und ein Snapshot als Notausgang. was etcd ist verteilte Schlüssel-Wert-Datenbank – jedes Objekt (Pod, Secret, CO…) liegt hier, sonst nirgends der kube-apiserver ist der einzige Client ein etcd-Static-Pod je Control-Plane-Node verwaltet vom etcd-Operator (cluster-etcd-operator) gemeldet als ClusterOperator etcd (G6) Namespace openshift-etcd · oc get etcd cluster -o yaml Encryption at rest (B9) verschlüsselt die Werte – auch im Snapshot Raft & Quorum Schreibvorgänge gehen an den Leader, der sie repliziert ein Schreibvorgang gilt erst mit Mehrheit als bestätigt 3 Member → Mehrheit 2 → überstehen 1 Ausfall 5 Member → Mehrheit 3 → überstehen 2 Ausfälle unter der Mehrheit → Quorum verloren → API read-only / tot darum haben Cluster 3 (oder 5) Control-Plane-Nodes, nie 2 oder 4 2 von 3 tot = manueller Restore nötig, kein Selbstheilen der Operator ersetzt einen einzelnen ausgefallenen Member automatisch SNO – ein Member, kein Quorum Single-Node OpenShift hat genau einen etcd-Member kein Quorum, keine HA – Node weg = Cluster weg ein Backup ist der einzige Rettungsweg der Disk-Snapshot von sno.ps1 down/up fängt etcd mit ein zusätzlich cluster-backup.sh laufen lassen (Cron) Restore auf SNO = cluster-restore.sh auf demselben Node unser SNO ist der Alltags-Arbeitsplatz – hier reicht der Disk-Snapshot der 3-Node-Cluster kommt nur für Block G kurz hoch Backup & Restore Backup (auf einem Control-Plane-Node, als root): /usr/local/bin/cluster-backup.sh /home/core/backup snapshot.db + static-pod-resourcesvor jeder riskanten Änderung + per Cron Restore (Quorum verloren): einen Node wählen → cluster-restore.sh mit dem Snapshot → dieser Node wird die einzige Wahrheit → andere treten neu bei & syncen → Steuerungsebene startet neu danach: oc get co (G6) durchsehen, evtl. hängende Operatoren neu starten Pflege & Diagnose Defrag: die DB wächst durch alte Revisionen – der Operator defragmentiert automatisch, sonst oc rsh + etcdctl defrag oc get pods -n openshift-etcd – laufen alle Member? etcdctl endpoint status / member list im Pod Alarme: etcdInsufficientMembers, etcdHighFsyncDurations etcd will schnelle Disks (fsync-Latenz) – SSD/NVMe, nie HDD Merksatz: etcd hält jedes API-Objekt, repliziert per Raft, braucht Mehrheit – 3 Nodes überstehen 1 Ausfall, 5 Nodes 2 verlorenes Quorum heilt nicht von selbst – cluster-restore.sh aus einem snapshot.db ist der Weg zurück SNO: ein Member, kein Quorum – der sno.ps1-Disk-Snapshot ist die Absicherung · Backup vor Upgrade (G5) und MachineConfig (G4) OCP-Unterschied: identisch – etcd-Operator, cluster-backup.sh/cluster-restore.sh und die Quorum-Regeln sind in OKD und OCP gleich.
Die Datenbank des Clusters. etcd speichert jedes API-Objekt und repliziert per Raft – ein Schreibvorgang gilt erst mit Mehrheit als bestätigt, also überstehen 3 Member 1 Ausfall und 5 Member 2. Ein Static-Pod je Control-Plane-Node, verwaltet vom etcd-Operator und als ClusterOperator etcd gemeldet (G6). cluster-backup.sh erzeugt eine snapshot.db – vor jeder riskanten Änderung. Bei verlorenem Quorum ist cluster-restore.sh aus dem Snapshot der einzige Weg zurück. Auf SNO gibt es nur einen Member und kein Quorum – da ist der sno.ps1-Disk-Snapshot die Absicherung.
  • etcd ist die verteilte Schlüssel-Wert-Datenbank des Clusters – jedes API-Objekt (Pod, Secret, ClusterOperator…) liegt darin und sonst nirgends
  • ein etcd-Static-Pod je Control-Plane-Node, verwaltet vom etcd-Operator (cluster-etcd-operator), gemeldet als ClusterOperator etcd (G6) – der kube-apiserver ist der einzige Client
  • Raft: Schreibvorgänge gehen an den Leader und gelten erst mit Mehrheit der Member als bestätigt
  • Quorum: 3 Member überstehen 1 Ausfall, 5 Member 2 – darum 3 oder 5 Control-Plane-Nodes, nie 2 oder 4. Einen einzelnen Ausfall ersetzt der Operator, unter die Mehrheit gefallen heilt nicht von selbst
  • Backup: /usr/local/bin/cluster-backup.sh /home/core/backup auf einem Control-Plane-Node → snapshot.db + static-pod-resources
  • Backup vor jeder riskanten Änderung (Upgrade G5, MachineConfig G4) und regelmäßig per Cron
  • Restore bei verlorenem Quorum: einen Node wählen, cluster-restore.sh mit dem Snapshot → er wird die einzige Wahrheit, die anderen treten neu bei und syncen, die Steuerungsebene startet neu
  • Defrag: die DB wächst durch alte Revisionen – der Operator defragmentiert automatisch, notfalls oc rsh plus etcdctl defrag
  • etcd braucht schnelle Disks (niedrige fsync-Latenz) – SSD/NVMe, nie HDD. Alarme dazu: etcdInsufficientMembers, etcdHighFsyncDurations
  • SNO: genau ein Member, kein Quorum, keine HA – Node weg = Cluster weg, ein Backup ist der einzige Rettungsweg
  • zwei Backup-Wege: der richtige ist cluster-backup.sh (etcd-Snapshot, im echten Cluster Pflicht) – der pragmatische ist der Hetzner-Disk-Snapshot von sno.ps1 down/up, der etcd mit einfängt
  • auf einem Wegwerf-SNO nimmst du den zweiten und sparst dir den etcd-Snapshot – Quorum-Verlust und cluster-restore.sh übst du an einem lokalen kind-Cluster mit drei Control-Plane-Nodes
  • was im Snapshot steckt: sno.ps1 down stoppt kubelet und snapshottet die Nodes-Disk – etcd liegt darauf, also ist der ganze Cluster-Zustand mit drin, auch die veraltete image-registry-Lease und fehlende SA-Tokens. Darum räumt sno-wsl.sh remediate nach dem up auf (G6)
  • Diagnose: oc get pods -n openshift-etcd, oc get etcd cluster -o yaml, etcdctl endpoint status im Pod – Encryption at rest (B9) verschlüsselt die Werte, auch im Snapshot
  • OCP-Unterschied: identisch – etcd-Operator, die Backup- und Restore-Skripte und die Quorum-Regeln sind in OKD und OCP gleich
  • Backup wirklich erzeugen: etcd-Backup auf einem Control-Plane-Node – beide erzeugten Dateien außerhalb des Clusters sichern und von Anwendungs- und Disk-Backups abgrenzen

Block H – Legacy-Migration (Novaris)

H1 SCC & root – was bricht, wenn eine Legacy-App UID 0 erwartet

LegacySCCrootUID 0

Die alte Novaris-App ist als root gebaut, chownt Verzeichnisse beim Start und schreibt nach festen Pfaden. Unter der Vorgabe-SCC restricted-v2 (A7) bekommt der Pod eine zufällige hohe UID – und alles davon schlägt fehl. Das ist der häufigste Blocker beim Migrieren. Der Fix ist das Image, nicht die SCC.

Die alte App will Root – restricted-v2 gibt ihr eine zufällige UID. die alte Novaris-Annahme läuft als root (UID 0), USER nicht gesetzt chown -R auf Verzeichnisse beim Start schreibt nach festen Pfaden (/opt/novaris/logs) bindet Port 80 erwartet ein beschreibbares Root-Dateisystem auf WAS traditional lief das jahrelang problemlos der Container ist als root gebaut, weil es „schon immer“ so war nichts davon geht unter der Vorgabe-SCC unter restricted-v2 (A7) zufällige hohe UID (z. B. 1000700003), nicht 0 chownOperation not permitted Schreiben nach /opt/novaris/logsPermission denied Port 80 → nicht erlaubt (nur ab 1024) Symptome: CrashLoopBackOff container has runAsNonRoot and image will run as root welche SCC der Pod bekam: Annotation openshift.io/scc Fix in dieser Reihenfolge 1 Image UID-agnostisch bauen Dateien Gruppe root, chmod -R g=u, kein chown USER 1001, Port 8080, readOnlyRootFilesystem (H2) 2 Brücke: anyuid an genau die eine SA läuft dann als die UID aus dem Image (auch 0) nicht am Namespace, nicht an einer Gruppe 3 nie privileged als Reflex nonroot-v2 reicht, wenn das Image nur einen festen Nicht-Null-UID will Ziel: zurück auf restricted-v2, sobald das Image sitzt die Brücke einbauen (Schritt 2) oc create sa novaris-legacy -n novaris oc adm policy add-scc-to-user anyuid -z novaris-legacy -n novaris dann serviceAccountName: novaris-legacy in der Deployment-Vorlage prüfen mit oc get pod <p> -o jsonpath auf die scc-Annotation Grundlagen: A7 ↗ die SCC zählt die der ServiceAccount, nicht die des Users darum ist die SA der richtige Ort für die Ausnahme der häufigste Legacy-Blocker, und der erste beim Migrieren Novaris: die 3 Legacy-Module bekommen je eine eigene SA Merksatz: nicht die SCC lockern und vergessen – das Image reparieren, anyuid nur als Brücke an einer SA Reihenfolge der Fehler: erst runAsNonRoot-Abweisung, dann chown, dann Schreibrechte, dann der kleine Port OCP-Unterschied: identisch – SCC ist Kern-OpenShift. Auf reinem Kubernetes bremst stattdessen die Pod Security Admission (baseline / restricted).
Root ist nicht mehr selbstverständlich. Die alte Novaris-App läuft als root, chownt beim Start und bindet Port 80 – unter restricted-v2 (A7) bekommt der Pod eine zufällige hohe UID, und jeder dieser Schritte scheitert mit Operation not permitted oder Permission denied. Der richtige Fix ist das Image: Dateien der Gruppe root, chmod g=u, kein chown, Port 8080. anyuid an genau die eine ServiceAccount ist nur die Brücke, bis das Image sitzt – nie privileged.
  • die alte App läuft als root, chownt Verzeichnisse, schreibt nach festen Pfaden und bindet Port 80 – auf WAS traditional lief das jahrelang
  • unter restricted-v2 (A7) bekommt der Pod eine zufällige hohe UID, nicht 0 – und damit scheitert alles davon
  • chown gibt Operation not permitted, Schreiben gibt Permission denied, Port 80 ist nicht erlaubt
  • Symptome: CrashLoopBackOff, container has runAsNonRoot and image will run as root, EACCES in den Logs
  • welche SCC der Pod bekam, steht in der Pod-Annotation openshift.io/scc
  • Fix 1 (der richtige): Image UID-agnostisch bauen – Dateien Gruppe root, chmod -R g=u, kein chown, USER 1001, Port 8080
  • Fix 2 (die Brücke): anyuid an genau die eine ServiceAccount – nicht am Namespace, nicht an einer Gruppe
  • Fix 3: nie privileged als Reflex – nonroot-v2 reicht, wenn das Image nur einen festen Nicht-Null-UID braucht
  • Brücke einbauen: oc create sa novaris-legacy -n novaris, oc adm policy add-scc-to-user anyuid -z novaris-legacy -n novaris
  • dann serviceAccountName: novaris-legacy in die Deployment-Vorlage – die SCC zählt die der SA, nicht die des Users (A7)
  • Novaris: die drei Legacy-Module bekommen je eine eigene SA, damit die Ausnahme so klein wie möglich bleibt
  • Ziel bleibt restricted-v2: sobald das Image UID-agnostisch ist, die anyuid-Ausnahme wieder entfernen – die Brücke ist temporär
  • OCP-Unterschied: identisch – auf reinem Kubernetes bremst stattdessen die Pod Security Admission (baseline / restricted)
  • von Hand durchspielen: legacy-modernisierung-openshift.html – die zufällige UID und den chown-Fehler live provozieren, die anyuid-Brücke einbauen

H2 Schreibbares Dateisystem – readOnlyRootFilesystem, emptyDir für /tmp

LegacyreadOnlyRootFilesystememptyDir

Legacy-Apps schreiben mitten ins Image: nach /tmp, in Log-Verzeichnisse, in Arbeits- und Temp-Ordner des App-Servers. Die saubere Lösung: das Root-Dateisystem schreibgeschützt stellen (readOnlyRootFilesystem: true) und an jeden Pfad, der beschreibbar sein muss, ein emptyDir mounten. Logs gehen sowieso nach stdout.

Das Image ist read-only – beschreibbare Inseln kommen als emptyDir. Container – Root-FS schreibgeschützt securityContext.readOnlyRootFilesystem: true nichts in den Image-Layern ist veränderbar – Best Practice beschreibbare Mounts (je ein emptyDir): /tmp (medium: Memory = tmpfs) /opt/IBM/WebSphere/AppServer/profiles/.../logs /opt/IBM/WebSphere/AppServer/profiles/.../temp /wstemp /opt/novaris/work alles andere: ein Schreibversuch → Read-only file system so fällt beim ersten Lauf auf, was die App wirklich braucht Reihenfolge: erst ohne Mounts starten, die Fehler sammeln, dann Mounts ergänzen oc set volume deploy/novaris --add --type=emptyDir --mount-path=/tmp emptyDir – die Eigenschaften pro Pod, leer beim Start, weg beim Neustart liegt auf der Node-Disk (oder RAM bei medium: Memory) sizeLimit deckelt den Verbrauch passt für Temp, Cache, Logs – alles Flüchtige nicht für Daten, die überleben müssen Logs statt Log-Dateien der App-Server auf stdout/stderr umstellen Liberty: <logging consoleLogLevel="INFO"/> dann sammelt Vector die Logs (G2), kein Log-Volume nötig das Log-emptyDir bleibt nur für das, was der Server erzwingt oc logs -f deploy/novaris statt in den Pod steigen welches Volume wofür emptyDir Temp, Cache, Scratch – darf weg sein emptyDir Memory schnelles /tmp, zaehlt gegen das RAM-Limit PVC Daten, die einen Neustart überleben (G3) ConfigMap/Secret Konfiguration, nicht Daten (B8, H5) Regel: teilen zwei Pods dieselben Bytes → PVC (RWX) oder S3 Falle: Upload-Ordner als emptyDir → Datei landet auf Pod A, der Download geht an Pod B → 404 Uploads gehören in Objekt-Storage (S3) oder ein RWX-Volume Novaris: Policen-Dokumente wandern nach S3, nicht in den Pod readOnlyRootFilesystem + allowPrivilegeEscalation: false gehören zusammen in den securityContext passt zur SCC-Vorgabe restricted-v2 (A7) und macht ein kompromittiertes Image weniger nützlich Merksatz: Image read-only, beschreibbare Pfade als emptyDir, Logs nach stdout, echte Daten in eine PVC Vorgehen: ohne Mounts starten, die Read-only file system-Fehler sammeln, genau diese Pfade als emptyDir nachrüsten Novaris: WAS schreibt an vier feste Pfade – vier emptyDir-Mounts, Policen-Dokumente in Objekt-Storage OCP-Unterschied: keiner – readOnlyRootFilesystem und emptyDir sind reines Kubernetes.
Das Image bleibt unangetastet. readOnlyRootFilesystem: true sperrt jede Schreiboperation in den Image-Layern – und an jeden Pfad, den die App wirklich beschreiben muss, mountest du ein emptyDir. Bei Novaris sind das die vier festen WAS-Verzeichnisse (logs, temp, wstemp, /tmp). Logs stellst du auf stdout um, dann sammelt sie der Cluster (G2). Alles, was einen Neustart überleben muss, gehört in eine PVC (G3) oder einen externen Store – nie in ein emptyDir.
  • Legacy-Apps schreiben mitten ins Image: /tmp, Log-Verzeichnisse, App-Server-Arbeits- und Temp-Ordner, Upload-Ordner
  • Best Practice: securityContext.readOnlyRootFilesystem: true – nichts in den Image-Layern ist mehr veränderbar
  • an jeden Pfad, der beschreibbar sein muss, ein emptyDir-Volume mounten
  • Novaris (WAS): vier feste Pfade – .../logs, .../temp, wstemp, /tmp – also vier emptyDir-Mounts
  • emptyDir ist pro Pod und weg beim Neustart – für Temp und Logs in Ordnung, weil Logs sowieso nach stdout gehen
  • emptyDir.medium: Memory gibt ein tmpfs-/tmp – schnell, aber es zählt gegen das Memory-Limit
  • sizeLimit auf dem emptyDir deckelt den Verbrauch, damit ein volllaufender Ordner nicht die Nodes-Disk killt
  • Vorgehen: erst ohne Mounts starten, die Read-only file system-Fehler sammeln, genau diese Pfade nachrüsten
  • Falle: Upload-Ordner als emptyDir – die Datei landet auf Pod A, der Download geht an Pod B
  • Uploads gehören in Objekt-Storage (S3) oder ein RWX-Volume (G3) – Novaris: Policen-Dokumente wandern nach S3
  • Logs statt Log-Dateien: den App-Server auf stdout/stderr umstellen, dann sammelt Vector sie (G2)
  • readOnlyRootFilesystem gehört zusammen mit allowPrivilegeEscalation: false in den securityContext – passt zu restricted-v2 (A7)
  • OCP-Unterschied: keinerreadOnlyRootFilesystem und emptyDir sind reines Kubernetes
  • von Hand durchspielen: legacy-modernisierung-openshift.html – den Read-only file system-Fehler live sammeln, dann gezielt ein emptyDir nachrüsten

H3 Session-State – Sticky Sessions am Router oder raus nach Redis

LegacySession-StateSticky SessionsRedis

Die alte Novaris-App hält den Warenkorb und den Login im JVM-Heap. Mit einer Replica ging das gut. Mit drei Replicas landet der zweite Request bei einem anderen Pod, der die Session nicht kennt – der Nutzer ist plötzlich ausgeloggt. Zwei Auswege: Sticky Sessions am Router (schnell, aber ohne echte Ausfallsicherheit) oder den Zustand raus aus dem Heap in Redis.

Session im JVM-Heap + mehrere Replicas = ausgeloggt beim nächsten Klick der kaputte Zustand – In-Heap-Session Pod A Pod B Pod C Request 1 (Login) legt Session an Request 2 (Warenkorb) Round-Robin → Pod B kennt die Session nicht Ergebnis: HTTP 302 auf die Login-Seite, Warenkorb leer Workaround: Sticky Sessions am Router (B4) metadata.annotations: haproxy.router.openshift.io/balance: source haproxy.router.openshift.io/disable-cookies: "false" der Router pinnt einen Client per Cookie oder Quell-IP an denselben Pod stirbt der Pod → Session weg. Rollout → alle Nutzer wandern um kein echtes HA, keine gleichmäßige Last, kein Skalieren nach unten ohne Schmerz legitim als Zwischenschritt beim Lift-and-Shift – nicht als Ziel bei WebSockets / langen Verbindungen ist Sticky ohnehin fast Pflicht der echte Fix: Zustand raus aus dem Heap Redis externer Session-Store TTL = Session- Timeout Pod A Pod B Pod C Spring: spring-session-data-redis – eine Dependency, kein App-Code WAS: Session-Replikation auf externen Store umstellen, jeder Pod austauschbar für reine APIs: gar keine Session der Client schickt ein signiertes JWT im Authorization: Bearer-Header der Server prüft die Signatur und hält nichts – komplett zustandslos skaliert linear, kein Store, kein Sticky – dafür: Token-Widerruf ist schwer kurze Laufzeit + Refresh-Token, sensible Daten nie in die Payload passt für die Novaris-REST-API, nicht für das server-gerenderte Web-Frontend OAuth2 / OIDC (Keycloak) liefert die Tokens, der Router macht nur TLS Merksatz: ein Pod darf nichts wissen, was ein anderer Pod nicht auch herausfinden kann Novaris: Web-Frontend → Spring Session nach Redis, REST-API → JWT, Sticky nur als Brücke während der Migration OCP-Unterschied: die Router-Annotation ist OpenShift-spezifisch – auf Ingress-NGINX heißt sie nginx.ingress.kubernetes.io/affinity
In-Heap-Sessions und Skalierung schließen sich aus. Sobald mehr als eine Replica läuft, trifft der nächste Request statistisch einen anderen Pod. haproxy.router.openshift.io/balance: source pinnt einen Client per Cookie oder Quell-IP an denselben Pod – ein gültiger Zwischenschritt, aber ohne Ausfallsicherheit. Der echte Fix ist ein externer Store: spring-session-data-redis fürs Web-Frontend, ein signiertes JWT für die REST-API. Danach ist jeder Pod austauschbar und ein Neustart kostet keine Session.
  • Die alte Novaris-App hält Login und Warenkorb im JVM-Heap – mit einer Replica unauffällig
  • mit mehreren Replicas geht der zweite Request per Round-Robin an einen anderen Pod, der die Session nicht kennt
  • Symptom: der Nutzer wird mitten im Ablauf ausgeloggt, der Warenkorb ist leer, HTTP 302 auf die Login-Seite
  • Workaround Sticky Sessions: Route-Annotation haproxy.router.openshift.io/balance: source
  • der Router pinnt dann einen Client per Cookie oder Quell-IP an denselben Pod
  • Sticky hat keine Ausfallsicherheit: stirbt der Pod, ist die Session weg – und beim Rollout wandern alle Nutzer um
  • Sticky verhindert außerdem gleichmäßige Last und sauberes Herunterskalieren – nur als Brücke, nicht als Ziel
  • echter Fix: Zustand raus aus dem Heap in einen externen Store – spring-session-data-redis, eine Dependency ohne App-Code
  • bei WAS: die Session-Replikation auf einen externen Store umstellen, danach ist jeder Pod austauschbar
  • die Redis-TTL entspricht dem Session-Timeout – abgelaufene Sessions räumt Redis selbst weg
  • für reine APIs: gar keine Session – ein signiertes JWT im Authorization-Header, der Server hält nichts
  • JWT-Preis: Widerruf ist schwer – kurze Laufzeit plus Refresh-Token, keine sensiblen Daten in der Payload
  • Novaris-Entscheidung: Web-Frontend nach Redis, REST-API auf JWT, Sticky nur während der Migration (H6)
  • OCP-Unterschied: die balance-Annotation ist OpenShift-Router-spezifisch – auf Ingress-NGINX ist es affinity
  • von Hand durchspielen: legacy-modernisierung-openshift.html – Pinning per Cookie live mit und ohne Sticky-Annotation vergleichen

H4 EJB-Timer & Singletons – in einer Welt mit mehreren Replicas

LegacyEJB-TimerSingletonsReplicas

Die alte Novaris-App hat einen EJB-Timer, der jede Nacht die Prämien neu berechnet, und einen @Singleton-Bean als Cache. Beides ging davon aus, dass es genau eine JVM gibt. Mit drei Replicas feuert der Timer dreimal und jeder Pod hat seinen eigenen, abweichenden Cache. Die saubere Lösung ist fast immer: den Timer aus der App holen und zum CronJob machen.

Ein Timer in der App + N Replicas = der Job läuft N-mal das Problem @Schedule(hour="2") in einem EJB – in jeder JVM aktiv Pod A · Timer feuert 02:00 Pod B · Timer feuert 02:00 Pod C · Timer feuert 02:00 Prämienlauf 3× doppelte Buchungen und der @Singleton-Cache "Singleton" gilt nur pro JVM – jeder Pod hat seinen eigenen Pod A kennt den neuen Tarif, Pod B noch den alten welche Antwort du bekommst, hängt am Zufall des Load-Balancers dasselbe gilt für @Startup-Init, In-Memory-Locks, lokale Zaehler Faustregel: alles, was "genau einmal" sein soll, gehört nach außen vier Lösungen – von sauber nach pragmatisch 1 · CronJob – der empfohlene Weg der Timer wird ein eigenes CronJob-Objekt (F4): schedule: "0 2 * * *", concurrencyPolicy: Forbid gleiches Image, anderer Entrypoint – läuft genau einmal, unabhängig von den Web-Replicas 2 · Leader-Election die Replicas wählen über die Lease-API einen Leader, nur der führt den Timer aus gut, wenn der Job Zustand aus der laufenden App braucht – mehr Code, echtes Failover 3 · Deployment auf eine Replica replicas: 1 + strategy.type: Recreate – nie zwei JVMs gleichzeitig der einfachste Schnitt, aber kein HA für genau diesen Dienst 4 · verteilter Lock Redis SET key NX PX oder DB SELECT ... FOR UPDATE – der erste Pod nimmt den Lock, die anderen überspringen Timer darf ruhig N-mal wecken, die Arbeit passiert nur einmal – Lock-Expiry nicht vergessen Singleton-Cache: raus nach Redis – dann teilen sich alle Pods denselben Zustand (H3) Merksatz: ein Timer in der App feuert pro JVM – also pro Replica. Was genau einmal passieren soll, gehört aus der App heraus Novaris: nächtliche Prämienberechnung → CronJob, @Singleton-Tarif-Cache → Redis Zwischenschritt bei Zeitdruck: das Batch-Modul auf replicas: 1 setzen, Web-Modul frei skalieren OCP-Unterschied: keiner – CronJob, Lease und Recreate sind Standard-Kubernetes. OpenShift gibt dir nur die Konsole dazu
"Genau einmal" ist keine App-Eigenschaft mehr. Ein @Schedule-EJB läuft in jeder JVM – also in jeder Replica – und ein @Singleton ist nur pro JVM einer. Die sauberste Antwort ist der CronJob: der Timer wird ein eigenes Objekt mit concurrencyPolicy: Forbid, unabhängig von den Web-Pods. Reicht das nicht (der Job braucht App-Zustand), dann Leader-Election über die Lease-API oder ein verteilter Lock. Der Singleton-Cache wandert nach Redis. Bei Novaris wird die nächtliche Prämienberechnung ein CronJob, der Tarif-Cache geht nach Redis.
  • Novaris hat einen EJB-Timer für die nächtliche Prämienberechnung und einen @Singleton-Cache – beide gingen von einer JVM aus
  • ein @Schedule-Timer ist in jeder JVM aktiv – mit drei Replicas feuert er dreimal, es gibt doppelte Buchungen
  • ein @Singleton gilt nur pro JVM – jeder Pod hat seinen eigenen Cache, sie driften auseinander
  • dasselbe Muster trifft @Startup-Init, In-Memory-Locks und lokale Zähler
  • Lösung 1 – CronJob (empfohlen): der Timer wird ein eigenes Objekt, schedule: "0 2 * * *", concurrencyPolicy: Forbid
  • der CronJob nutzt dasselbe Image mit anderem Entrypoint und läuft unabhängig von den Web-Replicas (F4)
  • Lösung 2 – Leader-Election: die Replicas wählen über die Lease-API einen Leader, nur der führt den Timer aus
  • Leader-Election lohnt, wenn der Job Zustand aus der laufenden App braucht – mehr Code, dafür echtes Failover
  • Lösung 3 – eine Replica: replicas: 1 plus strategy.type: Recreate, einfach, aber kein HA für diesen Dienst
  • Lösung 4 – verteilter Lock: Redis SET key NX PX oder DB SELECT ... FOR UPDATE, der erste Pod nimmt ihn, die anderen überspringen
  • beim Lock die Expiry setzen – sonst blockiert ein abgestürzter Pod den Job dauerhaft
  • Singleton-Cache: raus nach Redis, dann teilen sich alle Pods denselben Zustand (H3)
  • Novaris-Zuordnung: Prämienberechnung wird ein CronJob, der @Singleton-Tarif-Cache wandert nach Redis
  • OCP-Unterschied: keinerCronJob, Lease und Recreate sind Standard-Kubernetes
  • von Hand durchspielen: legacy-modernisierung-openshift.html – einen echten CronJob mit concurrencyPolicy: Forbid anlegen und manuell auslösen

H5 JNDI → configDropins/Env – wem die Datenbankverbindung gehört

LegacyJNDIconfigDropinsDatenbank

In der alten Welt legt ein Admin die DataSource NovarisDS in der server.xml oder über die WAS-Konsole an – URL, Nutzer und Passwort stehen im Image oder im Profil. Auf OpenShift gehört die Verbindung der Plattform, nicht dem Image: URL als Env aus einer ConfigMap, Passwort aus einem Secret, in Liberty zusammengeführt über ein configDropins/overrides-Fragment.

Die DB-Verbindung gehört der Plattform, nicht dem Image alte Welt: DataSource im Image Admin legt NovarisDS an: • in der server.xml von Hand, oder • per WAS-Admin-Konsole / wsadmin (I4) <dataSource jndiName="jdbc/NovarisDS"> url=jdbc:db2://prod-db:50000/NOV user=novaris password=S3cr3t! (im Layer) </dataSource> jede Umgebung braucht ihr eigenes Image Passwort im Klartext im Registry-Layer Rotation = neuer Build Config über die Konsole ist nicht reproduzierbar (I4) und geht bei jedem Pod-Neustart verloren der db2jcc.jar-Treiber dagegen ist Code – der darf ins Image OpenShift-Form: Verbindung von außen ConfigMap db-config DB_URL: jdbc:db2://... DB_USER: novaris HIKARI_MAX: "20" nicht-geheime Werte, pro Umgebung anders Secret db-credentials DB_PASSWORD: **** als Env oder als Datei gemountet (B8) Rotation ohne Rebuild, Datei-Mount aktualisiert live Liberty configDropins-Fragment .../configDropins/overrides/datasource.xml <dataSource jndiName="jdbc/NovarisDS"> <properties url="${env.DB_URL}" user="${env.DB_USER}" password="${env.DB_PASSWORD}"/> kommt selbst aus einer ConfigMap, Liberty liest es beim Start kein Neubau des Images, kein Eingriff in die Basis-server.xml das Image (umgebungs-agnostisch) kennt nur jdbc/NovarisDS + den JDBC-Treiber-Jar. Sonst nichts über die DB App-Code ändert sich nicht – lookup("jdbc/NovarisDS") bleibt, wie es war Spring-Boot-Variante: SPRING_DATASOURCE_URL / _PASSWORD als Env – dieselbe Idee ohne XML HikariCP-Pool-Größen (max, timeout) sind auch Konfiguration – ab in die ConfigMap DB-TLS-Zertifikat als weiteres Secret, per Datei gemountet und in der URL referenziert Merksatz: der Treiber ist Code und darf ins Image – URL, Nutzer und Passwort sind Konfiguration und gehören nach außen Novaris: NovarisDS raus aus der server.xml, db-credentials als Secret, Pool-Werte als ConfigMap – ein Image für Dev, Test und Prod Datei-Mount statt Env fürs Passwort: es taucht nicht in oc set env --list oder Crash-Dumps auf (B8) OCP-Unterschied: keiner – ConfigMap, Secret und configDropins sind Kubernetes plus Liberty-Standard
Der Treiber ist Code, die Verbindung ist Konfiguration. Der db2jcc.jar darf im Image bleiben. URL, Nutzer und Passwort gehören nach außen: die URL in eine ConfigMap, das Passwort in ein Secret, in Liberty zusammengeführt über ein configDropins/overrides/datasource.xml-Fragment mit ${env.DB_URL}. Der App-Code ruft weiterhin lookup("jdbc/NovarisDS") – er merkt nichts. Ergebnis: ein Image für alle Umgebungen, Passwort-Rotation ohne Rebuild.
  • alte Welt: ein Admin legt NovarisDS in der server.xml oder über die WAS-Konsole an – URL, Nutzer, Passwort im Image oder Profil
  • Folgen: jede Umgebung braucht ein eigenes Image, das Passwort liegt im Klartext im Registry-Layer, Rotation heißt Rebuild
  • Konfiguration über die Konsole ist nicht reproduzierbar und geht bei jedem Pod-Neustart verloren (I4)
  • OpenShift-Form: das Image ist umgebungs-agnostisch und kennt nur den JNDI-Namen jdbc/NovarisDS plus den Treiber-Jar
  • die URL und der Nutzer kommen aus einer ConfigMap db-config als Environment-Variablen (B8)
  • das Passwort kommt aus einem Secret db-credentials – als Env oder besser als Datei gemountet
  • in Liberty legt ein configDropins/overrides/datasource.xml-Fragment die DataSource an und referenziert ${env.DB_URL} und ${env.DB_PASSWORD}
  • das Fragment kommt selbst aus einer ConfigMap und wird nach .../configDropins/overrides gemountet – Liberty liest es beim Start automatisch
  • der App-Code ändert sich nichtlookup("jdbc/NovarisDS") bleibt unverändert
  • Spring-Boot-Variante: SPRING_DATASOURCE_URL und _PASSWORD als Env – dieselbe Idee ohne XML
  • die HikariCP-Pool-Größen (max, timeout) sind auch Konfiguration und gehören in die ConfigMap
  • Datei-Mount statt Env fürs Passwort: es taucht nicht in oc set env --list oder Crash-Dumps auf (B8)
  • Novaris: NovarisDS raus aus der server.xml, db-credentials als Secret, Pool-Werte als ConfigMap – ein Image für Dev, Test und Prod
  • OCP-Unterschied: keiner – ConfigMap, Secret und configDropins sind Kubernetes plus Liberty-Standard
  • von Hand durchspielen: legacy-modernisierung-openshift.html – eine Datenquelle per ConfigMap/Secret umkonfigurieren, ohne das Image neu zu bauen

H6 Vorher/Nachher – legacy-parent auf WAS traditional vs modern-parent auf Liberty/OCP

LegacyVorher / NachherWASLiberty

Die Zusammenfassung von Block H als eine Tafel: links die alte Novaris-Landschaft auf WebSphere Application Server traditional, rechts dieselbe Anwendung auf OpenShift mit Open Liberty. Jede Zeile ist eine Design-Entscheidung, die sich mit der Plattform ändert – und am Ende drei Dinge, die auch nach der Migration schwer bleiben.

Dieselbe Anwendung, zwei Plattformen – Zeile für Zeile legacy-parent · WAS traditional modern-parent · Open Liberty auf OpenShift Topologie DMgr + Node Agents + n App Server in einer Zelle, von Hand aufgesetzt ein Deployment, N austauschbare Pods, vom Scheduler platziert Skalierung vertikal – mehr Heap und CPU für denselben JVM-Prozess horizontal – mehr Replicas, HPA nach CPU oder Requests/s (F2) Konfiguration Admin-Konsole und wsadmin gegen den laufenden Server (I4) Deployment + ConfigMap + Secret in Git, per GitOps ausgerollt (E3) Artefakt eine .ear wird in die laufende Zelle deployt ein Container-Image, das komplett ersetzt wird (C1) Session WAS-Session-Replikation zwischen den App-Servern zustandslose Pods + Redis oder JWT (H3) Timer @Schedule-EJB im App-Server CronJob oder Leader-Election (H4) Datenquelle JNDI in der server.xml oder Konsole, Passwort im Profil Env aus ConfigMap + Secret, configDropins (H5) Dateisystem schreibt frei ins Profil und nach /tmp readOnlyRootFilesystem + emptyDir (H2) Identität läuft als root oder als fester Betriebsnutzer zufällige hohe UID unter restricted-v2 (A7, H1) Routing Web-Server-Plugin + IBM HTTP Server davor Route + Router, TLS am Edge (B4) Neustart Server-Neustart in Minuten, Zustand im Heap Pod-Restart in Sekunden, Zustand außen Image-Größe Installation im GB-Bereich, selten neu gebaut Liberty-Image 200–400 MB, bei jedem Commit neu (I3) was auch nach der Migration schwer bleibt Verteilte XA-Transaktionen – Zwei-Phasen-Commit über mehrere Ressourcen. In Pods ist der Transaction-Recovery-Log schwer sicher abzulegen Echte stateful Singletons – Logik, die genau eine Instanz mit strikter Reihenfolge braucht. Leader-Election hilft, löst es aber nicht völlig Langlebige In-Memory-JVM-Caches – die auf einen warmen Heap angewiesen sind. Ein Pod-Neustall ist billig, das Neu-Füllen des Caches nicht Merksatz: die Migration macht das Deployment einfach – die schweren Fälle sind Architektur-Entscheidungen, keine Plattform-Themen So liest man die Tafel: jede Zeile links ist eine Annahme der alten Welt, jede Zeile rechts die OpenShift-Antwort aus Block H Reihenfolge der Migration: erst Identität und Dateisystem (H1, H2), dann Session und Timer (H3, H4), dann Konfiguration (H5), zuletzt GitOps (E3) OCP-Unterschied: die rechte Spalte ist zu 90% reines Kubernetes – OpenShift liefert Route, SCC, die Konsole und den integrierten Build (C1)
Block H auf einer Tafel. Links legacy-parent auf WAS traditional – Deployment Manager, Node Agents, vertikale Skalierung, Konsolen-Konfiguration, EAR-Deploy, Session-Replikation. Rechts modern-parent auf Liberty/OpenShift – Deployment, horizontale Skalierung mit HPA, GitOps, Image-Replace, zustandslose Pods mit Redis. Jede Zeile verweist auf das Diagramm in Block H, das sie im Detail behandelt. Der Kasten unten nennt drei Fälle, die Architektur bleiben und sich nicht wegmigrieren lassen: verteilte XA-Transaktionen, echte stateful Singletons, warme JVM-Caches.
  • Topologie: DMgr + Node Agents + n App Server in einer Zelle → ein Deployment mit N austauschbaren Pods
  • Skalierung: vertikal (mehr Heap/CPU pro JVM) → horizontal (mehr Replicas, HPA nach CPU oder Requests/s)
  • Konfiguration: Admin-Konsole und wsadmin gegen den laufenden Server (I4) → Deployment + ConfigMap + Secret in Git, per GitOps (E3)
  • Artefakt: eine .ear ins laufende System deployen → ein Container-Image komplett ersetzen (C1)
  • Session: WAS-Session-Replikation zwischen App-Servern → zustandslose Pods plus Redis oder JWT (H3)
  • Timer: @Schedule-EJB im App-Server → CronJob oder Leader-Election (H4)
  • Datenquelle: JNDI in server.xml oder Konsole → Env aus ConfigMap und Secret plus configDropins (H5)
  • Dateisystem: schreibt frei ins Profil → readOnlyRootFilesystem plus emptyDir (H2)
  • Identität: läuft als root oder festem Betriebsnutzer → zufällige hohe UID unter restricted-v2 (A7, H1)
  • Routing: Web-Server-Plugin plus IBM HTTP Server → Route und Router mit TLS am Edge (B4)
  • Neustart: Server-Neustart in Minuten mit Zustand im Heap → Pod-Restart in Sekunden mit Zustand außen
  • was schwer bleibt: verteilte XA-/Zwei-Phasen-Transaktionen – der Recovery-Log ist in Pods schwer sicher abzulegen
  • was schwer bleibt: echte stateful Singletons mit strikter Ordnung – Leader-Election hilft, löst es aber nicht völlig
  • was schwer bleibt: langlebige In-Memory-JVM-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
  • von Hand durchspielen: legacy-modernisierung-openshift.html – alle sechs Bruchstellen der Reihe nach live durchspielen

Block I – kommerzielle Middleware (Concordia/WAS)

I1 Das Lizenz-Gate im Dockerfile – warum der Container ohne Zustimmung nicht startet

MiddlewareLizenz-GateDockerfile

Kommerzielle IBM-Images (WAS, MQ, Db2) haben im Entrypoint eine Lizenz-Schranke: ohne LICENSE=accept gibt der Container die Lizenzbedingungen aus und beendet sich mit einem Fehlercode. Auf OpenShift sieht man das als CrashLoopBackOff, in den Logs steht der Lizenztext. Der Fix ist eine einzige Environment-Variable – die Frage dahinter ist, wer die Zustimmung verantwortet.

Der Entrypoint prüft LICENSE, bevor irgendetwas anderes passiert Entscheidungsbaum im Entrypoint Wert von $LICENSE ? accept App-Server läuft an view druckt Bedingungen Exit 0 leer / anderes "must accept" Exit ≠ 0 der Check läuft vor jedem Server-Start – nichts wird gestartet gilt genauso für IBM MQ (LICENSE), Db2 (LICENSE), viele kommerzielle Images manche Images nutzen zusätzlich ACCEPT_EULA=Y (MS SQL) – gleiche Idee der Exit-Code ≠ 0 ist Absicht: er zwingt eine bewusste Antwort LICENSE=view lokal ausführen, um die Bedingungen zu lesen der Text nennt die IBM-Lizenz-ID (L-Nummer) und eine URL Symptom in OpenShift Pod-Status: CrashLoopBackOff $ oc logs novaris-was-xxent You must accept the license terms to use this program. Set LICENSE=accept (container exited with code 1) restartCount steigt, die Readiness-Probe wird nie grün oc get events zeigt BackOff ohne inhaltlichen Grund typischer Irrweg: man sucht einen Konfig-, Port- oder Speicherfehler die eine Log-Zeile ist die ganze Diagnose – immer zuerst oc logs tritt oft erst in Stufe 2 auf: lokal war -e LICENSE=accept im docker run-Skript und wurde beim Übersetzen ins Deployment vergessen grenzt sich ab von ImagePullBackOff (I2) – da startet das Image nicht und von OOMKilled – da steht der Speicher als Reason der Fix spec.template.spec.containers[0].env: - name: LICENSE value: accept alternativ: Build-Arg --build-arg LICENSE=accept beim abgeleiteten Image oder eine Datei, die der Entrypoint liest (/opt/ibm/.../license) oc set env deploy/novaris-was LICENSE=accept für den schnellen Test danach: Pod startet, Readiness wird grün der eigentliche Punkt die Zustimmung ist rechtlich bindend – jemand muss sie verantworten nicht der Entwickler nebenbei – Betrieb, Einkauf oder Recht die Bedingungen und die Lizenz-URL gehören ins Repo, neben das Deployment LICENSE=accept per Default in ein Basis-Image zu backen nimmt dem Betreiber die bewusste Entscheidung – besser explizit im Deployment der Wert steht sichtbar im Merge-Request und ist im Audit nachvollziehbar Merksatz: CrashLoopBackOff bei einem kommerziellen Image – zuerst oc logs, dann nach LICENSE schauen Concordia: das WAS-Image von IBM – LICENSE=accept steht im Deployment und ist im Merge-Request dokumentiert verwandt, aber anders: MQ und Db2 nutzen dasselbe LICENSE, MS SQL nutzt ACCEPT_EULA=Y, UBI-Images brauchen nichts davon OCP-Unterschied: keiner am Mechanismus – OpenShift zeigt den Crash nur klarer in Konsole und Events an
Ein Container, der läuft und sich sofort beendet. Kommerzielle IBM-Images prüfen im Entrypoint LICENSE, bevor der App-Server startet. Ohne LICENSE=accept wird der Lizenztext ausgegeben und der Container beendet sich mit Fehlercode – in Kubernetes ein CrashLoopBackOff, die Diagnose steht als eine Zeile in oc logs. Der Fix ist ein env-Eintrag im Deployment. Die Frage dahinter ist organisatorisch: die Zustimmung ist rechtlich bindend, sie gehört sichtbar ins Repo und muss von jemandem verantwortet werden.
  • kommerzielle IBM-Images (WAS, MQ, Db2) haben im Entrypoint eine Lizenz-Schranke – sie läuft vor jedem Server-Start
  • ohne LICENSE=accept gibt der Container die Lizenzbedingungen aus und beendet sich mit Fehlercode
  • LICENSE=view druckt nur die Bedingungen und beendet mit Code 0 – zum Nachlesen vor der Zustimmung
  • in Kubernetes sieht man das als CrashLoopBackOff mit steigendem restartCount, die Readiness-Probe wird nie grün
  • die ganze Diagnose ist eine Zeile in oc logs: You must accept the license terms ... Set LICENSE=accept
  • typischer Irrweg: man sucht einen Konfig-, Port- oder Speicherfehler statt zuerst in die Logs zu schauen
  • tritt oft erst bei der Übersetzung von docker run -e LICENSE=accept ins Deployment auf – die Variable wird vergessen
  • Fix: im Deployment unter env ein Eintrag name: LICENSE / value: accept
  • alternativ als Build-Argument --build-arg LICENSE=accept beim abgeleiteten Image oder über eine Datei, die der Entrypoint liest
  • abzugrenzen von ImagePullBackOff (I2) – dort startet das Image gar nicht – und von OOMKilled mit eigenem Reason
  • der eigentliche Punkt: die Zustimmung ist rechtlich bindend – Betrieb, Einkauf oder Recht muss sie verantworten, nicht der Entwickler nebenbei
  • die Lizenzbedingungen und die IBM-Lizenz-URL gehören ins Repo, neben das Deployment
  • LICENSE=accept per Default in ein Basis-Image zu backen nimmt dem Betreiber die bewusste Entscheidung – besser explizit im Deployment
  • Concordia: das WAS-Image von IBM – LICENSE=accept steht im Deployment und ist im Merge-Request dokumentiert
  • verwandt: MQ und Db2 nutzen dasselbe LICENSE, MS SQL nutzt ACCEPT_EULA=Y, UBI-Images brauchen nichts davon
  • von Hand durchspielen: middleware-concordia-openshift.html – die Lizenz-Schranke live provozieren und mit einer Env-Variable fixen

I2 Entitled builds – Pull aus registry.redhat.io, das Pull-Secret

Middlewareentitled buildsregistry.redhat.ioPull-Secret

Ein Build, der volle RHEL-Pakete braucht (nicht nur UBI), muss sich bei Red Hat ausweisen. Das Basis-Image aus registry.redhat.io braucht ein Pull-Secret mit Zugangsdaten von console.redhat.com, und ein dnf install aus den vollen RHEL-Repos braucht ein Entitlement-Zertifikat unter /etc/pki/entitlement. UBI-Images brauchen keins von beidem.

Braucht der Build nur UBI oder volle RHEL-Pakete? Was zieht der Build in FROM und dnf install? UBI – Universal Base Image Images unter registry.access.redhat.com/ubi9/... sind frei ziehbar – kein Login die dnf-Repos ubi, ubi-baseos, ubi-appstream brauchen keine Zugangsdaten deckt die meisten Java-, Python- und Node-Workloads ab kein Pull-Secret, kein Entitlement, keine Subskription der Standardfall – wenn UBI reicht, nimm UBI ubi-minimal nutzt microdnf und ist ~40 MB – guter Runtime-Layer (C3, I3) Red Hat baut die eigenen Sprach-Images (ubi9/openjdk-17) darauf auf Lizenz erlaubt Weitergabe der fertigen Images ohne RHEL-Abo Grenze: Pakete, die nur im vollen BaseOS oder in CodeReady liegen volle RHEL-Pakete – zwei getrennte Dinge 1 · Pull-Secret für registry.redhat.io das Cluster-Pull-Secret openshift-config/pull-secret braucht einen Eintrag für registry.redhat.io mit einem Service-Account-Token von console.redhat.com fehlt der Eintrag → schon der FROM-Pull scheitert: unauthorized: authentication required 2 · Entitlement für dnf der Cluster stellt die Zertifikate als etc-pki-entitlement in openshift-config-managed bereit die BuildConfig mountet sie nach /etc/pki/entitlement in den Build-Container dann findet dnf die vollen RHEL-Repos (rhel-9-for-x86_64-baseos-rpms) fehlt der Mount → der dnf install-Schritt scheitert: Repository '...-baseos-rpms' is not available Symptom → Ursache unauthorized: authentication required beim FROM → Pull-Secret-Eintrag für registry.redhat.io fehlt (B10) Repository ... is not available / Unable to find a match beim dnf → Entitlement-Mount fehlt manifest unknown → Tag falsch geschrieben, nicht Auth  ·  no space left on device → Node-Disk, gehört nicht hierher (I3) Concordia: das WAS-Liberty-Image liegt in icr.io mit eigenem Entitlement-Key – eigener Pull-Secret-Eintrag, die eigenen Layer bleiben auf UBI OCP-Unterschied: der Entitlement-Weg ist OpenShift-spezifisch – auf reinem Kubernetes legt man die Zertifikate selbst als Secret an
Zwei Ausweise, nicht einer. Das Basis-Image aus registry.redhat.io braucht einen registry.redhat.io-Eintrag im Cluster-Pull-Secret (Token von console.redhat.com) – sonst scheitert schon der FROM-Pull mit unauthorized. Ein dnf install aus den vollen RHEL-Repos braucht zusätzlich das Entitlement-Zertifikat unter /etc/pki/entitlement, das die BuildConfig in den Build-Container mountet – sonst Repository ... is not available. UBI-Images brauchen keins von beidem. Bei Concordia liegt das WAS-Image in icr.io mit eigenem Key, die eigenen Layer bleiben auf UBI.
  • ein Build, der nur UBI-Inhalte braucht, kommt ohne alles aus – Images unter registry.access.redhat.com sind frei ziehbar
  • die dnf-Repos ubi, ubi-baseos, ubi-appstream brauchen keine Zugangsdaten – das deckt die meisten Java-/Python-Workloads ab
  • wenn UBI reicht, nimm UBIubi-minimal mit microdnf ist ~40 MB und ein guter Runtime-Layer (C3, I3)
  • volle RHEL-Pakete brauchen zwei getrennte Dinge: ein Pull-Secret und ein Entitlement
  • 1 · Pull-Secret: das Cluster-Secret openshift-config/pull-secret braucht einen Eintrag für registry.redhat.io
  • die Zugangsdaten sind ein Service-Account-Token von console.redhat.com – fehlt der Eintrag, scheitert schon der FROM-Pull
  • 2 · Entitlement: der Cluster stellt die Zertifikate als etc-pki-entitlement in openshift-config-managed bereit
  • die BuildConfig mountet sie nach /etc/pki/entitlementdann findet dnf die vollen RHEL-Repos
  • Symptom unauthorized: authentication required beim FROM → Pull-Secret-Eintrag für registry.redhat.io fehlt (B10)
  • Symptom Repository ... is not available oder Unable to find a match beim dnf-Schritt → Entitlement-Mount fehlt
  • abzugrenzen: manifest unknown ist ein falscher Tag, nicht Auth – no space left ist Node-Disk (I3)
  • Concordia: das WAS-Liberty-Image liegt in icr.io mit eigenem Entitlement-Key – eigener Pull-Secret-Eintrag
  • die eigenen Build-Layer von Concordia bleiben auf UBI und brauchen kein RHEL-Entitlement
  • OCP-Unterschied: der Entitlement-Weg ist OpenShift-spezifisch – auf reinem Kubernetes legt man die Zertifikate selbst als Secret an
  • von Hand durchspielen: middleware-concordia-openshift.html – das echte Pull-Secret und Entitlement-Secret im Cluster nachsehen

I3 Riesen-Base-Images – Layer-Caching, warum das WAS-Image 2 GB ist

MiddlewareBase-ImagesLayer-Caching

Ein WAS-traditional-Image ist 1,5 bis 2,5 GB. Open Liberty liegt bei 200 bis 400 MB, ein Spring-Boot-Fat-Jar auf UBI bei etwa 150 MB. Die Größe kostet an vier Stellen: langsamer Pull, Disk-Druck auf den Nodes, jeder Rollout schiebt Gigabytes pro Node, die Registry läuft voll. Der größte Hebel ist die Layer-Reihenfolge – das Stabile nach unten, den Code nach oben.

Größe kostet vierfach – und die Layer-Reihenfolge ist der größte Hebel Größenvergleich WAS traditional   1,5–2,5 GB WAS Liberty (voll)   400–700 MB Open Liberty (gezielte Features)   200–400 MB Spring Boot Fat Jar / ubi9-openjdk   ~150 MB ubi-minimal + JRE + App   ~40 MB Basis die 2 GB sind fast alle App-Server – die eigene App sind meist < 30 MB komprimiert in der Registry etwas kleiner, entpackt auf der Node voll oc image info / skopeo inspect zeigt die Layer-Größen die vier Folgen 1 Pull – neuer Node oder Cache-Miss zieht das ganze Image, bevor der Pod startet verlängert Rollout und Scale-out spürbar – ein 2-GB-Pull dauert Minuten 2 Disk-Druck – viele große Images füllen den Image-Store der Node ab der Schwelle: Kubelet-GC, im Extremfall Evicted wegen DiskPressure 3 Rollout-Kosten – jede Version schiebt die geänderten Layer an jeden Ziel-Node bei einem 2-GB-Monolith-Layer ist das jedes Mal fast das ganze Image 4 Registry füllt sich – jeder Build ein neues Manifest ohne Aufräum-Policy wächst der Storage-Bedarf linear mit der Build-Zahl (C5) alle vier verschwinden nicht durch mehr Hardware – nur durch kleinere Images Messgröße: Zeit von Scheduled bis Running bei einem kalten Node Layer-Reihenfolge: Stabiles nach unten, Code nach oben WAR / Fat Jar – jeder Commit Abhängigkeiten – selten server.xml / Feature-Liste Liberty-Runtime + JDK Basis-OS (UBI) ein Code-Commit trifft nur den obersten kleinen Layer alle darunter bleiben im Build- und im Node-Cache Dependencies vor dem App-Code kopieren (pom.xml vor src) so wird der 200-MB-Runtime-Layer einmal gezogen und nie wieder imagePullPolicy: IfNotPresent lässt den Node-Cache wirken (statt Always) unveränderliche Tags (Digest / :git-sha) machen IfNotPresent sicher Gegenmaßnahmen • Layer-Reihenfolge fixen (der billigste Gewinn) • auf Open Liberty statt WAS traditional wechseln • in Liberty nur genutzte Features aktivieren statt javaee-8 komplett • chained build: ubi-minimal-Runtime-Stage mit nur JRE + App (C3) • kritische Nodes vor-pullen (DaemonSet oder Node-Image-Preload) • alte Tags mit skopeo delete / Registry-Prune räumen (C5) .dockerignore – keine Build-Caches oder .git ins Image Merksatz: die App sind 30 MB – der Rest ist der App-Server. Kleiner wird es nur durch einen kleineren Server oder eine schlanke Runtime-Stage Concordia: Liberty-Image, Feature-Liste auf jaxrs-2.1, cdi-2.0, jpa-2.2 reduziert, die App-WAR ist der oberste Layer und wenige MB OCP-Unterschied: keiner an den Mechanismen – die integrierte Registry und der Image-Pruner sind OpenShift-Komfort (C5)
Zwei Gigabyte, von denen 30 Megabyte deine App sind. Der Rest ist der Application Server. Die Größe kostet an vier Stellen: langsamer Pull vor jedem Pod-Start, DiskPressure-Eviction auf vollen Nodes, jeder Rollout schiebt fast das ganze Image an jeden Ziel-Node, die Registry wächst mit jeder Build-Nummer. Der größte Hebel ist die Layer-Reihenfolge: Basis-OS und Runtime nach unten, die WAR nach oben, dann trifft ein Commit nur den obersten kleinen Layer. Danach: Open Liberty statt WAS traditional, gezielte Feature-Liste, eine ubi-minimal-Runtime-Stage (C3), alte Tags räumen.
  • ein WAS-traditional-Image ist 1,5 bis 2,5 GB – Open Liberty 200 bis 400 MB, ein Spring-Boot-Fat-Jar auf UBI etwa 150 MB
  • die eigene App sind meist unter 30 MB – der Rest ist der Application Server
  • Folge 1 – langsamer Pull: ein neuer Node oder Cache-Miss zieht das ganze Image, bevor der Pod startet, das verlängert Rollout und Scale-out
  • Folge 2 – Disk-Druck: viele große Images füllen den Image-Store, ab der Schwelle Kubelet-GC und im Extremfall Evicted wegen DiskPressure
  • Folge 3 – Rollout-Kosten: jede Version schiebt die geänderten Layer an jeden Ziel-Node – bei einem Monolith-Layer fast das ganze Image
  • Folge 4 – Registry füllt sich: jeder Build ein neues Manifest, ohne Aufräum-Policy wächst der Storage linear mit der Build-Zahl (C5)
  • größter Hebel – Layer-Reihenfolge: Stabiles (OS, Runtime, JDK) nach unten, den Code (WAR, Fat Jar) nach oben
  • Dependencies vor dem App-Code kopieren (pom.xml zuerst, dann mvn, dann src) – dann trifft ein Commit nur den obersten Layer
  • imagePullPolicy: IfNotPresent statt Always lässt den Node-Cache wirken – sicher mit unveränderlichen Tags (Digest oder :git-sha)
  • auf Open Liberty wechseln und in Liberty nur die genutzten Features aktivieren statt javaee-8 komplett
  • chained build: eine ubi-minimal-Runtime-Stage mit nur JRE plus App – die Build-Werkzeuge bleiben draußen (C3)
  • kritische Nodes vor-pullen (DaemonSet oder Node-Image-Preload), damit der erste Pod nicht auf 2 GB wartet
  • alte Tags räumen mit skopeo delete oder dem Registry-Pruner, .dockerignore hält .git und Build-Caches aus dem Image (C5)
  • Concordia: Liberty-Image, Feature-Liste auf jaxrs-2.1, cdi-2.0, jpa-2.2 reduziert, die App-WAR ist der oberste Layer und wenige MB
  • OCP-Unterschied: keiner an den Mechanismen – die integrierte Registry und der Image-Pruner sind OpenShift-Komfort
  • von Hand durchspielen: middleware-concordia-openshift.html – reale Image-Größen gegeneinander vergleichen

I4 Die Admin-Konsole als Route – passthrough-TLS, der wsadmin/Scripting-Weg

MiddlewareAdmin-Konsolepassthrough-TLSwsadmin

Die WAS-Admin-Konsole läuft auf 9043/9443 mit einem selbstsignierten Zertifikat. Man kann sie über eine passthrough-Route erreichbar machen – aber die Konsole schreibt Konfiguration in den laufenden Server, skaliert nicht und ist nicht reproduzierbar. Der richtige Weg ist wsadmin-Scripting zur Build- oder Init-Zeit: Konfiguration als Code.

Konsole = Break-Glass. Konfiguration = Code (wsadmin / server.xml) der passthrough-Weg (nur Notfall-Zugang) Browser Admin Router reicht TLS durch WAS-Konsole 9043/9443, self-signed spec.tls.termination: passthrough spec.port.targetPort: 9443 # kein Pfad-Routing - passthrough kennt nur den Host (SNI) # edge/reencrypt bricht am self-signed Cert der Konsole warum das kein Betriebsmodell ist • schreibt in die Config eines laufenden Servers • bei mehreren Replicas trifft man einen zufälligen Pod, die anderen sehen nichts (H3) • die Änderung ist beim nächsten Pod-Neustart weg – Dateisystem nicht persistent (H2) • keine Historie, kein Review, kein Rollback • zwei Admins, die gleichzeitig speichern, überschreiben sich • die Konsole selbst hält JVM-State – sie skaliert nicht legitim: einmaliger Blick auf Laufzeit-MBeans, Thread-Dump, akute Analyse – mit NetworkPolicy einsperren der richtige Weg: Konfiguration als Code WAS traditional: wsadmin-Skripte Jython (oder Jacl) macht dieselben Config-Änderungen wie die Konsole läuft als RUN-Schritt beim Image-Bau, oder als init-Job vor dem Rollout wsadmin -lang jython -f /scripts/configure-ds.py Ergebnis: jeder frische Pod hat exakt dieselbe Konfiguration Liberty: noch direkter server.xml + configDropins/overrides sind schon die Konfiguration als Code es gibt keine Konsole, die man braucht – Werte kommen aus ConfigMap und Secret (H5) optional das Admin-Center-Feature für Lesezugriff, nie als Änderungsweg was beide gemeinsam haben die Skripte und XML-Dateien liegen im Git-Repo jede Änderung ist ein Merge-Request mit Review, Historie und Rollback (E3) die Umgebung ist reproduzierbar – Dev, Test und Prod aus derselben Quelle Merksatz: was du in der Konsole klickst, weiß nur ein Pod und nur bis zum nächsten Neustart. Skripte im Repo wissen es für immer passthrough braucht man auch für echte Ende-zu-Ende-TLS-Fälle (Client-Zertifikate, mTLS zum Pod) – nicht nur für die Konsole (B4) Concordia: Konsole nur als passthrough-Route mit NetworkPolicy für den Notfall, die Konfiguration ist ein Satz Jython-Skripte im init-Job Liberty-Ziel: server.xml plus configDropins, ganz ohne Konsole OCP-Unterschied: passthrough ist ein OpenShift-Route-Begriff – auf Ingress heißt es ssl-passthrough am NGINX-Controller
Die Konsole ist ein Notausgang, kein Betriebsweg. Erreichbar wird sie nur über eine passthrough-Route – der Router reicht die TLS-Verbindung unverändert an den Pod durch, weil edge am selbstsignierten Konsolen-Zertifikat bricht und passthrough kein Pfad-Routing kann. Aber was du in der Konsole klickst, kennt nur ein Pod und nur bis zum nächsten Neustart (H2). Der richtige Weg ist wsadmin-Scripting zur Build- oder Init-Zeit, in Liberty die server.xml plus configDropins (H5) – im Git-Repo, mit Review und Historie (E3).
  • die WAS-Admin-Konsole läuft auf 9043/9443 mit einem selbstsignierten Zertifikat, das der Server selbst terminiert
  • eine edge- oder reencrypt-Route bricht am self-signed Cert und an der Client-Cert-Erwartung der Konsole
  • also braucht es eine passthrough-Route: termination: passthrough, targetPort: 9443, der Router reicht TLS unverändert durch
  • passthrough kann kein Pfad-Routing – es kennt nur den Host aus der SNI, bevor entschlüsselt wird
  • Nachteil: die Konsole schreibt in die Config eines laufenden Servers – bei mehreren Replicas trifft man einen zufälligen Pod (H3)
  • Nachteil: die Änderung ist beim nächsten Pod-Neustart weg, weil das Dateisystem nicht persistent ist (H2)
  • Nachteil: keine Historie, kein Review, kein Rollback – und zwei Admins, die gleichzeitig speichern, überschreiben sich
  • Nachteil: die Konsole hält selbst JVM-State – sie skaliert nicht und ist nicht reproduzierbar
  • legitim: ein einmaliger Blick auf Laufzeit-MBeans, ein Thread-Dump, akute Analyse – mit NetworkPolicy einsperren
  • richtiger Weg – wsadmin: Jython- oder Jacl-Skripte machen dieselben Config-Änderungen wie die Konsole
  • die Skripte laufen als RUN-Schritt beim Image-Bau oder als init-Job vor dem Rollout – jeder frische Pod ist gleich konfiguriert
  • in Liberty noch direkter: server.xml plus configDropins/overrides sind schon die Konfiguration als Code, Werte aus ConfigMap und Secret (H5)
  • beiden gemeinsam: die Dateien liegen im Git-Repo, jede Änderung ist ein Merge-Request mit Review und Historie (E3)
  • Concordia: Konsole nur als passthrough-Route mit NetworkPolicy für den Notfall, die Konfiguration ist ein Satz Jython-Skripte im init-Job
  • OCP-Unterschied: passthrough ist ein OpenShift-Route-Begriff – auf Ingress-NGINX heißt es ssl-passthrough
  • von Hand durchspielen: middleware-concordia-openshift.html – eine passthrough-Route gegen einen selbstsignierten Testserver live aufbauen

Block J – Frontend auf OpenShift

J1 Eine SPA ausliefern – nginx-Pod vs nodejs-S2I, die Route

FrontendSPAnginxnodejs-S2I

Ein gebautes Angular- oder React-Frontend ist nur ein Ordner statischer Dateien – HTML, JS, CSS, keine Server-Logik. Zwei Wege, sie auszuliefern: ein winziges nginx-Image mit den Assets oder ein nodejs-S2I-Build. Für eine reine SPA gewinnt nginx. Davor eine edge-Route (B4).

Ein gebautes Frontend sind nur statische Dateien – irgendwer muss sie servieren. Build (ausserhalb, CI) ng build --configuration=prod erzeugt dist/: index.html, *.js, *.css keine Serverlogik, keine Umgebungslese Node nur zum Bauen, nicht zur Laufzeit Config kommt erst zur Laufzeit (J4) Weg A – nginx-Container (empfohlen) dist/ nach /usr/share/nginx/html kopieren eigene nginx.conf: try_files $uri /index.html (History-Routing), Port 8080 statt 80 (SCC, A7), gzip, Cache-Header ubi9/nginx-124 / nginxinc/nginx-unprivileged laufen als Nicht-Root Image ~20–40 MB, kein Node, startet in Millisekunden Weg B – nodejs-S2I ubi9/nodejs-20 · assemble: npm ci && npm run build run: ein kleiner Node-Server (serve, http-server) ein Build-Schritt (C3), aber Node läuft zur Laufzeit größer, mehr RAM, nur sinnvoll bei SSR / Next.js Service ClusterIP, port 8080 (B3) Selector auf die nginx-Pods edge-Route ↗ library-frontend.apps.<cluster> TLS endet am Router, dahinter HTTP Browser lädt index.html, dann die JS-Bundles von da an läuft alles im Client API-Aufrufe: J2 · Login: J3 dieselben Assets, anderes Laufzeit-Image die Stolpersteine Port 80 im Image → Permission denied unter restricted-v2 (A7) – auf 8080 kein try_files-Fallback → Deep-Links (/buch/42) geben 404 Standard-nginx-Image will nach /var/cache/nginx schreiben → emptyDir oder unprivileged-Image (H2) Cache-Header: index.html nie cachen, gehashte Bundles lange cachen Bibliothek / cloud-host library-frontend = ng build-Output, serviert von ubi9/nginx auf 8080 gebaut von der CI ausserhalb (E1), als kleines Image nach ghcr.io eine edge-Route, die Backend-URL kommt per env.js (J4) Login gegen Keycloak aus dem Browser (J3) Merksatz: statische SPA → kleines nginx-Image auf 8080 mit try_files-Fallback, davor eine edge-Route nodejs-S2I nur, wenn wirklich ein Node-Server gebraucht wird (SSR, API-Routen) – sonst unnötiger Ballast OCP-Unterschied: keiner – nginx und Node laufen wie überall. OpenShift zwingt nur zum Nicht-Root-Port (A7) und liefert die Route (B4).
Statische Dateien brauchen einen Server. Ein ng build liefert nur dist/ – HTML, JS, CSS. Am besten ein winziges nginx-Image (unter 40 MB) mit den Assets, einer try_files-Regel für das SPA-Routing und Port 8080 statt 80 (SCC, A7). Ein nodejs-S2I-Build lohnt nur, wenn wirklich ein Node-Server zur Laufzeit gebraucht wird. Davor eine edge-Route (B4) auf den Service. Gebaut wird ausserhalb, die Laufzeit-Config kommt separat (J4).
  • ein gebautes Frontend (ng build / npm run build) ist nur ein Ordner statischer Dateien (dist/) – keine Serverlogik, keine Umgebungslese
  • Weg A – nginx-Container: dist/ nach /usr/share/nginx/html, eigene nginx.conf, winziges Image, kein Node zur Laufzeit
  • die nginx.conf braucht try_files $uri /index.html für das SPA-History-Routing – sonst geben Deep-Links 404
  • Port 8080 statt 80 – unter restricted-v2 darf ein Nicht-Root-Container keinen Port unter 1024 binden (A7)
  • nimm ein Nicht-Root-Image: ubi9/nginx-124 oder nginxinc/nginx-unprivileged – das Standard-nginx-Image will nach /var/cache/nginx schreiben (H2)
  • Weg B – nodejs-S2I (ubi9/nodejs-20): assemble baut, run startet einen kleinen Node-Server – schwerer, weil Node zur Laufzeit läuft
  • für eine reine statische SPA gewinnt nginx – nodejs-S2I nur bei echtem Server-Bedarf (SSR, Next.js, API-Routen)
  • die Route ist edge TLS (B4) und zeigt auf den Service (B3) auf Port 8080
  • Cache-Header: index.html nie cachen, die gehashten Bundles (main.a1b2.js) sehr lange – sonst hängen alte Clients an alten Bundles
  • die SPA ausserhalb bauen (CI, E1) und ein kleines Image mit den Assets nach ghcr.io pushen – kein Build-Pod im Cluster (C2)
  • die Laufzeit-Config (Backend-URL, Keycloak-Realm) kommt nicht ins Build – sie wird per env.js zur Laufzeit geladen (J4)
  • Bibliothek: library-frontend ist ein ng build-Output, serviert von ubi9/nginx auf 8080, mit einer edge-Route
  • OCP-Unterschied: keiner – nginx und Node laufen wie überall. OpenShift verlangt nur den Nicht-Root-Port und liefert die Route (B4)
  • von Hand durchspielen: frontend-spa-openshift.html – eine SPA per Nicht-Root-nginx ausliefern, Deep-Links per try_files testen

J2 Browser → Route → api-gateway – eine Route vs pro Service, Pfad-Routing

FrontendRouteapi-gatewayPfad-Routing

Wie erreicht die SPA das Backend? Entweder zwei Hosts (Frontend und API je eine Route) – dann redet der Browser cross-origin und die API braucht CORS. Oder ein Host mit Pfad-Routing (/ zum Frontend, /api zur API) – same-origin, kein CORS, ein Zertifikat. Pfad-Routing ist einfacher, koppelt aber die Deploys.

Zwei Hosts → CORS. Ein Host mit Pfaden → same-origin, kein CORS. Option A – zwei Hosts, zwei Routen Browser lädt SPA von library-frontend.apps SPA ruft die API auf https://api-gateway.apps/… andere Origin → der Browser macht zuerst eine OPTIONS-Preflight-Anfrage → die api-gateway muss CORS-Header senden: Access-Control-Allow-Origin: https://library-frontend.apps Access-Control-Allow-Methods / -Headers / -Credentials fehlt einer → der Browser blockt die Antwort (die App sieht einen Netzwerkfehler) Cookies cross-site: SameSite=None + Secure + Allow-Credentials: true Vorteil: klare Grenzen, unabhängige Deploys · Nachteil: CORS-Pflege, zwei Zertifikate die SPA muss die API-URL kennen → env.js (J4) Option B – ein Host, Pfad-Routing library.apps.<cluster> eine Route je Pfad (edge, spec.path, B4): / → Service library-frontend:8080 /api → Service api-gateway:8080 Browser sieht eine Originkein CORS, kein Preflight ein Zertifikat, Cookies (same-site) funktionieren einfach der Router matcht den längsten Pfad zuerst · ggf. rewrite-target die SPA ruft einfach /api/… (relativ) – keine URL-Config nötig Nachteil: Frontend und API hängen an einem Host und einer Route-Gruppe Alternative: ein Reverse-Proxy im Frontend-Pod (nginx location /api) was CORS wirklich ist eine Browser-Regel: JS darf eine Antwort von einer anderen Origin nur lesen, wenn diese es per Header erlaubt der Server ist nicht „geschützt“ – curl ignoriert CORS. Es schützt den Nutzer vor fremden Seiten Preflight (OPTIONS) bei „nicht-einfachen“ Requests (JSON-Body, Authorization-Header) Wildcard Allow-Origin: * geht nicht zusammen mit Allow-Credentials: true Bibliothek / cloud-host getrennte Hosts: library-frontend.apps + api-gateway.apps CORS auf der api-gateway (Spring @CrossOrigin / WebFlux-Config) die API-Basis-URL kommt per env.js in die SPA (J4) Bearer-Token statt Cookie → CORS ohne credentials (J3) Merksatz: gleicher Host / Pfad-Routing → kein CORS, einfachste Lösung. Getrennte Hosts → CORS-Header auf der API pflegen Bearer-Token (J3) statt Session-Cookie macht CORS deutlich einfacher – kein Allow-Credentials, kein SameSite OCP-Unterschied: keiner – CORS ist Browser + App. OpenShift liefert nur die Route(n) (B4) und das Pfad-Routing über spec.path.
Eine Origin oder zwei. Bekommen Frontend und API je einen Host, redet die SPA cross-origin und die API muss CORS-Header senden (Access-Control-Allow-Origin …) – sonst blockt der Browser die Antwort. Teilen sie sich einen Host mit Pfad-Routing (/ und /api, per spec.path auf der Route, B4), ist alles same-origin: kein CORS, ein Zertifikat, Cookies funktionieren. CORS schützt den Nutzer, nicht den Server – ein Bearer-Token (J3) macht es einfacher.
  • Option A – zwei Hosts: Frontend und API je eine Route – die SPA ruft eine andere Origin, also cross-origin
  • cross-origin → CORS: der Browser macht erst eine OPTIONS-Preflight, die API muss Access-Control-Allow-Origin / -Methods / -Headers senden
  • fehlt ein CORS-Header, blockt der Browser die Antwort – die App sieht nur einen Netzwerkfehler, der Server hat trotzdem geantwortet
  • Option B – ein Host, Pfad-Routing: eine Route je Pfad (edge, spec.path, B4), / zum Frontend-Service, /api zur api-gateway (B3)
  • same-origin → kein CORS, kein Preflight, ein Zertifikat, Cookies funktionieren – die SPA ruft einfach relativ /api/…
  • Abwägung: Pfad-Routing koppelt die zwei Deploys unter einem Host, getrennte Hosts sind sauberere Grenzen (aber CORS-Pflege)
  • CORS ist eine Browser-Regelcurl ignoriert sie. Sie schützt den Nutzer vor fremden Seiten, nicht den Server
  • Wildcard Allow-Origin: * geht nicht zusammen mit Allow-Credentials: true – bei Cookies den Origin explizit setzen
  • Cookies cross-site brauchen SameSite=None mit Secure plus Allow-Credentials: true – ein Bearer-Token (J3) umgeht das ganz
  • Alternative zu beidem: ein Reverse-Proxy im Frontend-Pod (nginx location /api { proxy_pass http://api-gateway:8080; }) – die SPA sieht nur ihre eigene Origin
  • bei getrennten Hosts muss die SPA die API-URL kennen – nicht ins Build backen, sondern per env.js (J4)
  • Bibliothek: getrennte Hosts + CORS auf der api-gateway (Spring @CrossOrigin), Bearer-Token statt Cookie, API-URL per env.js
  • OCP-Unterschied: keiner – CORS ist Browser + App. OpenShift liefert die Route(n) und das Pfad-Routing über spec.path
  • von Hand durchspielen: frontend-spa-openshift.html – den fehlenden CORS-Header am Preflight sehen, dann per Pfad-Routing umgehen

J3 Keycloak-Login aus dem Browser – public client, der Redirect-Flow, PKCE

FrontendKeycloakOIDCPKCE

Die SPA ist ein public OIDC-Client – sie hat kein Client-Secret, denn alles im Browser-JS ist öffentlich. Sie nutzt den Authorization-Code-Flow mit PKCE: eine Kette von Redirects zwischen Browser, Keycloak und SPA, an deren Ende ein access_token (JWT) steht. Die api-gateway prüft nur noch die Signatur.

Kein Secret im Browser – PKCE ersetzt es durch ein Einmal-Secret pro Login. SPA im Browser public client, kein Secret erzeugt code_verifier + code_challenge = S256(verifier) state gegen CSRF, nonce Keycloak ↗ eigene Route, der IdP der App (nicht der Cluster-OAuth, A4) Realm bibliothek, Client library-frontend Endpoints: /authorize, /token, /logout, JWKS api-gateway bekommt Authorization: Bearer <jwt> prüft die Signatur gegen Keycloaks JWKS plus exp, aud, iss – keine Keycloak-Anfrage nötig Rollen aus dem Token → RBAC in der App 1 3 4 5 1  Redirect zu /authorize?response_type=code&code_challenge=E9m2XhV_aB3-Qk7p&state=f8a2c91d 2  Browser landet bei Keycloak → Nutzer meldet sich an (Passwort, MFA, Social…) 3  Redirect zurück zur redirect_uri?code=SplxlOBeZQQYbYS6&state=f8a2c91d (nur ein kurzer Code, kein Token) 4  SPA POSTet code + code_verifier an /tokenaccess_token (JWT) + id_token + refresh_token 5  jede API-Anfrage trägt Authorization: Bearer <access_token> was PKCE schützt ohne Secret könnte ein Angreifer den code abfangen und selbst einlösen mit PKCE: /token prüft, dass S256(code_verifier) == code_challenge der verifier hat nie den Browser verlassen → nur diese SPA kann den Code einlösen nie den Implicit-Flow (response_type=token) – abgekündigt, Token in der URL was matchen muss (alles öffentlich → env.js, J4) redirect_uri exakt, im Keycloak-Client registriert (auch die Logout-URL) web_origins im Client → CORS für den /token-Call (J2) realm + client-id + Keycloak-URL → in die SPA per env.js Bibliotheken: keycloak-js, oidc-client-ts, angular-auth-oidc-client Token-Ablage: im Speicher am sichersten, sessionStorage geht – nie localStorage für Tokens (XSS). Refresh per refresh_token oder stillem prompt=none-iframe Merksatz: SPA holt sich per Redirect + PKCE ein JWT, die API prüft nur die Signatur – kein Server-State, kein Session-Cookie OCP-Unterschied: keiner – das ist reines OIDC. Keycloak läuft als App im Cluster (eigene Route, B4), unabhängig vom Cluster-OAuth-Server (A4).
Der Redirect-Tanz. Die SPA hat kein Secret, also erzeugt sie pro Login ein Einmal-Secret (PKCE): code_verifier bleibt im Browser, code_challenge geht mit zum /authorize. Keycloak schickt nach dem Login einen kurzen code zurück, den die SPA zusammen mit dem code_verifier gegen ein access_token (JWT) tauscht. Die api-gateway prüft nur die Signatur gegen Keycloaks JWKS – kein Server-State. redirect_uri, web_origins, Realm und Client-id müssen exakt passen (env.js, J4).
  • die SPA ist ein public OIDC-Clientkein Client-Secret, weil alles im Browser-JS öffentlich ist
  • sie nutzt den Authorization-Code-Flow mit PKCE: code_verifier bleibt im Browser, code_challenge = S256(verifier) geht mit
  • Schritt 1: Redirect zu Keycloaks /authorize mit response_type=code, client_id, redirect_uri, code_challenge, state, nonce
  • Schritt 2: der Nutzer meldet sich bei Keycloak an – Keycloak ist hier der IdP der App (eigene Route, B4), nicht der Cluster-OAuth (A4)
  • Schritt 3: Keycloak leitet zurück zur registrierten redirect_uri mit ?code=SplxlOBeZQQYbYS6&state=f8a2c91d – nur ein kurzer Code, kein Token
  • Schritt 4: die SPA POSTet code + code_verifier an /tokenaccess_token (JWT) + id_token + refresh_token
  • Schritt 5: jede API-Anfrage trägt Authorization: Bearer <access_token>, die api-gateway prüft die Signatur gegen die JWKS (kein Keycloak-Call)
  • PKCE schützt davor, dass ein abgefangener code von jemand anderem eingelöst wird – nur wer den verifier hat, kommt an das Token
  • nie der Implicit-Flow (response_type=token) – abgekündigt, Token landet in der URL-History
  • was matchen muss: redirect_uri exakt registriert, web_origins für CORS des /token-Calls (J2), Realm + Client-id
  • Realm, Client-id und Keycloak-URL sind öffentlich → sie kommen per env.js in die SPA (J4), nicht ins Build
  • Token-Ablage: im Speicher am sichersten, sessionStorage geht, nie localStorage wegen XSS – Refresh per refresh_token oder stillem prompt=none-iframe
  • Logout = Keycloaks /logout aufrufen und die lokalen Tokens löschen
  • OCP-Unterschied: keiner – reines OIDC. Keycloak läuft als App im Cluster, unabhängig vom Cluster-OAuth-Server (A4)
  • von Hand durchspielen: frontend-spa-openshift.html – ein echtes PKCE-Codepaar mit openssl erzeugen und die Authorize-URL bauen

J4 Laufzeit-Config in ein statisches Build – das env.js-Muster, ConfigMap-mount

FrontendLaufzeit-Configenv.jsConfigMap

Eine statische SPA wird einmal gebaut und kann zur Laufzeit keine Environment-Variablen lesen – im Browser gibt es kein process.env. Das env.js-Muster löst das: das Build lädt vor dem App-Bundle ein env.js, das window.__APP_CONFIG__ setzt. Diese Datei ist nicht im Image – sie kommt beim Start aus einer ConfigMap (B8), als Volume-Mount. So läuft dasselbe Image überall mit anderer Config, ohne Rebuild.

Ein Image, viele Umgebungen – die Config kommt erst beim Start dazu. der falsche Weg API-URL und Keycloak-URL hart ins Bundle schreiben (environment.prod.ts) → jede Umgebung braucht ihr eigenes Image ein falscher Wert = neu bauen, Dev und Prod driften auseinander, das getestete Artefakt ist nicht das ausgelieferte Grundregel: build once, deploy everywhere – das Image ist umgebungsneutral 1 Build (kennt keine Config) npm run builddist/ index.html lädt zuerst: <script src="env.js"></script> <script src="main.[hash].js"> im Image liegt nur ein Platzhalter-env.js 2 ein Image ghcr.io/…/library-frontend nginx serviert dist/ (J1) unverändert für Dev, Stage, Prod ein Tag, ein Digest, einmal getestet 3 ConfigMap ↗ (B8) kind: ConfigMap name: frontend-config data: { env.js: | window.__APP_CONFIG__=… } pro Umgebung eine eigene ConfigMap – gleiche Keys, andere Werte 4 Pod Volume-Mount, kein env: mountPath: /usr/share/ nginx/html/env.js subPath: env.js liegt jetzt neben index.html 5 im Browser env.js läuft vor dem App-Bundle und setzt window.__APP_CONFIG__ = { apiUrl, keycloakUrl, realm } die App liest beim Start window.__APP_CONFIG__.apiUrl – kein process.env, keine Build-Konstante dieselben Werte füttern J2 (API-URL) und J3 (Keycloak-URL, Realm, Client-id) nur öffentliche Werte in env.js – es geht ungefiltert an jeden Browser, keine Secrets ConfigMap geändert? ein Volume-Mount wird nicht heiss neu geladen oc rollout restart deployment/library-frontend neue Pods bekommen die neue env.js (B8, C6) Alternative: envsubst-Entrypoint aus Pod-env, oder /config-Sidecar Bibliothek: library-frontend mountet die frontend-config-ConfigMap als env.jsapiUrl auf die api-gateway-Route, keycloakUrl auf die Keycloak-Route Merksatz: das Image ist umgebungsneutral, die ConfigMap macht es umgebungsspezifisch – erst beim Start, per Mount, nicht per Rebuild Server-Frameworks (Next.js SSR, Nuxt) brauchen das nicht – die lesen echte env-Vars im Node-Prozess. Das Muster gilt für rein statische SPAs window.__APP_CONFIG__ = { apiUrl: "https://library.apps.example.com/api", keycloakUrl: "https://sso.apps.example.com", realm: "bibliothek" }
Ein Image, viele Umgebungen. Das Build kennt keine URLs – index.html lädt vor dem App-Bundle ein env.js, das window.__APP_CONFIG__ setzt. Diese Datei ist nicht im Image, sondern kommt aus einer ConfigMap (B8), als Volume-Mount nach /usr/share/nginx/html/env.js. Dasselbe Image läuft in Dev, Stage und Prod – nur die ConfigMap unterscheidet sich. Änderung greift erst nach oc rollout restart deployment/library-frontend. In env.js gehören nur öffentliche Werte, nie Secrets.
  • eine statische SPA wird einmal gebaut und kann zur Laufzeit keine Env-Vars lesen – im Browser gibt es kein process.env
  • der falsche Weg: URLs hart ins Bundle (environment.prod.ts) → jede Umgebung braucht ihr eigenes Image, ein Fehler heißt neu bauen
  • Grundregel build once, deploy everywhere: das getestete Artefakt ist genau das, was ausgeliefert wird
  • das env.js-Muster: index.html lädt <script src="env.js"> vor dem App-Bundle
  • env.js setzt window.__APP_CONFIG__ auf { apiUrl, keycloakUrl, realm }, die App liest das beim Start
  • env.js ist nicht im Build – im Image liegt nur ein Platzhalter
  • zur Laufzeit kommt die echte Datei aus einer ConfigMap (B8), als Volume-Mount nach /usr/share/nginx/html/env.js – kein env
  • pro Umgebung eine eigene ConfigMap: gleiche Keys, andere Werte, dasselbe Image
  • Alternative: ein Entrypoint-Skript füllt per envsubst eine Vorlage aus den Pod-env-Vars, oder ein /config-Sidecar
  • ConfigMap geändert → oc rollout restart deployment/library-frontend, ein Mount wird nicht heiss neu geladen (B8, C6)
  • in env.js gehören nur öffentliche Werte – die Datei geht ungefiltert an jeden Browser, nie Secrets
  • dieselben Werte füttern J2 (API-URL fürs Pfad-Routing) und J3 (Keycloak-URL, Realm, Client-id)
  • Server-Frameworks (Next.js SSR, Nuxt) brauchen das nicht – die lesen echte env-Vars im Node-Prozess
  • von Hand durchspielen: frontend-spa-openshift.html – env.js aus einer ConfigMap mounten und ohne Rebuild ändern

Plan Plan & Regeln

Damit die Spirale nicht heimlich zerfällt: der Plan steht auf der Seite, nicht im Gedächtnis. Langfassung in openshift-atlas-plan.md.

Methode

Hybrid-Spirale. Erst ein Breiten-Durchgang über alle Themen – jedes Thema bekommt mindestens ein Diagramm. Danach Tiefe auf zwei bis drei job-relevante Themen hands-on. Die Diagramm-Methode aus der Diagrammseite zwingt zum Verstehen: ein falsches Diagramm fällt beim Zeichnen auf.

Versionsmatrix & Geltungsbereich
BereichReferenzHinweis
OKD / OCP-Plattform4.x; konkrete Cluster-Version maßgeblichCLI-, API- und Operator-Aussagen vor dem Hands-on mit oc version, oc api-resources und der Release-Dokumentation des Zielclusters prüfen.
ServiceAccount-TokenOCP 4.16+Keine langlebigen Token-Secrets mehr automatisch neu; vorhandene Secrets aus älteren Clustern können weiter existieren.
Deployment / DeploymentConfigOCP 4.14+DeploymentConfig ist deprecated. Das Ergebnis von oc new-app hängt von Client-Version, Eingabe und den gesetzten Flags ab.
SCC / Logging / OLM / Service MeshRelease- und Operator-abhängigChannel, CRD, Default-SCC und Stack vor dem Hands-on im Zielcluster prüfen; Namen und APIs können sich ändern.
Keycloak hinter RouteImage-Version des eingesetzten KeycloakBei aktuellen Images die Reverse-Proxy-Semantik mit KC_PROXY_HEADERS, KC_HOSTNAME und ggf. KC_HTTP_ENABLED konfigurieren; ältere Images können KC_PROXY erwarten.
  1. Breite = Pflicht, Tiefe = optional. Aufhören nach der Breite ist ein Erfolg, keine Ruine.
  2. Eine Einheit = eine Sitzung = ein Commit = ein Diagramm + Unterschrift. Kein „ich lerne jetzt Operatoren“, sondern „Diagramm D2: wie OLM einen Operator installiert“. Nach jedem Commit ist das Vorhandene kohärent.
  3. Deckungsmatrix (unten) = EX280-Lernziele vereint mit den Atlas-Blöcken A–J. „Breite fertig“ = jede Zeile hat ein Diagramm.
  4. Kein fester Takt. Jede Zeile trägt „zuletzt“. 30-Tage-Stolperdraht: zeigt die ganze Matrix 30 Tage nichts angefasst – bewusst weitermachen oder bewusst ins Regal stellen. Ein Alarm gegen das stille Sterben, kein Deadline-Druck.
  5. Erklärregel. Jeder Fachbegriff bekommt beim ersten Auftreten einen Satz Definition – auch scheinbar selbstverständliche (Namespace, Service, Deployment, Route). Einfache Sprache vor Jargon.
  6. Viele Punkte je Diagramm, jeder kurz. Nicht laenger erklaeren, sondern mehr Punkte ansprechen – mehr Beschriftungen, mehr Legendeneintraege, mehr kurze Callouts. Ein Punkt = ein Stichwort oder Halbsatz. Ein Nebenkasten „was im Gespräch dazu oft gefragt wird“ ist erwünscht.
  7. Glossar: der Loader der zentralen shared-glossary (Nachbarordner unter final_Besser) ist eingebunden und markiert bekannte Begriffe, wenn der Ordner erreichbar ist. Ist er es nicht (cloud-host allein veröffentlicht), passiert nichts – die Seite bleibt unverändert. Zusätzlich: die Erklärregel und die Punkte-Blöcke je Diagramm.
  8. Video (HyperFrames): ein Capstone-Clip pro vollständigem Block, montiert aus schon existierenden Diagrammen. Optional.
Referenz-Apps

Bibliothek-Enterprise ist das Rückgrat – rund neun von zehn Diagrammen. Novaris (Legacy-Versicherung) nur in Block H, Concordia/WAS nur in Block I. Ein Novaris-/Concordia-Diagramm wird nur gezeichnet, wenn es etwas OpenShift-Spezifisches zeigt, das Bibliothek nicht kann.

Infrastruktur

Die Spalte hands-on sagt, wo ein Diagramm ausprobierbar ist. SNO (Single Node OpenShift – ein Rechner ist Control-Plane-Node und Worker-Node zugleich) ist die Umgebung für fast alles. SNO ohne HA: läuft auf dem Single-Node, nur die Redundanz fehlt. Multi-Node: braucht einen zweiten Node – dafür reicht ein lokaler kind-Cluster (zwei Nodes für B6 und F1, drei Control-Plane-Nodes für G7), kein Hetzner-Cluster nötig. Recherche: reines Konzept ohne Umgebung. Der Atlas läuft auf OKD, der quelloffenen Variante – OCP-Unterschiede stehen inline. CRC (OpenShift Local auf dem Laptop) ist optional zum lokalen Vorführen.

Labor-Sicherheit

Destruktive Beispiele nur in einer Testumgebung ausführen. Dazu gehören oc delete project, sno.ps1 destroy, oc delete secret kubeadmin und Image-Pruning mit --confirm. Vor dem Löschen aktuellen Namespace, Snapshot und Wiederherstellungsweg prüfen; ein gelöschtes Projekt ist nicht rückgängig zu machen.

Passwörter und Tokens nicht mit -p oder --docker-password in die Shell-History schreiben, sondern interaktiv bzw. über --password-stdin oder einen Secret-Manager zuführen. --tls-verify=false bleibt auf ein isoliertes Labor begrenzt; besser die Router-CA vertrauen.

Matrix Deckungsmatrix

Jede Zeile verlinkt auf ihren ausformulierten Punkte-Block. in der Spalte Diagramm = das SVG ist gezeichnet, sonst sind es bisher nur die Punkte. „Breite fertig“ = jede Zeile hat ein Diagramm. = im EX280-Prüfungsumfang (Red Hat Certified Specialist in OpenShift Administration). App = die Referenz-App, an der die Punkte hängen (Bibliothek-Enterprise, Novaris nur Block H, Concordia nur Block I) – bei reinen Konzept-Zeilen. Dauer = grobe Zeit für die Hands-on-Anleitung, wo es eine gibt.

Atlas-Deckungsmatrix
NrThema – die eine FrageDiagrammAnleitunghands-onAppDauerzuletzt
Block 0 – Vorbereitung
01SNO-Aufbau: von Rescue-Boot zum laufenden OKD-Single-NodeUmgebungenSNOBibliothek~40 Min2026-09-05
02CRC / OpenShift Local – volles OpenShift auf dem Laptop, und wo es aufhörtUmgebungenCRCBibliothek~40 Min2026-09-05
03Was läuft wo – Laptop, Tailscale-Tunnel, SNO, lokaler Jenkins, der Deploy-WegUmgebungenSNOBibliothek~40 Min2026-09-05
04Umgebungen & Konfiguration – die Landkarte: wo alles liegt, wo man nachschautKonfigurationSNOBibliothek~25 Min (Referenz)2026-09-04
Block A – Fundament: was OpenShift zu Kubernetes hinzufügt
A1OKD vs OCP vs Kubernetes – welche Teile kommen obendrauf?Kern/AuthSNO~30 Min2026-09-04
A2Project vs Namespace – was entsteht bei oc new-project automatisch?Project/oc/SASNOBibliothek~30 Min2026-09-03
A3oc vs kubectl, API-Ressourcen, CRDs, Web-KonsoleProject/oc/SASNOBibliothek~30 Min2026-09-03
A4Authentifizierung – Identity Provider, OAuth-Stack, htpasswd, oc loginKern/AuthSNOBibliothek~30 Min2026-09-04
A5RBAC – Role, RoleBinding, oc adm policy, die Default-RollenRBAC/SCCSNOBibliothek~35 Min3. Sep 2026
A6ServiceAccount – welche Identität bekommt ein Pod?Project/oc/SASNOBibliothek~30 Min2026-09-03
A7SCC (Security Context Constraints) – warum ein Container nicht als root läuftRBAC/SCCSNOBibliothek~35 Min2026-09-03
Block B – Workloads laufen lassen, konfigurieren, ausliefern
B1Pod → ReplicaSet → Deployment – die Kette in einem BildBuild/RolloutSNOBibliothek~45 Min2026-09-05
B2Deployment vs DeploymentConfig – Trigger, Lifecycle-Hooks, wann welches?Workload-FeinschliffSNOBibliothek~35 Min2026-09-03
B3Service, Endpoints, Cluster-DNS – wie ein Name zum Pod wirdService/RouteSNOBibliothek~30 Min2026-09-04
B4Route – edge/passthrough/reencrypt, der HAProxy-Router, das Wildcard-ZertifikatService/RouteSNOBibliothek~30 Min2026-09-04
B5Probes – liveness, readiness, startup: was jede steuertService/RouteSNOBibliothek~30 Min2026-09-04
B6Scheduling – nodeSelector, Taints/Tolerations, AffinityWorkload-FeinschliffMulti-NodeBibliothek~35 Min2026-09-04
B7ResourceQuota & LimitRange – Ressourcengrenzen je ProjectWorkload-FeinschliffSNOBibliothek~35 Min2026-09-04
B8ConfigMap & Secret – wie ein Pod an Konfiguration und Secrets kommtWorkload-FeinschliffSNOBibliothek~35 Min2026-09-04
B9Secret-Schwerpunkt – Typen, Konsum, Verschlüsselung at-restSecretsSNOBibliothek~40 Min2026-09-04
B10Secret-Schwerpunkt – Herkunft, Pull-Secrets, Rotation, raus aus GitSecretsSNOBibliothek~40 Min2026-09-04
B11Helm-Schwerpunkt – Chart, Values, Template, Release: das mentale ModellHelmSNOBibliothek~30 Min2026-09-03
B12Helm-Schwerpunkt – Praxis am Bibliothek-Chart, wo Helm aufhörtHelmSNOBibliothek~30 Min2026-09-04
Block C – die Build-Kette (OpenShift-eigen)
C1ImageStream & ImageStreamTag – Digest statt Tag, und warum das die Tag-Falle löstBuild-KetteSNOBibliothek~50 Min2026-09-04
C2BuildConfig & Build-Strategien – S2I, Docker, customBuild-KetteSNOBibliothek~50 Min2026-09-04
C3Source-to-Image von innen – die assemble/run-Skripte, das Builder-Image, inkrementellBuild/RolloutSNOBibliothek~45 Min2026-09-05
C4oc new-app / oc new-build – welche Objekte sie erzeugenBuild-KetteSNOBibliothek~50 Min2026-09-04
C5Die interne Registry – der image-registry-Operator, die zwei Adressen, PruningBuild-KetteSNOBibliothek~50 Min2026-09-04
C6Deployment-Trigger – ImageChange und ConfigChangeBuild/RolloutSNOBibliothek~45 Min2026-09-05
Block D – Operatoren & OLM
D1Was ist ein Operator? – Controller + CRD + Betriebswissen als CodeReconcile/CARecherche~35 Min2026-09-05
D2OLM – CatalogSource, Subscription, InstallPlan, ClusterServiceVersion, OperatorGroupOLMSNOBibliothek~30 Min2026-09-04
D3OperatorHub – die vier Default-Kataloge, Installation über die KonsoleOLMSNOBibliothek~30 Min2026-09-04
D4Einen Operator hands-on installieren – Channels, Dependency-Resolution, ApprovalOLMSNOBibliothek~30 Min2026-09-04
D5Operator-verwaltete Last – eine Custom Resource anlegen, Reconcile beobachtenReconcile/CRSNOBibliothek~35 Min2026-09-05
Block E – CI/CD auf OpenShift
E1Jenkins auf OCP vs der OpenShift-Weg – wer die CPU im Cluster verbrauchtTektonSNOBibliothek~35 Min2026-09-04
E2OpenShift Pipelines (Tekton) – Task, Pipeline, PipelineRun, WorkspacesTektonSNOBibliothek~35 Min2026-09-04
E3OpenShift GitOps (Argo CD) – Application, Sync-Waves, das App-of-Apps-MusterGitOpsSNOBibliothek~40 Min2026-09-04
E4GitOps-Rebuild – den Cluster aus Git wiederherstellen (Disaster-Recovery-Übung)GitOpsSNOBibliothek~40 Min2026-09-04
E5Secrets in Git – Sealed Secrets bzw. External SecretsGitOpsSNOBibliothek~40 Min2026-09-04
Block F – Netzwerk & Sicherheit tiefer
F1OVN-Kubernetes – wie ein Paket von Pod zu Pod kommt (Geneve-Overlay)OVN-PfadMulti-NodeBibliothek~45 Min2026-09-05
F2NetworkPolicy – default-allow vs default-deny, Namespace-IsolationNetworkPolicySNOBibliothek~35 Min2026-09-04
F3Router-interna – die HAProxy-Konfig, Sharding, wie eine Route zum Backend wirdRouterSNOBibliothek~45 Min2026-09-05
F4Service Mesh (OpenShift Service Mesh / Istio) – Sidecar, mTLS, wann man es brauchtMeshSNOBibliothek~45 Min2026-09-05
F5serving-certs – der Service-CA-Operator: Zertifikate im Cluster ohne Let's EncryptService-CASNOBibliothek~35 Min2026-09-05
Block G – Betrieb (Admin-Tiefe, meist auf SNO)
G1Der Monitoring-Stack – Prometheus, Thanos, Alertmanager, user-workload monitoringCluster-BetriebSNO ohne HABibliothek~35 Min2026-09-04
G2Logging – der Loki/Vector-Stack, ClusterLogForwarderLoggingSNO ohne HABibliothek~45 Min2026-09-05
G3Storage – CSI, StorageClass, PV/PVC-Bindung, RWO vs RWXCluster-BetriebSNOBibliothek~35 Min2026-09-04
G4MachineConfig & der MCO – wie RHCOS-Nodes konfiguriert werden (und warum man nicht per SSH editiert)MCOSNO ohne HABibliothek~45 Min2026-09-05
G5Cluster-Upgrades – der Cluster Version Operator, Upgrade-Channels, oc adm upgradeCluster-BetriebSNO ohne HABibliothek~35 Min2026-09-04
G6ClusterOperators – was oc get co sagt: Available vs DegradedCluster-BetriebSNOBibliothek~35 Min2026-09-04
G7etcd – Quorum, Backup/Restore, die SNO-Besonderheitenetcd-BackupMulti-NodeBibliothek~45 Min2026-09-05
Block H – Legacy-Migration (Novaris)
H1SCC & root – was bricht, wenn eine Legacy-App UID 0 erwartetLegacySNONovaris~50 Min2026-09-04
H2Schreibbares Dateisystem – readOnlyRootFilesystem, emptyDir für /tmpLegacySNONovaris~50 Min2026-09-04
H3Session-State – Sticky Sessions am Router oder raus nach RedisLegacySNONovaris~50 Min2026-09-04
H4EJB-Timer & Singletons – in einer Welt mit mehreren ReplicasLegacySNONovaris~50 Min2026-09-04
H5JNDI → configDropins/Env – wem die Datenbankverbindung gehörtLegacySNONovaris~50 Min2026-09-04
H6Vorher/Nachher – legacy-parent auf WAS traditional vs modern-parent auf Liberty/OCPLegacySNONovaris~50 Min2026-09-04
Block I – kommerzielle Middleware (Concordia/WAS)
I1Das Lizenz-Gate im Dockerfile – warum der Container ohne Zustimmung nicht startetMiddlewareSNOConcordia~35 Min2026-09-04
I2Entitled builds – Pull aus registry.redhat.io, das Pull-SecretMiddlewareSNOConcordia~35 Min2026-09-04
I3Riesen-Base-Images – Layer-Caching, warum das WAS-Image 2 GB istMiddlewareSNOConcordia~35 Min2026-09-04
I4Die Admin-Konsole als Route – passthrough-TLS, der wsadmin/Scripting-WegMiddlewareSNOConcordia~35 Min2026-09-04
Block J – Frontend auf OpenShift
J1Eine SPA ausliefern – nginx-Pod vs nodejs-S2I, die RouteFrontendCRC/SNOBibliothek~35 Min2026-09-04
J2Browser → Route → api-gateway – eine Route vs pro Service, Pfad-RoutingFrontendSNOBibliothek~35 Min2026-09-04
J3Keycloak-Login aus dem Browser – public client, der Redirect-Flow, PKCEFrontendSNOBibliothek~35 Min2026-09-04
J4Laufzeit-Config in ein statisches Build – das env.js-Muster, ConfigMap-mountFrontendSNOBibliothek~35 Min2026-09-04

65 Zeilen, davon 23 mit (EX280). 65 mit Diagramm (alle), 65 mit eigener Anleitung.  Schwerpunkt = Thema mit mehreren Diagrammen (Helm, Secrets). Zuletzt bearbeitet: 2026-09-05.