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:
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.
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 IPv4okd-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.
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
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.
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 push → curl-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
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.
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.
Sieben Szenen, vom groben Bild (Kern + Schicht) bis zur Frage, warum ein Container nicht als root läuft.
Schritt 1/8
Szene 1 · OpenShift ist unverändertes Kubernetes plus eine feste Operatoren-Schicht – kein Fork.
Szene 2 · oc new-project legt mehr an als ein blosser Namespace – aber Quota und NetworkPolicy gehören nicht dazu.
Szene 3 · oc ist kubectl plus OpenShift-Verben, CRDs erweitern die API, die kubeconfig hält Cluster/User/Context.
Szene 4 · OpenShift bringt einen eigenen OAuth-Server mit – Rechte kommen aber erst über RBAC dazu.
Szene 5 · Role/RoleBinding sind Regel und Zuordnung, die Default-Rollen steigen von view bis cluster-admin.
Szene 6 · jeder Pod hat eine ServiceAccount als Identität – das Bound Token ist kurzlebig und rotiert automatisch.
Szene 7 · die SCC begrenzt den securityContext eines Pods – restricted-v2 ist die Vorgabe für jeden authentifizierten Account.
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.
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
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
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
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).
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
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
A3oc 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.
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-resourceslistet 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: keiner – oc, die API-Gruppen und die Konsole sind bei OKD und OCP identisch
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.
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 erreichbar – oc 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
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.
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 Bindungen – admin (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 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, deployer – builder und deployer sind OpenShift-eigen
ein Pod ohne serviceAccountName läuft als default – jeder 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 User – oc 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>
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.
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-Annotationenopenshift.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
Sieben Szenen: vom Objekt-Dreiklang bis zu den Ressourcengrenzen, die alles einrahmen.
Schritt 1/9
Szene 1 · Deployment verwaltet ReplicaSets, ReplicaSet hält Pods am Leben – ein Pod wird nie repariert, nur ersetzt.
Szene 2 · der Scheduler filtert und bewertet Node – nodeSelector, Taints/Tolerations und Affinity steuern die Platzierung.
Szene 3 · Deployment ist Kubernetes-Standard und passiv, DeploymentConfig ist OpenShift-eigen, aktiv und wird ausgephast.
Szene 4 · readiness entscheidet über Traffic, liveness über Leben, startup hält beide an, solange ein Pod noch bootet.
Szene 5 · ein Service ist ein fester Name vor wechselnden Pods, die EndpointSlice listet nur bereite IPs, DNS löst den Namen auf.
Szene 6 · edge, passthrough und reencrypt unterscheiden sich darin, wo TLS endet und ob Pfad-Routing geht.
Szene 7 · LimitRange füllt Defaults auf, ResourceQuota zieht die Obergrenze – zusammen bestimmen sie die QoS-Klasse jedes Pods.
Sechs Szenen: von der Grundform ConfigMap/Secret bis dahin, wo Helm aufhört und ein Operator anfangen muss.
Schritt 1/7
Szene 1 · ConfigMap und Secret haben dieselbe Form – nur Secret wird base64-kodiert und separat behandelt.
Szene 2 · der Secret-Typ erzwingt die Schlüssel, base64 ist keine Verschlüsselung – echte at-rest-Verschlüsselung ist ein Schalter am apiserver.
Szene 3 · ein Secret kommt aus sieben möglichen Quellen, oft ein Pull-Secret für die Registry.
Szene 4 · drei Wege, ein Secret trotzdem in Git zu versionieren: Sealed Secrets, External Secrets Operator, SOPS.
Szene 5 · ein Chart ist eine Textschablone, Werte werden gemergt, gerendert und als Release gespeichert.
Szene 6 · Template zuerst, dann atomarer Upgrade, Hooks daneben – und eine klare Grenze, wo Helm aufhört.
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.
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 ReplicaSethä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?
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.
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-appDeployments 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)
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.
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 8080 – api-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.
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
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 Ingressautomatisch 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
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 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
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
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.
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
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.
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
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
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 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) oderals 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: true – nur 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
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.
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
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
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.
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
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.
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 Secretsh.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.
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 10m – der 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)
Sechs Szenen: vom Digest-statt-Tag-Problem über Bauen und Registry bis zum Trigger, der den Kreis schliesst.
Schritt 1/8
Szene 1 · ein ImageStreamTag löst zu einem Digest auf – das löst die Tag-Falle von reinem Kubernetes.
Szene 2 · eine BuildConfig ist das Rezept, drei Strategien bauen: Source (S2I), Docker, Custom.
Szene 3 · das Builder-Image bringt assemble und run mit – assemble baut, run wird der Entrypoint.
Szene 4 · oc new-app rät aus der Eingabe und legt ImageStream, BuildConfig, Deployment und Service an – aber keine Route.
Szene 5 · die interne Registry hat zwei Adressen, innen und aussen – Pods ziehen ohne Extra-Secret.
Szene 6 · der Image-Trigger schliesst den Kreis: neuer Digest patcht die Vorlage, ein neues ReplicaSet rollt aus.
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.
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 ImageStreamcatalog-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
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).
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
Bibliothek: keinBuildConfig 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.
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/src → assemble → 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
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
C4oc 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.
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-apprä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=catalogrä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
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.
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 Routedefault-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 Zertifikat → podman 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.
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)
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
Fünf Szenen: vom Begriff Operator über OLM und den Hub bis zur echten, vom Operator verwalteten Last.
Schritt 1/8
Szene 1 · ein Operator ist Controller plus CRD – die CR ist die einzige Wahrheit, direkte Änderungen werden zurückgedreht.
Szene 2 · CatalogSource, Subscription und InstallPlan sind die drei OLM-Objekte, die einen Operator installieren.
Szene 3 · der OperatorHub zeigt vier Default-Kataloge mit unterschiedlichem Support-Niveau.
Szene 4 · Channels, Dependency-Resolution und Approval steuern, was und wann installiert wird.
Szene 5 · eine Custom Resource ist der Sollzustand – der Operator baut und hält alles, was daraus folgt.
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 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
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).
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.
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 channel → Installation mode → Update 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
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.
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-Pfad – 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 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}}'
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.
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.yaml – kind: 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
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 CR – Ready: 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
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
Sieben Szenen, eine durchgehende Geschichte: von der Frage wo wird gebaut bis zum Beweis, dass der ganze Cluster aus Git wiederherstellbar ist.
Schritt 1/10
Szene 1 · Code committen – zwei Wege, aber nur einer ist der OpenShift-Weg: nicht Jenkins im Cluster, sondern flüchtige Build-Pods oder externe CI.
Szene 2 · ein Webhook löst einen Trigger aus, Tekton baut das Image – Task, Pipeline, PipelineRun.
Szene 3 · zuerst muessen Sealed-Secrets-Controller und CRD vorhanden sein; danach kommt die Secret-Maschinerie in sync-wave -1.
Szene 4 · eine Wurzel-Application zeigt auf weitere Applications, eine je Dienst – ein Einstiegspunkt für den ganzen Cluster.
Szene 5 · Argo CD vergleicht Git gegen den Cluster, erkennt den Diff und synct in Wellen-Reihenfolge.
Szene 6 · eine manuelle Änderung im Cluster erzeugt Drift – automated.selfHeal dreht sie automatisch zurück.
Szene 7 · der Cluster ist wegwerfbar: destroy, install, Argo bootstrappen – und alles kommt aus Git zurück, nur die Daten nicht.
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.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 Diensthelm upgrade --install catalog ./chart -n library --atomic
authentifiziert alssystem: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
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.
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 steps – jeder Step ist ein Container (Image + Kommando), die Steps teilen sich einen Pod
eine Pipeline ist eine geordnete oder DAG-Anordnung von Tasks – runAfter 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 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
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.
Der Cluster ist wegwerfbar – die Daten nicht. Die Übung: sno.ps1 destroy → sno.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 destroy → sno.ps1 install → Argo 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.
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 secret – 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
Fünf Szenen: vom Paket zwischen zwei Pods bis zum internen TLS-Zertifikat, das kein Browser sehen soll.
Schritt 1/8
Szene 1 · OVN kapselt Pod-zu-Pod-Verkehr über Node hinweg in einem Geneve-Tunnel – auf demselben Node bleibt er lokal.
Szene 2 · ab Werk erreicht jeder Pod jeden – NetworkPolicy schaltet ausgewählte Pods auf default-deny und erlaubt gezielt wieder.
Szene 3 · der Router rendert aus Route und EndpointSlice eine HAProxy-Konfig – nur bereite Pods bekommen eine server-Zeile.
Szene 4 · ein Service Mesh spritzt Sidecars ein und regelt mTLS zentral – mit einem echten Preis in Latenz und RAM.
Szene 5 · eine Annotation genügt, der Service-CA-Operator liefert ein rotierendes TLS-Zertifikat für den internen Verkehr.
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.
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.
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
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.
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 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
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, 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.local – keine 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
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.ssl – api-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)
Sieben Szenen: von Metriken und Logs über Storage und Node-Konfiguration bis zu Upgrades, Operator-Gesundheit und etcd.
Schritt 1/8
Szene 1 · Plattform-Monitoring läuft immer, ServiceMonitor und PrometheusRule bringen die eigene App und ihre Alarme dazu.
Szene 2 · Vector sammelt je Node, LokiStack speichert nach Labels indiziert, der ClusterLogForwarder entscheidet, was wohin geht.
Szene 3 · ein PVC ist der Antrag, eine StorageClass die Vorlage – RWO reicht für die meisten Fälle, RWX braucht mehr.
Szene 4 · Node-Änderungen laufen nur über MachineConfig-Objekte, der MCO rollt sie kontrolliert aus – SSH-Änderungen überleben das nicht.
Szene 5 · der Cluster Version Operator gleicht Cluster und Nodes auf ein Release-Image ab – auf OKD-SNO oft eher ein Rebuild als ein Upgrade.
Szene 6 · rund 30 ClusterOperatoren melden Available/Degraded/Progressing – der erste Blick bei jedem Cluster-Problem.
Szene 7 · etcd hält den gesamten Cluster-Zustand, Quorum schützt vor Ausfällen – auf der SNO ist ein Backup der einzige Rettungsweg.
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, 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
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, 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
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.
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
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.
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: cordon → drain (PodDisruptionBudget beachtet) → schreiben → reboot → uncordon
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 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 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
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. ingressFalse → 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 authenticationDegraded, 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
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 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
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)
Sechs Szenen: die sechs Bruchstellen, an denen eine WAS-Legacy-App auf OpenShift zuerst stolpert – und wie man sie behebt.
Schritt 1/8
Szene 1 · unter restricted-v2 scheitert alles, was UID 0 erwartet – der richtige Fix ist ein UID-agnostisches Image, die Brücke ist anyuid an genau einer SA.
Szene 2 · readOnlyRootFilesystem macht das Image unveränderlich – jeder Schreibpfad braucht ein eigenes emptyDir.
Szene 3 · Sticky Sessions sind eine Brücke ohne Ausfallsicherheit – der echte Fix ist Zustand raus aus dem Heap, nach Redis oder als JWT.
Szene 4 · ein Timer, der in jeder JVM feuert, braucht ein CronJob oder eine Leader-Election, damit er nur einmal läuft.
Szene 5 · die Datenquelle wandert aus der server.xml in ConfigMap, Secret und ein configDropins-Fragment – der App-Code ändert sich nicht.
Szene 6 · vom Deployment auf den laufenden Server bis zum Image-Replace – manches bleibt trotzdem schwer, etwa verteilte Transaktionen.
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.
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
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 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 emptyDirdeckelt 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: keiner – readOnlyRootFilesystem 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.
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
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.
"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: keiner – CronJob, Lease und Recreate sind Standard-Kubernetes
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.
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 ConfigMapdb-config als Environment-Variablen (B8)
das Passwort kommt aus einem Secretdb-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 nicht – lookup("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.
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
Vier Szenen, vier eigene Bilder: ein Gate im Entrypoint, zwei Schlösser fuer volle RHEL-Pakete, ein Größenvergleich mit Layer-Kuchen, und ein Routenplan fuer die Admin-Konsole.
Schritt 1/6
Szene 1 · das Lizenz-Gate im Entrypoint entscheidet vor jedem Start – ohne LICENSE=accept endet der Container sofort, der Fix ist eine einzige Env-Variable im Deployment.
Szene 2 · reicht UBI, ist der Pull frei – für volle RHEL-Pakete braucht es zwei getrennte Dinge zugleich: Pull-Secret und Entitlement.
Szene 3 · der Application Server ist oft der Großteil des Images – Layer-Reihenfolge und schlanke Runtimes sind der größte Hebel dagegen.
Szene 4 · die Admin-Konsole braucht eine passthrough-Route, schreibt aber in einen laufenden, nicht reproduzierbaren Server – der richtige Weg ist Konfiguration als Code.
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.
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
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.
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 UBI – ubi-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/entitlement – dann 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
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.
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
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.
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/overridessind 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
Vier Szenen, letzter Bogen dieser Reihe: ein Verzweigungs-Diagramm fuer die Auslieferung, ein Seite-an-Seite-Vergleich fuer CORS, ein nummeriertes Ablaufdiagramm fuer den PKCE-Login, und ein Build/Runtime-Diagramm fuer env.js.
Schritt 1/6
Szene 1 · ein Build-Ordner ohne Serverlogik verzweigt sich in nginx (empfohlen) oder nodejs-S2I (nur bei echtem Serverbedarf), beide enden an einer edge-Route.
Szene 2 · getrennte Hosts brauchen CORS und eine Preflight-Runde, ein Host mit Pfad-Routing braucht keins von beidem.
Szene 3 · der PKCE-Login tauscht einen kurzen Code gegen Tokens, ohne dass der Code allein etwas wert wäre.
Szene 4 · dasselbe Image läuft in jeder Umgebung, nur env.js aus einer ConfigMap liefert die passenden Laufzeit-Werte.
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).
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.
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-Regel – curl 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.
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-Client – kein 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 /token → access_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. 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
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
Bereich
Referenz
Hinweis
OKD / OCP-Plattform
4.x; konkrete Cluster-Version maßgeblich
CLI-, API- und Operator-Aussagen vor dem Hands-on mit oc version, oc api-resources und der Release-Dokumentation des Zielclusters prüfen.
ServiceAccount-Token
OCP 4.16+
Keine langlebigen Token-Secrets mehr automatisch neu; vorhandene Secrets aus älteren Clustern können weiter existieren.
Deployment / DeploymentConfig
OCP 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 Mesh
Release- und Operator-abhängig
Channel, CRD, Default-SCC und Stack vor dem Hands-on im Zielcluster prüfen; Namen und APIs können sich ändern.
Keycloak hinter Route
Image-Version des eingesetzten Keycloak
Bei 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.
Breite = Pflicht, Tiefe = optional. Aufhören nach der Breite ist ein Erfolg, keine Ruine.
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.
Deckungsmatrix (unten) = EX280-Lernziele vereint mit den Atlas-Blöcken A–J. „Breite fertig“ = jede Zeile hat ein Diagramm.
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.
Erklärregel. Jeder Fachbegriff bekommt beim ersten Auftreten einen Satz Definition – auch scheinbar selbstverständliche (Namespace, Service, Deployment, Route). Einfache Sprache vor Jargon.
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.
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.
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.
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.