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.
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
Deploymentist Kubernetes-Standard (apps/v1), gefahren vomkube-controller-managerin-process - der
DeploymentConfigist OpenShift-eigen und wird ausgephast (apps.openshift.io/v1) – neu anlegen immer einDeployment - 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, SAdeployer, A6) – langsamer, klemmt eher - eingebaute DC-Trigger:
ImageChange(ein neuerImageStreamTaglöst automatisch einen Rollout aus, C1) undConfigChange - 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) undpost: ein Kommando in einem Pod, mit einerfailurePolicy - beim Deployment ersetzt man die Hooks durch Helm-Hook-Jobs,
initContainersoder einen eigenenJob - DC-Strategien:
Rolling,RecreateundCustom(ein eigenes Deployer-Image für Blue-Green oder Canary) - manuell: beim DC
oc rollout latest dc/<n>undoc rollout retry dc/<n>, beim Deploymentoc rollout undo deploy/<n>(dazurestart,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)
Deployment | DeploymentConfig | |
|---|---|---|
| API | apps/v1, Kubernetes-Standard | apps.openshift.io/v1, OpenShift-eigen, wird ausgephast |
| Kind-Objekt | ReplicaSet (B1) | ReplicationController |
| Rollout-Motor | der Controller selbst, in-process | ein eigener deployer-Pod (<dc>-<n>-deploy, SA deployer) – langsamer |
| Trigger | keine eingebauten – Annotation image.openshift.io/triggers | eingebaut: ImageChange, ConfigChange |
| Hooks | Helm-Hooks, initContainer, eigener Job | eingebaut: pre, mid (nur bei Recreate), post |
| Strategien | RollingUpdate, Recreate | Rolling, Recreate, Custom (eigenes Deployer-Image) |
# 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.
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.nodeNamesetzen - 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 bedeutetPending- 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: wienodeSelector, aber mit Operatoren undrequired(hart) /preferred(weich)podAffinity/podAntiAffinity: nah bei / fern von anderen Pods, immer mit einemtopologyKey(hostname, zone) – Anti-Affinity spreizt ReplicastopologySpreadConstraints: der moderne Weg, Replicas gleichmäßig zu verteilen (maxSkew,topologyKey,whenUnsatisfiable)- Priorität & Preemption: ein wartender Pod mit höherer
PriorityClasskann 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:
podAntiAffinityaufcatalog-service(2 Replicas auf verschiedene Nodes), Postgres pernodeSelectorauf einen Storage-Node - OCP-Unterschied: der Scheduler ist Kubernetes – OpenShift ergänzt Infra-/Worker-Rollen als Node-Labels und einen
Scheduler-Operator mit Profilen
PendingNoSchedule, PreferNoSchedule, NoExecutenodeAffinity (wie nodeSelector, mit required/preferred), podAffinity/podAntiAffinity (nah bei/fern von anderen Pods, immer mit topologyKey)BestEffort und packt schlechtDer 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.
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.
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 fehlenderequests/limitsje Container mit Default-Werten auf und setzt Min/Max – ein Admission-Plugin mutiert den PodResourceQuota(je Namespace): Obergrenze für alles zusammen – Summerequests.cpu/limits.memory, Anzahlpods/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,Burstabledazwischen,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 librarybringt aus demproject-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 aufcpx52(24 GB). Ohne die LimitRange würden mehrere der 8 Deploys abgelehnt, sobald die Quota existiert - OCP-Unterschied:
ClusterResourceQuotaist OpenShift-eigen,LimitRangeundResourceQuotasind reines Kubernetes
| wirkt | |
|---|---|
LimitRange | je Namespace – füllt fehlende requests/limits je Container mit Default-Werten auf, 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 |
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.
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).
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 indescribe, tmpfs beim Mount)- base64 ist keine Verschlüsselung – jeder mit
get secretssieht 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) oderoc 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:
viewdarf keine Secrets lesen.editdarf 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 alsSecret– das Frontend-env.jsist eine als Datei gemountete ConfigMap (J4) - OCP-Unterschied: keiner – ConfigMap und Secret sind reines Kubernetes,
oc extract/oc setsind Komfort
| Weg | bei einer Änderung |
|---|---|
env / envFrom | bei 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.
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
| Symptom | meist |
|---|---|
Pod bleibt Pending | kein Node passt – oc describe pod nennt den Grund in den Events |
Deploy scheitert mit must specify requests.cpu | eine Quota existiert, aber keine LimitRange füllt die Requests auf |
| App liest alten Wert nach ConfigMap-Änderung | der Wert kam per env – der ist eingefroren, ein Mount wäre aktuell |
| Pod stirbt mit Exit 137 | OOMKilled – Memory-Limit zu knapp, oder BestEffort ohne Limits stirbt zuerst bei Knappheit |
| DeploymentConfig-Rollout hängt | der deployer-Pod selbst hängt – oc logs dc/<name>-<n>-deploy |
9 Wie die Bibliothek es macht
| Baustein | real |
|---|---|
| Deployment | alle 8 Dienste sind Deployment (aus dem Helm-Chart, B11) – einen DeploymentConfig triffst du nur im Novaris-Legacy-Block (H) |
| Scheduling | podAntiAffinity 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 |
| Quota | oc new-project library bringt aus dem project-request-Template (A2) schon eine Quota und eine LimitRange mit |
| Ressourcenbudget | OKD + 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/Secret | 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) |
+ Die Kurzfassung
DeploymentConfig ist aktiv, OpenShift-eigen, wird ausgephast