Ich habe einen Service auf Azure Kubernetes Service (AKS) mit kubectl port-forward getestet, so wie es das Microsoft-Learn-Modul tut: 200, OK. Dann hat ein Pod im Cluster denselben Service aufgerufen: Connection refused. Zweimal.
Dieser Beitrag zeigt, womit port-forward wirklich verbindet, warum ein Pod, der nie bereit wird, trotzdem antwortet, und wie Sie einen Service richtig testen. Alles stammt aus der Übung des Microsoft-Learn-Moduls und ein paar kleinen Ergänzungen von mir, ausgeführt in meinem eigenen Cluster.
Das Wichtigste
- port-forward nutzt den Service nur, um einen Pod zu finden. kubectl liest den Selector des Service, wählt einen passenden Pod und öffnet über den API-Server und das Kubelet einen Tunnel zu ihm. Sechs Anfragen über port-forward erreichten einen Pod; dieselben sechs Aufrufe über den Service, aus dem Cluster heraus, erreichten drei.
- port-forward ignoriert die Readiness. Ein Pod, der seine Readiness-Probe nicht bestand (
Running,0/1), beantwortete port-forward mit 200, während beide Aufrufe über seinen Service abgewiesen wurden. Der Service schickt Aufrufe nur an bereite Pods. - Endpoint-Slice und Endpoints-Zeile beantworten verschiedene Fragen. Der Slice listet jeden Pod, der zu den Labels passt, bereit oder nicht. Die Zeile
Endpoints:vonkubectl describe servicelistet nur die bereiten, also die, die Traffic bekommen. - Einen Service testen Sie aus dem Cluster heraus, über seinen Namen. Prüfen Sie vorher die Endpoint-Slices und die Endpoints-Zeile. Ein port-forward testet einen Pod, und er stirbt mit diesem Pod.
Wohin port-forward wirklich geht
In der Übung läuft eine kleine Python-API im Namespace aks-troubleshoot hinter einem Service: einer stabilen Adresse, die Aufrufe auf die Pods der App verteilt. Sein Selector legt fest, welche Pods dazugehören, und er leitet seinen Port 80 auf den Port 8000 des Containers:
apiVersion: v1kind: Servicemetadata:name: api-servicespec:type: ClusterIPselector:app: apiports:- name: httpport: 80targetPort: 8000protocol: TCP
Die Übung prüft ihn mit port-forward: kubectl öffnet den lokalen Port 8080 und leitet ihn auf Port 80 des Service weiter, und eine Anfrage an localhost antwortet. pf.py ist mein kleiner Wrapper für diesen Schritt: Er startet kubectl port-forward, gibt die Zeilen von kubectl mit den Sekunden seit dem Start aus, schickt die Anfragen an localhost:8080 und beendet die Weiterleitung wieder.
> python pf.py service/api-service /healthz0.0s $ kubectl port-forward service/api-service 8080:800.5s kubectl: Forwarding from 127.0.0.1:8080 -> 80000.5s kubectl: Forwarding from [::1]:8080 -> 80001.2s kubectl: Handling connection for 80801.3s GET /healthz -> 200 {"status":"healthy","timestamp":"2026-11-02T13:58:56.934290"}2.3s forward stopped
Achten Sie auf den Pfeil: kubectl leitet auf 8000 weiter, den Port des Containers, nicht auf Port 80 des Service. Dann habe ich die App auf drei Replikate skaliert, drei Pods hinter demselben Service. Der Endpunkt /api/info der App antwortet mit der Startzeit seines Pods, jede Antwort zeigt also, welcher Pod geantwortet hat.
> kubectl scale deployment api-deployment --replicas 3deployment.apps/api-deployment scaled> python pf.py service/api-service /api/info --n 60.0s $ kubectl port-forward service/api-service 8080:800.6s kubectl: Forwarding from 127.0.0.1:8080 -> 80000.6s kubectl: Forwarding from [::1]:8080 -> 80001.1s kubectl: Handling connection for 80801.3s GET /api/info -> 200 pod started 13:58:45.051.3s kubectl: Handling connection for 80801.4s GET /api/info -> 200 pod started 13:58:45.051.4s kubectl: Handling connection for 80801.5s GET /api/info -> 200 pod started 13:58:45.051.5s kubectl: Handling connection for 80801.6s GET /api/info -> 200 pod started 13:58:45.051.6s kubectl: Handling connection for 80801.7s GET /api/info -> 200 pod started 13:58:45.051.7s kubectl: Handling connection for 80801.8s GET /api/info -> 200 pod started 13:58:45.052.9s forward stopped
Sechs Antworten, ein Pod. Dann dieselben sechs Aufrufe aus dem Cluster heraus: Ein Client-Pod, den ich dort gestartet hatte, führt ein winziges Python-Skript aus, das den Service über seinen Namen aufruft.
> kubectl exec client -- python /c/call.py http://api-service/api/info 61 200 pod started 14:16:35.842 200 pod started 14:16:35.663 200 pod started 14:16:35.844 200 pod started 14:16:35.665 200 pod started 13:58:45.056 200 pod started 14:16:35.66
Drei verschiedene Pods: Der Service hat die Aufrufe verteilt. Der Grund: kubectl schickt Ihre Anfrage nicht an den Service. Es öffnet einen Tunnel über den API-Server des Clusters, die Eingangstür für jeden kubectl-Befehl, zum Kubelet, dem Agenten auf dem Node, und von dort in einen Pod. Es liest den Selector des Service, die Label-Regel, die festlegt, welche Pods dazugehören, und wählt einen passenden Pod. Mehr nutzt es vom Service nicht.
port-forward testet einen Pod. Der Service ist alles, was vor den Pods liegt.
Ein Pod, der nie bereit wird
Eine Readiness-Probe ist die Prüfung, die entscheidet, ob ein Pod Traffic bekommt. Ich habe eine zweite App deployt, api-unready, mit eigenem Service und mit der kaputten Probe aus der Übung: einer Prüfung, die einen Pfad abfragt, den es nicht gibt.
readinessProbe:httpGet:path: /invalid-pathport: 8000initialDelaySeconds: 5periodSeconds: 10
pods.py ist ein weiteres kleines Skript von mir: Es gibt die Pods der App aus, sobald sich einer ändert.
> kubectl apply -f k8s/unready.yamldeployment.apps/api-unready createdservice/api-unready created> python pods.py --app api-unready --secs 300.3s api-unready-64cc88bb49-w6vpd ContainerCreating 0/1 restarts 02.5s api-unready-64cc88bb49-w6vpd Running 0/1 restarts 0
Running, 0/1: Er läuft, wird aber nie bereit. Taucht er im Endpoint-Slice des Service auf, der Liste der Adressen hinter dem Service? Ja:
> kubectl get endpointslices -l kubernetes.io/service-name=api-unreadyNAME ADDRESSTYPE PORTS ENDPOINTS AGEapi-unready-8srvl IPv4 8000 10.224.0.6 32s
Der Service selbst sagt etwas anderes (die Ausgabe gekürzt auf die Zeilen, auf die es ankommt):
> kubectl describe service api-unreadySelector: app=api-unreadyType: ClusterIPPort: http 80/TCPTargetPort: 8000/TCPEndpoints:
| Wo Sie nachsehen | Was dort steht |
|---|---|
kubectl get endpointslices -l kubernetes.io/service-name=… | jeder Pod, der zu den Labels passt, bereit oder nicht (der Slice markiert die nicht bereiten) |
Endpoints: in kubectl describe service … | nur die bereiten Pods, die Traffic bekommen |
Dann dieselben zwei Wege wie vorher. port-forward auf den neuen Service, mit demselben Befehl, und ein Aufruf über den Namen des Service aus dem Client-Pod:
> python pf.py service/api-unready /api/info0.0s $ kubectl port-forward service/api-unready 8080:800.7s kubectl: Forwarding from 127.0.0.1:8080 -> 80000.7s kubectl: Forwarding from [::1]:8080 -> 80001.3s kubectl: Handling connection for 80801.5s GET /api/info -> 200 pod started 14:19:54.662.5s forward stopped> kubectl exec client -- python /c/call.py http://api-unready/api/info 21 FAIL URLError <urlopen error [Errno 111] Connection refused>2 FAIL URLError <urlopen error [Errno 111] Connection refused>
port-forward ist egal, ob ein Pod bereit ist. Es braucht nur einen laufenden Pod. Die Readiness-Probe soll Traffic von genau so einem Pod fernhalten, und port-forward geht einfach an ihr vorbei.
Wenn kein Pod zum Selector passt
Ignoriert port-forward den Service also komplett? Nicht ganz. Die Übung hat einen zweiten Fehler: Sie ändert den Selector des Service auf ein Label, das kein Pod hat. Die Pods sind gesund und bereit; der Service sucht nur nicht mehr nach ihnen.
selector:app: api-v2
> kubectl get endpointslices -l kubernetes.io/service-name=api-serviceNAME ADDRESSTYPE PORTS ENDPOINTS AGEapi-service-mvjnn IPv4 <unset> <unset> 20m> kubectl exec client -- python /c/call.py http://api-service/api/info 21 FAIL URLError <urlopen error [Errno 111] Connection refused>2 FAIL URLError <urlopen error [Errno 111] Connection refused>> python pf.py service/api-service /healthz0.0s $ kubectl port-forward service/api-service 8080:8060.3s kubectl: error: timed out waiting for the condition61.0s kubectl exited with code 1
<unset>: Kein Pod passt, also gibt es gar keine Adresse. Der Client wurde abgewiesen, und port-forward hat eine Minute auf einen Pod gewartet und dann aufgegeben. Beide sind gescheitert, aus verschiedenen Gründen: Der Service hatte keinen Endpunkt, und port-forward hatte keinen Pod zur Auswahl. Nur der Endpoint-Slice hat gezeigt, warum.
Einen Service richtig testen
Drei Prüfungen, in dieser Reihenfolge:
# 1. Which pods match the selector? <unset> = nonekubectl get endpointslices -l kubernetes.io/service-name=api-service# 2. Which of them are ready? The Endpoints linekubectl describe service api-service# 3. Call it the way your other apps do: from a pod, by its namekubectl exec client -- python /c/call.py http://api-service/api/info 6
- Die Endpoint-Slices, über das Namens-Label des Service.
<unset>heißt: Kein Pod passt. Prüfen Sie die Labels auf beiden Seiten, im Selector des Service und im Pod-Template. Eine Adresse dort heißt, dass ein Pod passt, nicht, dass er bereit ist. - Den Service beschreiben. Seine Zeile
Endpoints:listet nur die bereiten Pods. Leer, obwohl Pods laufen, heißt: ein Readiness-Problem. Sehen Sie sich die Events des Pods an. - Ihn aus dem Cluster heraus aufrufen, aus einem Pod, über den Namen des Service. Bei mir war das der kleine Python-Client; jeder Pod mit curl tut dasselbe. Das ist der Weg, den Ihre anderen Apps nehmen: Das DNS des Clusters macht aus dem Namen die Adresse des Service, und der Service verteilt den Aufruf auf jeden bereiten Pod.
Sehen diese drei richtig aus und der Aufruf scheitert trotzdem, prüfen Sie die Ports: den port des Service und den targetPort, den Port, auf dem der Container lauscht.
Nutzen Sie port-forward für das, was es ist: eine schnelle, private Leitung zu einem Pod, nur für TCP (kein UDP). Ideal, um sich vom Laptop aus einen Pod anzusehen; kein Beweis, dass der Service funktioniert. Und weil der Tunnel über den API-Server läuft, kann er auch die Netzwerkregeln des Clusters umgehen. Es lohnt sich also, einzuschränken, wer ihn nutzen darf.
Wenn der Pod hinter der Weiterleitung verschwindet
Was passiert mit einem offenen port-forward, wenn sein einer Pod verschwindet? Ich habe ihn noch einmal geöffnet, zwei Antworten vom Pod mit dem Start um 13:58 bekommen und dann genau diesen Pod gelöscht (die Zeilen „Handling connection“ von kubectl weggelassen; den vollständigen Fehler finden Sie unten):
> python pf.py service/api-service /api/info --kill0.0s $ kubectl port-forward service/api-service 8080:800.6s kubectl: Forwarding from 127.0.0.1:8080 -> 80001.2s GET /api/info -> 200 pod started 13:58:45.051.4s GET /api/info -> 200 pod started 13:58:45.051.6s deleting the pod behind the forward: api-deployment-695945c854-nx6qj8.0s kubectl: error: lost connection to pod8.0s GET /api/info -> FAIL ConnectionResetError [WinError 10054] An existing connection was forcibly closed13.1s GET /api/info -> FAIL URLError <urlopen error [WinError 10061] No connection could be made18.2s GET /api/info -> FAIL URLError <urlopen error [WinError 10061] No connection could be made20.2s kubectl had already exited with code 1
Die Weiterleitung ist mit ihrem Pod gestorben. Sie wechselt nicht zu einem anderen, obwohl noch zwei Pods hinter demselben Service liefen. Währenddessen hat der Client im Cluster den Service weiter aufgerufen, einmal pro Sekunde:
> kubectl logs client --tail 814:20:43 200 pod started 14:16:35.6614:20:44 200 pod started 14:16:35.8414:20:45 200 pod started 14:20:34.3514:20:46 200 pod started 14:16:35.6614:20:47 200 pod started 14:20:34.3514:20:48 200 pod started 14:16:35.6614:20:49 200 pod started 14:20:34.3514:20:50 200 pod started 14:16:35.84
200, jede Sekunde, von den beiden anderen Pods und vom neuen (gestartet um 14:20:34), der den gelöschten ersetzt hat.
Was beim Ausführen passiert ist
Getestet am 2. November 2026 in meinem eigenen Azure-Abonnement (AKS 1.36 in westeurope, Azure CLI 2.88.0); danach wurde alles gelöscht. Der Cluster hatte einen Node (Standard_D2s_v5, Kubernetes v1.36.4) mit angebundener Container Registry, wie in der Übung, dazu kubectl 1.36.1; die App und die Manifeste der Übung liefen unverändert in ihrem Namespace. Meine Ergänzungen: der Client-Pod, die zweite App api-unready mit der kaputten Probe der Übung und die beiden Hilfsskripte. Die Befehle apply, scale, get und describe brauchten jeweils unter einer Sekunde, die Aufrufe aus dem Client-Pod 1,5 bis 1,7 s pro Lauf, und der port-forward ohne passenden Pod gab nach 60,3 s mit Exit-Code 1 auf. Der Pod, der nie bereit wurde, war nach 2,5 s Running und blieb 0/1, solange ich ihn beobachtet habe (30 s). Im letzten Test wurde der Pod 1,6 s nach dem Start gelöscht; die nächste Anfrage, bei 8,0 s, fand den Tunnel nicht mehr. Der vollständige Fehler von kubectl in diesem Moment:
an error occurred forwarding 8080 -> 8000: error forwarding port 8000 to pod 76384ad233c3f04a440c8448770a1e70b5f0486b6470954bec93a24e07438900, uid : network namespace for sandbox "76384ad233c3f04a440c8448770a1e70b5f0486b6470954bec93a24e07438900" is closed
Was das Video weglässt
- Der Weg von außen: Ingress und die externe IP eines LoadBalancer-Service (ein eigener Beitrag verfolgt ein
kubectl applyüber den Azure Load Balancer bis zur ersten Antwort). - NodePort-Services und
kubectl proxy. - UDP:
kubectl port-forwardist nur für TCP-Ports implementiert.
Weiterführende Links
- Microsoft-Learn-Modul: Monitor and troubleshoot applications on Azure Kubernetes Service
- Use port forwarding to access applications in a cluster (Kubernetes-Dokumentation)
- EndpointSlices und Service (Kubernetes-Dokumentation)


