☸️ Kubernetes Lektion 14/20 ~9 Min. Experte

DNS und Netzwerk-Troubleshooting

Service Discovery debuggen und Netzwerk-Probleme lösen.

📝 Meine Notizen

Lernziele

Nach dieser Lektion hast du eine feste Diagnose-Reihenfolge für „Service A erreicht Service B nicht” im Kopf: erst DNS, dann Endpoints, dann Netzwerkpfad, dann Policies. Du kannst mit netshoot, nslookup und tcpdump gezielt Schicht für Schicht testen, kennst die typischen CoreDNS-Probleme und weißt, wie du im Interview ein Netzwerk-Troubleshooting-Szenario strukturiert durchspielst.

Das Problem: „kann Service nicht erreichen” sagt fast nichts

Die Meldung im Incident-Channel lautet selten präziser als „Backend nicht erreichbar”. Dahinter können völlig verschiedene Defekte stecken: DNS löst nicht auf, der Service hat keine Endpoints, eine NetworkPolicy verwirft Pakete, kube-proxy hat veraltete Regeln, das CNI hat ein Routing-Problem — oder die Anwendung ist schlicht abgestürzt. Wer wild durcheinander testet, verbrennt im Incident Zeit und wirkt im Interview planlos.

Der Schlüssel ist, die Frage zu zerlegen. Eine Verbindung „Pod A → backend.payments → Pod B” hat drei trennbare Stufen: Namensauflösung (wird aus dem Namen eine IP?), Service-Schicht (zeigt der Service auf lebende Pods?) und Paketweg (kommen Pakete physisch an — oder filtert sie etwas?). Jede Stufe hat eigene Werkzeuge und eigene typische Defekte. Die Reihenfolge ist nicht beliebig: DNS zuerst, weil ohne Auflösung alles andere gar nicht erst startet.

Schon die Fehlermeldung grenzt ein: „could not resolve host” → Stufe 1. „Connection refused” → Pakete kommen an, aber niemand lauscht (falscher targetPort, App down) — Stufe 2. „Timeout” → Pakete versickern, typisch für NetworkPolicy oder Routing — Stufe 3.

Stufe 1: DNS — kennt CoreDNS den Namen?

Erster Reflex: einen Debug-Pod starten und auflösen lassen. Das Schweizer Taschenmesser dafür ist das netshoot-Image — es bringt dig, nslookup, curl, tcpdump und mehr mit:

kubectl run tmp --rm -it --image=nicolaka/netshoot -- bash
# darin:
nslookup backend.payments.svc.cluster.local
nslookup backend.payments      # Kurzform über Search-Domains
cat /etc/resolv.conf           # zeigt Nameserver (kube-dns ClusterIP) + Search-Domains

Schlägt die Auflösung fehl, gibt es drei Verdächtige. Erstens: Der Service existiert nicht oder heißt anders (kubectl get svc -n payments). Zweitens: Du bist im falschen Namespace unterwegs — die Kurzform backend funktioniert nur im eigenen Namespace, sonst brauchst du backend.payments (Lektion 11). Drittens: CoreDNS selbst hat ein Problem. Dann prüfst du:

kubectl -n kube-system get pods -l k8s-app=kube-dns
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=50
# Erreiche ich CoreDNS direkt, am resolv.conf-Eintrag vorbei?
dig @<kube-dns-ClusterIP> backend.payments.svc.cluster.local

Funktioniert dig @CoreDNS direkt, aber die normale Auflösung nicht, liegt das Problem zwischen Pod und CoreDNS — klassischerweise eine Egress-NetworkPolicy, die Port 53 blockiert. Ein subtilerer Dauerbrenner sind intermittierende Timeouts unter Last: zu wenige CoreDNS-Replicas für die Clustergröße, oder das bekannte Conntrack-Race bei parallelen UDP-Anfragen, das man mit NodeLocal DNSCache entschärft. „DNS ist langsam” ist in Kubernetes ein eigenes Krankheitsbild, kein Randthema.

Stufe 2: Service-Schicht — zeigen die Endpoints auf lebende Pods?

DNS liefert eine ClusterIP, aber die Verbindung scheitert trotzdem? Dann prüfst du die Kette Service-Selector → Endpoints → Readiness:

kubectl describe svc backend -n payments        # Selector + Ports
kubectl get endpointslices -n payments -l kubernetes.io/service-name=backend
kubectl get pods -n payments -l app=backend -o wide   # laufen Pods mit dem Label? Ready?

Leere EndpointSlices haben genau zwei Ursachen: Kein Pod trägt die Labels aus dem Selector (Tippfehler, vergessenes Label im Pod-Template), oder die Pods sind nicht ready (Readiness-Probe rot). Volle Endpoints plus „connection refused” deuten dagegen auf einen targetPort, der nicht zum Container-Port passt — oder auf eine App, die auf 127.0.0.1 statt 0.0.0.0 lauscht.

Der entscheidende Differenzialtest: die Pod-IP direkt ansprechen, am Service vorbei. curl http://<pod-ip>:8080 aus dem netshoot-Pod. Geht das, ist die Anwendung gesund und der Fehler liegt in der Service-Definition oder bei kube-proxy. Geht es nicht, ist es kein Service-Problem — weiter zu Stufe 3.

Stufe 3: Paketweg — Policies und tiefes Graben

Timeout bei direkter Pod-IP heißt: Pakete werden unterwegs verworfen. Hauptverdächtiger ist eine NetworkPolicy — prüfe beide Seiten und beide Richtungen:

kubectl get networkpolicy -A
kubectl describe networkpolicy -n payments

