← Übersicht  ·  Skripte & Dateien  ·  cloud-host · OpenShift · Workloads

Workload-Feinschliff in OpenShift

Die Zeichnungen dazu stehen hier bei den jeweiligen Schritten. Vier Bausteine, die jeden Alltag betreffen: warum Deployment und nicht DeploymentConfig, wie ein Pod gezielt (oder gar nicht) platziert wird, warum eine Quota ohne LimitRange alles blockiert – und der Unterschied zwischen env und Mount, live gesehen. EX280-relevant.

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

Voraussetzung: ein Cluster mit oc login als Admin, Namespace library.

1 Deployment vs DeploymentConfig

Beide fahren einen Rollout – nur einer ist Kubernetes-Standard.

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

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

# ein DeploymentConfig existiert nur noch als Altlast - sichtbar mit:
oc get dc -A
oc explain deploymentconfig.spec.strategy.type

Kernsatz: Deployment ist passiv (du oder Helm lösen einen Rollout aus), DeploymentConfig ist aktiv (beobachtet Image und Config selbst und reagiert). Neu anlegen immer ein Deployment – ein DeploymentConfig begegnet dir noch in echten Legacy-Migrationen (H).

2 Scheduling: Taint, Toleration, Affinity

Der Scheduler filtert (welche Nodes können?), bewertet (welcher ist am besten?), setzt dann spec.nodeName.

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

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

Der Node-Controller taintet automatisch bei disk-pressure, memory-pressure, not-ready – Control-Plane-Nodes sind per Default getaintet. topologySpreadConstraints ist der moderne Weg, Replicas gleichmässig zu verteilen (maxSkew, topologyKey, whenUnsatisfiable). Eine PriorityClass kann niedriger priorisierte Pods verdrängen (Preemption).

3 Einen Pod gezielt platzieren

Auf SNO ist alles einerlei (ein Node) – hier siehst du trotzdem live, wie ein nodeSelector ohne Match einen Pod hängen lässt.

Laptop · Bash
cat <<'YAML' | oc apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-schedule
  namespace: library
spec:
  replicas: 1
  selector: { matchLabels: { app: demo-schedule } }
  template:
    metadata: { labels: { app: demo-schedule } }
    spec:
      nodeSelector:
        kubernetes.io/hostname: nonexistent-node
      containers:
        - name: web
          image: registry.access.redhat.com/ubi9/httpd-24
YAML

oc get pod -n library -l app=demo-schedule
# -> Pending

oc describe pod -n library -l app=demo-schedule | grep -A3 Events
# -> 0/1 nodes are available: 1 node(s) didn't match Pod's node affinity/selector

# reparieren - Selector entfernen, damit der Pod wirklich startet:
oc patch deployment demo-schedule -n library --type=json \
  -p '[{"op":"remove","path":"/spec/template/spec/nodeSelector"}]'
oc get pod -n library -l app=demo-schedule -w

Genau dieses Muster – Pending plus die Events-Zeile – ist der erste Griff bei jedem Scheduling-Problem, egal ob nodeSelector, ein zu enger Taint oder zu wenig freie Ressourcen die Ursache sind.

4 ResourceQuota und LimitRange

Zwei Objekte, ein häufig übersehener Zusammenhang: die Reihenfolge, in der sie greifen.

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

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

der HakenEine Quota auf requests.* lässt nur Pods durch, bei denen jeder Container einen Request hat – ohne LimitRange werden Deploys mit der Fehlermeldung must specify requests.cpu abgelehnt. Darum erst die LimitRange, dann die Quota – sonst brechen alte Deploys ohne eigene Requests.

QoS-Klassen sind abgeleitet, nicht gesetzt: Guaranteed (requests == limits überall) stirbt zuletzt, Burstable dazwischen, BestEffort (nichts gesetzt) stirbt zuerst. CPU über Limit wird gedrosselt, Memory über Limit wird OOMKilled (Exit 137).

5 Eine Quota live gegen einen Deploy laufen lassen

Erst ohne LimitRange scheitern lassen, dann die Reihenfolge korrigieren.

Laptop · Bash
oc create quota demo-quota -n library --hard=requests.cpu=1,requests.memory=512Mi,pods=5

# ohne Requests im Deploy - scheitert an der Quota:
oc create deployment demo-quota-test -n library --image=registry.access.redhat.com/ubi9/httpd-24
oc get events -n library --field-selector involvedObject.name=demo-quota-test | grep -i quota
# -> failed quota: demo-quota: must specify requests.cpu,requests.memory

# jetzt eine LimitRange mit Default-Requests nachruesten:
cat <<'YAML' | oc apply -f -
apiVersion: v1
kind: LimitRange
metadata:
  name: demo-limits
  namespace: library
spec:
  limits:
    - type: Container
      defaultRequest: { cpu: 100m, memory: 128Mi }
      default: { cpu: 300m, memory: 256Mi }
