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.
| 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 heisst 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).
# 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, ausser 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.
failureThreshold-mal, tötet der kubelet den Container, restartPolicy startet ihn neu – zu knapp eingestellt heisst 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