Kubernetes-Netzwerk wird verständlicher, wenn man aufhört, einen Service als kleinen Load Balancer im Cluster zu behandeln — und anfängt, ihn als Vertrag zu sehen.
Der Vertrag lautet ungefähr: „Clients können diesen stabilen Namen und Port nutzen. Kubernetes hält die aktuelle Menge passender Pods dahinter.“ Die Idee ist einfach. Die Details zählen, weil die meisten Anfängerprobleme im Netzwerk keine tiefen Routing-Fehler sind. Es sind meist Label-Mismatches, verwechselte Ports, Namespace-Annahmen oder DNS-Namen, die auf einen Service ohne bereite Endpoints zeigen.
Dieser Beitrag richtet sich an die Phase, in der Pods und Deployments grundsätzlich bekannt sind — und die Frage lautet: Warum erreicht man die Anwendung manchmal, aus manchen Orten, mit manchen Namen, aber nicht aus anderen?
Welches Problem Services lösen
Ein Pod bekommt eine IP-Adresse. In vielen Clustern ist diese IP von anderen Pods aus erreichbar. Wenn ein Pod web-7b9d4c8d6f-q2xk9 heißt und die IP 10.244.1.23 hat, kann ein anderer Pod unter Umständen http://10.244.1.23:8080 aufrufen.
Darauf sollte man eine Anwendung aber fast nie bauen.
Pods sind austauschbar. Ein Deployment löscht während eines Rollouts alte Pods und erstellt neue. Ein Node kann ausfallen. Eine Readiness-Probe kann einen Pod aus dem Traffic nehmen. Der Ersatz-Pod bekommt einen anderen Namen und meist eine andere IP. Merkt sich ein Client die alte Pod-IP, hält er plötzlich eine veraltete Adresse.
Dafür gibt es Services. Ein Service gibt Clients eine stabile Eingangstür für eine wechselnde Gruppe von Pods.
Die Gruppe wird über Labels ausgewählt:
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
spec:
replicas: 3
selector:
matchLabels:
app: checkout
template:
metadata:
labels:
app: checkout
spec:
containers:
- name: checkout
image: ghcr.io/example/checkout:1.0.0
ports:
- containerPort: 8080
Der Service zeigt auf Pods mit demselben Label:
apiVersion: v1
kind: Service
metadata:
name: checkout
spec:
type: ClusterIP
selector:
app: checkout
ports:
- name: http
port: 80
targetPort: 8080
Das kleine Stück YAML lohnt sich langsam zu lesen. Clients sprechen checkout auf Port 80 an. Kubernetes leitet an passende Pods auf Port 8080 weiter. port ist der Port des Service. targetPort ist der Port im Container. Diese beiden Felder zu verwechseln gehört zu den Klassikern.
Der Service bleibt, die Endpoints wechseln
Wenn ein Service einen Selector hat, beobachtet Kubernetes Pods, deren Labels dazu passen. Die tatsächlichen Zieladressen werden in EndpointSlices gespeichert. In älteren Tutorials sieht man oft noch Endpoints; moderne Cluster nutzen intern EndpointSlices. Für das Verständnis reicht zunächst: Dort steht, welche Pod-IPs und Ports im Moment hinter dem Service liegen.
Das ergibt einen brauchbaren Weg beim Debuggen:
kubectl get svc checkout
kubectl get pods -l app=checkout -o wide
kubectl get endpointslice -l kubernetes.io/service-name=checkout
kubectl describe svc checkout
Wenn der Service existiert, aber keine Endpoints hat, ist „das Netzwerk“ nicht der erste Verdacht. Der Service findet keine bereiten Pods. Dann prüft man Labels, Namespace, Readiness und Ports.
Nützliche Befehle:
kubectl get pods --show-labels
kubectl describe pod <pod-name>
kubectl get pods -l app=checkout
Der Selector eines Service muss zu den Labels im Pod-Template passen — nicht nur zu Labels am Deployment-Objekt. Labels nur am Deployment reichen nicht. Ein Service schickt keinen Traffic an Deployments. Er schickt Traffic an Pods.
Was ClusterIP bedeutet
Der Standardtyp eines Service ist ClusterIP. Kubernetes vergibt dafür eine virtuelle IP, zum Beispiel:
kubectl get svc checkout
Die Ausgabe kann so aussehen:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
checkout ClusterIP 10.96.42.117 <none> 80/TCP
Diese CLUSTER-IP bleibt für die Lebensdauer des Service stabil. Sie ist keine Pod-IP und gehört nicht zu einem einzelnen Container. In vielen Clustern programmiert kube-proxy — oder eine andere Datenebene — Regeln auf den Nodes, damit Traffic zu dieser virtuellen IP an einen der bereiten Backends verteilt wird.
Man muss am Anfang nicht jede iptables- oder eBPF-Variante auswendig kennen. Wichtiger ist das Modell: Die ClusterIP ist eine stabile virtuelle Adresse innerhalb des Clusters. Sie funktioniert nur von Orten aus, die das Cluster-Netz erreichen können. Der eigene Laptop kann sie normalerweise nicht direkt aufrufen.
Wenn also curl http://10.96.42.117 im lokalen Terminal fehlschlägt, beweist das nicht, dass der Service kaputt ist. Besser testen aus dem Cluster heraus:
kubectl run tmp-shell --rm -it --image=curlimages/curl -- sh
curl -v http://checkout
curl -v http://checkout.default.svc.cluster.local
In gemeinsam genutzten Clustern sollte man temporäre Debug-Pods mit Bedacht verwenden. Manche Organisationen verbieten sie aus guten Gründen. In einem lokalen Lerncluster sind sie sehr hilfreich.
DNS ist die lesbare Schicht über Services
Kubernetes betreibt meistens CoreDNS. Wird ein Service checkout im Namespace default angelegt, entstehen DNS-Einträge, über die Pods ihn finden können.
Aus einem Pod im selben Namespace funktioniert meistens:
checkout
Aus einem anderen Namespace:
checkout.default
checkout.default.svc
checkout.default.svc.cluster.local
Die vollständige Form lautet:
<service-name>.<namespace>.svc.cluster.local
Die Cluster-Domain ist oft cluster.local, kann aber anders konfiguriert sein. Wenn DNS merkwürdig wirkt, schaut man aus einem Pod hinein:
kubectl exec -it <pod-name> -- cat /etc/resolv.conf
kubectl exec -it <pod-name> -- nslookup checkout
kubectl exec -it <pod-name> -- nslookup checkout.default.svc.cluster.local
Der Kurzname checkout funktioniert wegen der Suchdomänen in /etc/resolv.conf. Ein Pod im Namespace payments sucht zum Beispiel zuerst unter payments.svc.cluster.local, dann unter svc.cluster.local, dann unter cluster.local. Diese Bequemlichkeit kann Namespace-Fehler verdecken. Gibt es in zwei Namespaces einen Service checkout, zeigt der Kurzname zuerst auf den Service im Namespace des aufrufenden Pods.
Beim Debuggen lohnt sich der vollständige Name. Er nimmt eine Variable aus der Gleichung.
Service-Typen regeln Erreichbarkeit, nicht Identität
Eine typische Anfängerfrage lautet: „Soll meine Anwendung ClusterIP, NodePort, LoadBalancer oder Ingress nutzen?“ Die Antwort hängt davon ab, wer die Anwendung erreichen soll.
ClusterIP ist für Kommunikation innerhalb des Clusters. Die meisten Services sollten damit beginnen.
NodePort öffnet einen Port auf jedem Node und leitet zum Service weiter. Das ist zum Lernen nützlich und hat einzelne Infrastruktur-Einsätze — ist aber selten der Weg, den ich für eine normale Webanwendung in Produktion wählen würde.
LoadBalancer bittet die darunterliegende Plattform, meist einen Cloud-Anbieter, einen externen Load Balancer für den Service zu erstellen. In lokalen Clustern bleibt dieser Typ oft auf pending, außer man nutzt zum Beispiel MetalLB oder eine lokale Emulation.
ExternalName liefert einen DNS-CNAME auf einen externen Namen. Dabei entstehen keine normalen Endpoints, und Kubernetes leitet keinen Traffic wie bei einem ClusterIP-Service weiter.
Ingress ist kein Service-Typ. Ingress ist eine eigene API für HTTP-Routing, meistens umgesetzt durch einen Ingress-Controller. Ein üblicher Produktionsweg sieht eher so aus:
Internet -> Cloud Load Balancer -> Ingress-Controller -> ClusterIP-Service -> Pods
Das wirkt nach vielen Schichten, aber jede Schicht hat eine Aufgabe. Der Service stabilisiert die wechselnden Pod-Ziele. Der Ingress-Controller versteht HTTP-Hosts und Pfade. Der externe Load Balancer bringt Traffic überhaupt erst in den Cluster.
Ports: kleine Wörter, große Wirkung
In Service-YAML stehen Feldnamen, die man leicht zu schnell liest:
ports:
- name: http
port: 80
targetPort: 8080
protocol: TCP
port ist der Port, den Clients am Service ansprechen.
targetPort ist der Port, auf dem der Pod den Traffic bekommt.
containerPort im Deployment ist vor allem Dokumentation und Metadatum für Menschen und Werkzeuge. Es öffnet keinen Port wie eine Firewall-Regel. Die Anwendung muss im Container trotzdem selbst auf diesem Port lauschen.
Man kann auch benannte Ports verwenden:
containers:
- name: checkout
image: ghcr.io/example/checkout:1.0.0
ports:
- name: http
containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: checkout
spec:
selector:
app: checkout
ports:
- port: 80
targetPort: http
Benannte Ports finde ich angenehm, wenn sie konsequent genutzt werden. Sie zeigen Absicht und überstehen manche Portänderung besser. Gleichzeitig ist es ein Name mehr, den man richtig schreiben muss.
Ein praktischer Ablauf beim Debuggen
Wenn ein Service nicht funktioniert, versuche ich, nicht zu raten. Ich gehe vom Client zum Backend.
Zuerst klären: Von wo wird getestet? Innerhalb oder außerhalb des Clusters macht einen großen Unterschied.
kubectl config current-context
kubectl config view --minify --output 'jsonpath={..namespace}'
Dann den Service prüfen:
kubectl get svc checkout -o wide
kubectl describe svc checkout
Wichtig sind Selector, Typ, ClusterIP und Ports.
Dann prüfen, ob Pods dazu passen:
kubectl get pods -l app=checkout -o wide
kubectl get pods --show-labels
Dann Endpoints ansehen:
kubectl get endpoints checkout -o yaml
kubectl get endpointslice -l kubernetes.io/service-name=checkout -o yaml
Wenn keine Endpoints vorhanden sind, geht der Blick zu Readiness:
kubectl describe pod <pod-name>
kubectl get pod <pod-name> -o jsonpath='{.status.conditions}'
Wenn Endpoints vorhanden sind, testet man DNS und HTTP aus dem Cluster:
kubectl run netcheck --rm -it --image=curlimages/curl -- sh
nslookup checkout
curl -v http://checkout
curl -v http://checkout.default.svc.cluster.local
Wenn DNS auflöst, aber HTTP fehlschlägt, kann es an Anwendung, Port, Protokoll, NetworkPolicy oder daran liegen, dass die Anwendung im Container nur auf localhost lauscht. Das passiert öfter, als man denkt. Ein Prozess auf 127.0.0.1 ist über die Pod-IP nicht erreichbar. Meist sollte die Anwendung auf 0.0.0.0 lauschen.
Häufige Missverständnisse
Erstes Missverständnis: „Ein Service startet meine Pods neu.“ Nein. Deployments und andere Controller steuern den Lebenszyklus von Pods. Services leiten Traffic.
Zweites Missverständnis: „Ein Service wartet automatisch, bis meine Anwendung gesund ist.“ Nicht von sich aus. Er nutzt bereite Endpoints. Ohne Readiness-Probe kann ein Pod sehr früh als bereit gelten — nämlich sobald der Container läuft.
Drittes Missverständnis: „Wenn DNS funktioniert, ist die Anwendung erreichbar.“ DNS beantwortet nur den Namen. Der Name kann auf einen Service ohne Endpoints, mit falschen Ports, blockiertem Traffic oder einer fehlerhaften Anwendung zeigen.
Viertes Missverständnis: „Nach ClusterIP kommt immer NodePort.“ In Tutorials manchmal. In Produktion oft nicht. Viele Plattformen nutzen Ingress, Gateway API, Service Meshes oder Cloud Load Balancer, je nach Standard der Umgebung.
Fünftes Missverständnis: „Namespaces sind nur Ordner.“ Sie beeinflussen DNS-Suche, Policies, RBAC, Quotas und die Art, wie Services adressiert werden. Ein vergessener Namespace ist kein kleines Detail.
Das Modell, das hängen bleiben sollte
Pods sind kurzlebige Arbeitslasten mit eigenen IPs. Services sind stabile Verträge über wechselnde Pod-Gruppen. EndpointSlices sind die aktuelle Passagierliste hinter diesem Vertrag. DNS gibt diesem Vertrag einen Namen, den Menschen und Anwendungen nutzen können.
Wenn etwas nicht erreichbar ist, beginne ich nicht mit „Kubernetes-Netzwerk ist kaputt“. Ich beginne mit diesen Fragen:
- Teste ich vom richtigen Ort?
- Existiert der Service im erwarteten Namespace?
- Passt der Selector zu bereiten Pods?
- Enthalten EndpointSlices Backend-Adressen?
- Löst DNS genau den Namen auf, den ich verwende?
- Lauscht die Anwendung auf dem erwarteten Port und Interface?
- Blockiert eine Policy oder eine Ingress-/Load-Balancer-Konfiguration den Weg?
Diese Reihenfolge löst nicht jedes Netzwerkproblem. Echte Cluster bringen CNI-Plugins, Network Policies, Ingress-Controller, Service Meshes, Cloud Load Balancer und alte Entscheidungen mit, die niemand anfassen möchte. Aber sie nimmt viel Nebel heraus.
Kubernetes-Netzwerk ist kein Zaubertrick. Es besteht aus Verträgen und Übersetzungen. Sobald klar ist, welches Objekt welchen Teil des Weges verantwortet, lässt sich das System viel besser befragen.