YAML

# derselbe Deploy funktioniert jetzt, weil die LimitRange die Requests ergaenzt:
oc rollout restart deployment/demo-quota-test -n library
oc describe quota demo-quota -n library

oc describe quota -n library zeigt Used gegen Hard, oc describe limitrange -n library die konfigurierten Defaults. Eine ClusterResourceQuota (OpenShift-eigen) macht dasselbe über mehrere Projects per Label-Selektor.

6 ConfigMap und Secret: env vs Mount

Zwei Konsumwege, ein wichtiger Unterschied im Verhalten bei Änderungen.

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

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

Keiner von beiden löst einen Rollout aus – eine Änderung an ConfigMap/Secret rollt den Pod nicht automatisch neu. Üblich: eine checksum/config-Annotation (Helm rollt dann bei Änderung) oder von Hand oc rollout restart. Secret-Typen erzwingen ihre Schlüssel: Opaque, kubernetes.io/tls, kubernetes.io/dockerconfigjson, service-account-token, basic-auth, ssh-auth.

7 Den Unterschied live sehen

Ein ConfigMap-Wert per env, derselbe Wert per Mount – dann ändern und vergleichen.

Laptop · Bash
oc create configmap demo-cfg -n library --from-literal=GREETING=hallo

cat <<'YAML' | oc apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-config-consumer
  namespace: library
spec:
  replicas: 1
  selector: { matchLabels: { app: demo-config-consumer } }
  template:
    metadata: { labels: { app: demo-config-consumer } }
    spec:
      containers:
        - name: web
          image: registry.access.redhat.com/ubi9/ubi-minimal
          command: ["sleep", "3600"]
          envFrom:
            - configMapRef: { name: demo-cfg }
          volumeMounts:
            - name: cfg
              mountPath: /etc/demo-cfg
      volumes:
        - name: cfg
          configMap: { name: demo-cfg }
YAML

POD=$(oc get pod -n library -l app=demo-config-consumer -o jsonpath='{.items[0].metadata.name}')
oc exec -n library "$POD" -- sh -c 'echo ENV=$GREETING; cat /etc/demo-cfg/GREETING'

# jetzt die ConfigMap aendern, OHNE den Pod neu zu starten:
oc set data configmap/demo-cfg -n library --from-literal=GREETING=servus
sleep 70
oc exec -n library "$POD" -- sh -c 'echo ENV=$GREETING; echo MOUNT=$(cat /etc/demo-cfg/GREETING)'
# ENV zeigt noch "hallo" (eingefroren), MOUNT zeigt "servus" (aktualisiert)

was zu sehen istGenau der Unterschied aus Schritt 6, live: die Env-Variable bleibt beim alten Wert stehen, die gemountete Datei hat sich ohne Neustart aktualisiert.

8 Typische Fallen

Symptommeist
Pod bleibt Pendingkein Node passt – oc describe pod nennt den Grund in den Events
Deploy scheitert mit must specify requests.cpueine Quota existiert, aber keine LimitRange füllt die Requests auf
App liest alten Wert nach ConfigMap-Änderungder Wert kam per env – der ist eingefroren, ein Mount wäre aktuell
Pod stirbt mit Exit 137OOMKilled – Memory-Limit zu knapp, oder BestEffort ohne Limits stirbt zuerst bei Knappheit
DeploymentConfig-Rollout hängtder deployer-Pod selbst hängt – oc logs dc/<name>-<n>-deploy

9 Wie die Bibliothek es macht

Bausteinreal
Deploymentalle 8 Dienste sind Deployment (aus dem Helm-Chart, B11) – einen DeploymentConfig triffst du nur im Novaris-Legacy-Block (H)
SchedulingpodAntiAffinity auf catalog-service (2 Replicas auf verschiedene Nodes), Postgres per nodeSelector auf einen Storage-Node – auf der SNO wirkungslos, auf einem echten Multi-Node greift es
Quotaoc new-project library bringt aus dem project-request-Template (A2) schon eine Quota und eine LimitRange mit
RessourcenbudgetOKD + 8 JVMs + Postgres/Redis/Kafka/Keycloak zusammen ~15,6 GB – darum läuft die SNO auf cpx52 (24 GB). Ohne LimitRange würden mehrere der 8 Deploys abgelehnt
Config/SecretApp-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)

+ Die Kurzfassung

Deploymentpassiv, Kubernetes-Standard – DeploymentConfig ist aktiv, OpenShift-eigen, wird ausgephast
SchedulingRequests treiben die Platzierung, nicht Limits – nodeSelector/Taint/Affinity steuern sie gezielt
Quota + Configerst LimitRange, dann Quota – env ist eingefroren, ein Mount aktualisiert sich, keiner von beiden triggert einen Rollout
⌂ Cockpit