Voraussetzung: ein Cluster mit oc login,
Namespace library (oc new-project library oder oc project library).
Die Demos laufen mit einem einfachen httpd-Image, damit nichts vom
Bibliothek-Stack abhängt.
1 Das Service-Modell
Ein Service ist ein fester Name plus feste ClusterIP vor einer wechselnden Menge Pods – das Objekt bleibt, die Pods dahinter nicht.
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.
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 außen
kommt man erst mit einer Route (B4) rein.- ein
Serviceist ein fester Name plus festeclusterIPvor 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
EndpointSlicelistet die bereiten Pod-IPs – nur Pods mit passendem Label und grüner Readiness-Probe (B5). Sie löst das alteEndpoints-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.localzur 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) portist der Service-Port,targetPortder Container-Port – named ports gehen auchsessionAffinity: None(round-robin je Verbindung) oderClientIP(klebrig nach Quell-IP, mit Timeout)- die interne API ist selbst nur ein Service:
kubernetes.default.svc - prüfen:
oc describe svc catalog(ZeileEndpointsleer = 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 auf8080–api-gatewayruft die Fachdienste überhttp://catalog-service:8080usw. auf. Der Name kommt ausfullnameOverrideim Helm-Chart (B11) und muss exakt passen, sonst geht die Auflösung ins Leere - von außen erst über Route/Ingress (B4) – der Service allein hört nur im Cluster
- OCP-Unterschied: keiner – der DNS-Operator verwaltet CoreDNS, sonst reines Kubernetes
| Typ | ist |
|---|---|
ClusterIP | Vorgabe, nur clusterintern erreichbar |
NodePort | ClusterIP plus ein hoher Port auf jedem Node |
LoadBalancer | externe IP über Cloud-Provider oder MetalLB |
ExternalName | reiner DNS-CNAME auf einen externen Namen |
Headless (clusterIP: None) | DNS gibt die Pod-IPs direkt zurück, keine ClusterIP |
Der selector (ein Label-Ausdruck) entscheidet, welche Pods dazugehören –
der Endpoints-Controller pflegt daraus die EndpointSlice: die Liste der
bereiten Pod-IPs, nur Pods mit passendem Label und grüner Readiness-Probe (Schritt 6).
Hinter der ClusterIP läuft kein Prozess – OVN-Kubernetes programmiert den Node-Datapath
und macht DNAT auf eine ready Pod-IP, round-robin.
2 Service anlegen, per DNS erreichen
Ein Deployment mit zwei Replicas, ein Service davor, und der DNS-Name aus einem dritten Pod heraus getestet.
oc create deployment demo-web -n library --image=registry.access.redhat.com/ubi9/httpd-24 --replicas=2
oc expose deployment demo-web -n library --port=8080 --target-port=8080
oc get svc demo-web -n library
# von einem Test-Pod im selben Namespace aufloesen:
oc run dns-test -n library --rm -it --restart=Never \
--image=registry.access.redhat.com/ubi9/ubi-minimal -- \
curl -s -o /dev/null -w "%{http_code}\n" http://demo-web:8080/
# der vollqualifizierte Name geht auch namespaceuebergreifend:
oc run dns-test -n library --rm -it --restart=Never \
--image=registry.access.redhat.com/ubi9/ubi-minimal -- \
getent hosts demo-web.library.svc.cluster.local
Kurz demo-web löst nur im selben Namespace auf,
demo-web.library namespaceübergreifend, und
demo-web.library.svc.cluster.local ist der volle Name, den CoreDNS auflöst.
port ist der Service-Port, targetPort der Container-Port –
hier gleich, müssen es aber nicht sein.
3 Endpoints live brechen
Der häufigste Service-Fehler: der selector trifft keine Pods
mehr.
# Ist-Zustand: Endpoints gefuellt
oc get endpointslices -n library -l kubernetes.io/service-name=demo-web
oc describe svc demo-web -n library | grep Endpoints
# den Selector kaputt machen (Tippfehler nachstellen):
oc patch svc demo-web -n library --type=merge -p '{"spec":{"selector":{"app":"demo-web-typo"}}}'
oc describe svc demo-web -n library | grep Endpoints
# -> Endpoints:
# von aussen (Route/curl) kommt jetzt 503 statt einer Antwort - der Router
# hat niemanden, an den er weiterreichen kann
# zurueck:
oc patch svc demo-web -n library --type=merge -p '{"spec":{"selector":{"app":"demo-web"}}}'
oc describe svc demo-web -n library | grep Endpoints
Fallenselector ≠ Pod-Labels,
falscher targetPort, oder ein flappender Pod (Schritt 7) → sporadische
502/503 an der Route. Erster Griff: oc describe svc <name> – Zeile
Endpoints leer heißt Selector-Mismatch oder kein Pod ready.
4 Eine Route anlegen
Eine Route ist ein OpenShift-Objekt: Hostname → Service, mit Port, TLS-Modus und optional einem Pfad. Kubernetes-Pendant ist Ingress (F3).
Ein Service hört nur im Cluster (B3). Eine Route macht ihn von außen 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.
*.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
Routeist ein OpenShift-Objekt: Hostname → Service, mit Port, TLS-Modus und optional einem Pfad – Kubernetes-Pendant istIngress - 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 eineedge-Route kein eigenes Zertifikat (auf OKD/SNO selbstsigniert) edge: TLS endet am Router, dahinter HTTP zum Pod – der Router setztX-Forwarded-*, Pfad-Routing gehtpassthrough: 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 Headerreencrypt: Router terminiert und baut eine neue TLS-Verbindung zum Pod (mitdestinationCACertificate) – Ende-zu-Ende und L7-Routing- eigenes Zertifikat: inline in
route.spec.tls.certificate/.key, oder ein Secret über cert-manager - Keycloak-Falle: bei
edgesieht der Pod HTTP und bauthttp://-Redirects. Aktuelle Images brauchenKC_PROXY_HEADERS=xforwardedplus passendeKC_HOSTNAME/KC_HTTP_ENABLED=true, alte ImagesKC_PROXY=edge. Bei Spring hilftserver.forward-headers-strategy - anlegen:
oc expose svc/library-frontend(reine HTTP-Route),oc create route edge --service=library-frontend --hostname=... - Route-Status
Admitted: True= der Router hat sie angenommen.HostAlreadyClaimedwenn 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, schreibtIngress(F3) - Bibliothek: drei
edge-Routes –library-frontend,api-gatewayundkeycloakunter*.apps.sno.<ip>.nip.io, in der Jenkins-Prod-Stage peroc exposeangelegt. Die anderen 6 Dienste sind rein clusterintern
# reine HTTP-Route:
oc expose svc/demo-web -n library
oc get route demo-web -n library
# oder gleich verschluesselt:
oc create route edge demo-web-tls --service=demo-web -n library
oc get route demo-web-tls -n library -o jsonpath='{.spec.host}{"\n"}'
curl -s -o /dev/null -w "%{http_code}\n" http://$(oc get route demo-web -n library -o jsonpath='{.spec.host}')/
curl -sk -o /dev/null -w "%{http_code}\n" https://$(oc get route demo-web-tls -n library -o jsonpath='{.spec.host}')/
| Modus | TLS endet | kann |
|---|---|---|
edge | am Router, dahinter HTTP zum Pod | Pfad-Routing, Header wie X-Forwarded-* |
passthrough | am Pod – der Router fasst TLS nicht an | Client-Zertifikate, aber kein Pfad-Routing, keine Header |
reencrypt | am Router, neue TLS-Verbindung zum Pod (destinationCACertificate) | Ende-zu-Ende und L7-Routing |
oc get route demo-web -n library zeigt Admitted: True, sobald der
Router sie angenommen hat – HostAlreadyClaimed, wenn zwei Routes denselben
Host wollen.
5 Der Router und das Wildcard-Zertifikat
Der Router (HAProxy als Pod) hält Port 80 und 443 auf den Nodes, liest alle Routes clusterweit und schreibt daraus die HAProxy-Konfig neu.
Das Wildcard-DNS *.apps.<cluster> ist ein einziger A-Record, der auf den
Router zeigt – du legst ihn einmal an, jede neue Route braucht danach keinen eigenen
DNS-Eintrag. Der Router bringt ein Default-Wildcard-Zertifikat für
*.apps.<cluster> mit (auf OKD/SNO selbstsigniert) – darum braucht eine
edge-Route kein eigenes Zertifikat, außer du willst ein anderes: inline in
route.spec.tls.certificate/.key, oder ein Secret über cert-manager.
Keycloak-FalleBei edge sieht der
Pod nur HTTP und baut http://-Redirects. Aktuelle Keycloak-Images brauchen
KC_PROXY_HEADERS=xforwarded plus passende KC_HOSTNAME/
KC_HTTP_ENABLED=true, ältere Images KC_PROXY=edge. Bei Spring
hilft server.forward-headers-strategy.
OpenShift übersetzt ein Ingress-Objekt automatisch in eine Route – wer portabel bleiben will, schreibt Ingress und überlässt OpenShift die Übersetzung.
6 Das Probe-Modell
Drei Probes, drei getrennte Aufgaben – die häufigste Verwechslung kostet einen unnötigen Restart.
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.
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 istlivenessProbe: fällt siefailureThreshold-mal, tötet der kubelet den Container undrestartPolicystartet ihn neu – zu knapp = CrashLoopstartupProbe: solange sie läuft, sind readiness und liveness pausiert – für langsame Starter,failureThreshold × periodSeconds= maximale Bootzeit- readiness prüft Abhängigkeiten (DB erreichbar, Cache warm), liveness bleibt flach („antwortet der Prozess“) – sonst startet ein DB-Blip alle Replicas neu
- Arten:
httpGet(200–399 = ok),tcpSocket,exec(exit 0),grpc - Parameter:
initialDelaySeconds,periodSeconds(10),timeoutSeconds(1 – oft zu kurz),failureThreshold(3),successThreshold - „0/1 Running“ = läuft, aber nicht ready → in keinem Endpoint → 503 an der Route
- ohne startupProbe auf einer JVM tötet die liveness den Container mitten im Boot → CrashLoop – früher half ein großes
initialDelaySeconds - feste Health-Pfade: Spring Actuator
/actuator/health/livenessund/actuator/health/readiness, MicroProfile Health/health/liveund/health/ready - der Rollout ohne Ausfall hängt an der readiness (B1) – ein neuer Pod bekommt erst Verkehr, wenn bereit, und
maxUnavailablezählt bereite Pods - prüfen:
oc describe pod <p>(Events), die Spalte READY beioc get podist die Summe der readiness-Ergebnisse - Bibliothek-Stolperstein: die liveness mit
initialDelaySeconds: 30tötetecatalog-servicemitten im Spring-Kaltstart auf der geladenen SNO → hochgesetzt auf 150 s liveness / 90 s readiness. Alle 8 Dienste mit Actuator-Probes, Keycloak zusätzlich mitstartupProbe(langer Boot) - OCP-Unterschied: keiner – Probes sind reines Kubernetes, die Konsole zeigt sie zusätzlich in der Topologie-Ansicht
failureThreshold-mal, tötet der kubelet den Container, restartPolicy startet ihn neu – zu knapp eingestellt heißt CrashLoopfailureThreshold × periodSeconds = maximale Bootzeitreadiness prüft Abhängigkeiten (DB erreichbar, Cache warm),
liveness bleibt flach („antwortet der Prozess überhaupt“) –
sonst startet ein kurzer DB-Blip gleich alle Replicas neu. Arten: httpGet
(200–399 = ok), tcpSocket, exec (exit 0), grpc.
Parameter: initialDelaySeconds, periodSeconds (10),
timeoutSeconds (1 – oft zu kurz), failureThreshold (3),
successThreshold.
7 Eine readiness-Probe live brechen
Ein Pod mit einer readiness-Probe auf einen Pfad, der 500 antwortet – und beobachten, wie er aus den Endpoints fliegt, ohne zu sterben.
cat <<'YAML' | oc apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-probe
namespace: library
spec:
replicas: 1
selector: { matchLabels: { app: demo-probe } }
template:
metadata: { labels: { app: demo-probe } }
spec:
containers:
- name: web
image: registry.access.redhat.com/ubi9/httpd-24
ports: [{ containerPort: 8080 }]
readinessProbe:
httpGet: { path: /nonexistent-path, port: 8080 }
periodSeconds: 5
failureThreshold: 2
livenessProbe:
httpGet: { path: /, port: 8080 }
periodSeconds: 10
YAML
oc get pod -n library -l app=demo-probe -w
# -> READY 0/1, RESTARTS bleibt 0 (liveness ist gruen, nur readiness ist rot)
oc describe pod -n library -l app=demo-probe | grep -A2 Readiness
oc get events -n library --field-selector involvedObject.name=$(oc get pod -n library -l app=demo-probe -o jsonpath='{.items[0].metadata.name}') | grep -i unhealthy
oc delete deployment demo-probe -n library
was zu sehen ist0/1 Running,
keine Restarts – genau der Unterschied zwischen readiness (raus aus dem Verkehr) und
liveness (Neustart). Ein Service davor hätte in dieser Zeit Endpoints: <none>
(Schritt 3) und der Router 503.
8 Typische Fallen
| Symptom | meist |
|---|---|
| Route antwortet 503 | Service hat keine Endpoints – Selector-Mismatch oder alle Pods not-ready |
| sporadische 502 | ein flappender Pod wird kurz in den Endpoints geführt und fällt wieder raus |
0/1 Running dauerhaft | readiness prüft eine Abhängigkeit, die nicht erreichbar ist (falscher DB-Host, Netzwerkpolicy) |
| CrashLoopBackOff kurz nach dem Start | liveness ohne startupProbe tötet einen langsamen Starter mitten im Boot |
| Deploy hängt bei „progressing“ | der neue ReplicaSet wird nie ready – ohne grüne readiness bekommt er nie Verkehr, der Rollout wartet ewig (B1) |
9 Wie die Bibliothek es macht
| Baustein | real |
|---|---|
| Service | jeder der 8 Dienste ist ein ClusterIP-Service auf 8080 – api-gateway ruft die Fachdienste über http://catalog-service:8080 usw. Der Name kommt aus fullnameOverride im Helm-Chart und muss exakt passen |
| Route | nur drei edge-Routes – library-frontend, api-gateway und keycloak unter *.apps.sno.<ip>.nip.io, per oc expose in der Jenkins-Prod-Stage angelegt. Die anderen 6 Dienste sind rein clusterintern |
| Probes | alle 8 Dienste mit Spring-Actuator-Probes (/actuator/health/liveness, /actuator/health/readiness) – initialDelaySeconds: 30 tötete catalog-service mitten im Kaltstart auf der geladenen SNO, hochgesetzt auf 150 s liveness / 90 s readiness. Keycloak zusätzlich mit einer startupProbe für den langen Boot |
OCP-Unterschied bei allen dreien: keiner – Service, Route-Übersetzung des DNS-Operators und Probes sind entweder Kern-Kubernetes oder Kern-OpenShift, identisch auf OKD und OCP.
+ Die Kurzfassung
selector füllt die EndpointSlice – nur ready Pods sind drinedge/passthrough/reencrypt steuern wo TLS endet