Denk an die Semantik aus Lektion 13: Sobald irgendeine Policy den Ziel-Pod für Ingress selektiert, muss dein Quell-Pod explizit erlaubt sein — und beim Quell-Pod gilt dasselbe für Egress. Stimmen die Namespace-Labels, auf die der namespaceSelector matcht? Das prüft man chronisch zu spät.

Wenn keine Policy verantwortlich ist, hilft nur Hinschauen auf Paketebene. tcpdump im netshoot-Pod (oder als Ephemeral Container am betroffenen Pod) zeigt, ob SYN-Pakete ankommen und ob Antworten zurückgehen:

kubectl debug -it <ziel-pod> --image=nicolaka/netshoot --target=<container> -- \
  tcpdump -ni any port 8080

SYN kommt an, keine Antwort → Problem im Ziel-Pod. Gar nichts kommt an → CNI-/Routing-Ebene, jetzt sind Node-Routen und CNI-Pod-Logs dran. Wer Cilium fährt, hat es komfortabler: Hubble zeigt pro Flow an, ob er FORWARDED oder DROPPED wurde — inklusive der Policy, die gedroppt hat (hubble observe --verdict DROPPED). Das ersetzt in vielen Fällen das tcpdump-Stochern komplett.

Praxis

Baue dir ein kaputtes Szenario und debugge es entlang der drei Stufen (kind oder minikube, CNI mit Policy-Support):

kubectl create deployment backend --image=nginx
kubectl expose deployment backend --port=80 --target-port=80

# Fehler 1 einbauen: Selector zerstören
kubectl patch svc backend -p '{"spec":{"selector":{"app":"backend-typo"}}}'
kubectl run tmp --rm -it --image=nicolaka/netshoot -- \
  curl -m 2 http://backend    # schlägt fehl
kubectl get endpointslices -l kubernetes.io/service-name=backend   # leer → gefunden
kubectl patch svc backend -p '{"spec":{"selector":{"app":"backend"}}}'

# Fehler 2 einbauen: Policy ohne DNS-Ausnahme
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-egress
spec:
  podSelector: {}
  policyTypes: ["Egress"]
EOF
kubectl run tmp --rm -it --image=nicolaka/netshoot -- nslookup backend
# Timeout → DNS-Egress blockiert; Policy wieder löschen:
kubectl delete networkpolicy deny-all-egress

Beide Defekte erzeugen dasselbe Oberflächen-Symptom („backend nicht erreichbar”), haben aber völlig verschiedene Signaturen — genau das Unterscheiden trainierst du hier.

Typische Stolperfallen

Debug-Image ohne Werkzeuge: Distroless- und Scratch-Images haben weder Shell noch curl. Statt zu verzweifeln: kubectl debug mit netshoot als Ephemeral Container an den laufenden Pod hängen.

Im falschen Kontext getestet: Vom Laptop aus ist die ClusterIP nie erreichbar — das ist kein Bug. Cluster-DNS und ClusterIPs existieren nur innerhalb des Clusters; immer aus einem Pod heraus testen.

Nur eine Richtung geprüft: Egress-Policy beim Sender und Ingress-Policy beim Empfänger müssen beide passen. Wer nur die Zielseite prüft, übersieht die Hälfte der Fälle.

DNS-Caching verschleiert den Fix: Manche Anwendungen (JVM!) cachen DNS-Antworten intern. Nach einem Fix scheint das Problem zu bleiben, bis der Pod neu startet — erst frisch testen, dann weitergraben.

Interview-Vorbereitung

Auf das Szenario „Pod erreicht einen Service nicht — wie gehst du vor?” antwortest du als Stufenplan: Symptom präzisieren (resolve-Fehler, refused oder timeout?) → DNS aus einem netshoot-Pod testen, resolv.conf und CoreDNS prüfen → Service-Schicht: Selector, EndpointSlices, Readiness, targetPort; Differenzialtest direkt gegen die Pod-IP → Paketweg: NetworkPolicies beider Seiten, dann tcpdump bzw. Hubble. Dass du laut benennst, welche Hypothese jeder Test prüft, ist der eigentliche Punktebringer.

Typische Follow-ups:

  • „DNS-Auflösung dauert sporadisch 5 Sekunden — woran denkst du?” — UDP-Conntrack-Race bei parallelen A/AAAA-Anfragen bzw. überlastetes CoreDNS; Abhilfe: NodeLocal DNSCache, mehr Replicas.
  • „Endpoints sind leer, Pods laufen — warum?” — Label-Mismatch zum Selector oder Readiness-Probe schlägt fehl; describe pod zeigt beides.
  • „Connection refused vs. timeout — was sagt dir das?” — Refused: Pakete kommen an, kein Listener (Port/App-Problem). Timeout: Pakete werden verworfen (Policy, Routing).
  • „Wie debuggst du einen Pod ohne Shell?” — kubectl debug mit Ephemeral Container und einem Tool-Image wie netshoot.

Zusammenfassung

Netzwerk-Debugging in Kubernetes ist eine Reihenfolge, kein Bauchgefühl: DNS (netshoot, nslookup, CoreDNS-Logs) → Service-Schicht (Selector, EndpointSlices, Readiness, targetPort, Direkttest auf die Pod-IP) → Paketweg (NetworkPolicies in beide Richtungen, tcpdump, Hubble). Die Fehlermeldung selbst — resolve-Fehler, refused, timeout — sagt dir, auf welcher Stufe du einsteigst.

Damit ist der Networking-Block abgeschlossen. Als Nächstes wechseln wir die Baustelle: Was passiert mit Daten, wenn ein Pod stirbt? PersistentVolumes, PVCs und StorageClasses sind das Thema der nächsten Lektion.