• English icon
  • German icon
Erklärvideo

Einen Kubernetes-Service auf AKS testen (nicht mit port-forward)

· Reihe: Container auf Azure

azureakskuberneteskubectlnetworkingtroubleshooting

Das Video ist auf Englisch.

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: von kubectl describe service listet 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: v1
kind: Service
metadata:
name: api-service
spec:
type: ClusterIP
selector:
app: api
ports:
- name: http
port: 80
targetPort: 8000
protocol: 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 /healthz
0.0s $ kubectl port-forward service/api-service 8080:80
0.5s kubectl: Forwarding from 127.0.0.1:8080 -> 8000
0.5s kubectl: Forwarding from [::1]:8080 -> 8000
1.2s kubectl: Handling connection for 8080
1.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 3
deployment.apps/api-deployment scaled
> python pf.py service/api-service /api/info --n 6
0.0s $ kubectl port-forward service/api-service 8080:80
0.6s kubectl: Forwarding from 127.0.0.1:8080 -> 8000
0.6s kubectl: Forwarding from [::1]:8080 -> 8000
1.1s kubectl: Handling connection for 8080
1.3s GET /api/info -> 200 pod started 13:58:45.05
1.3s kubectl: Handling connection for 8080
1.4s GET /api/info -> 200 pod started 13:58:45.05
1.4s kubectl: Handling connection for 8080
1.5s GET /api/info -> 200 pod started 13:58:45.05
1.5s kubectl: Handling connection for 8080
1.6s GET /api/info -> 200 pod started 13:58:45.05
1.6s kubectl: Handling connection for 8080
1.7s GET /api/info -> 200 pod started 13:58:45.05
1.7s kubectl: Handling connection for 8080
1.8s GET /api/info -> 200 pod started 13:58:45.05
2.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 6
1 200 pod started 14:16:35.84
2 200 pod started 14:16:35.66
3 200 pod started 14:16:35.84
4 200 pod started 14:16:35.66
5 200 pod started 13:58:45.05
6 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-path
port: 8000
initialDelaySeconds: 5
periodSeconds: 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.yaml
deployment.apps/api-unready created
service/api-unready created
> python pods.py --app api-unready --secs 30
0.3s api-unready-64cc88bb49-w6vpd ContainerCreating 0/1 restarts 0
2.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-unready
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
api-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-unready
Selector: app=api-unready
Type: ClusterIP
Port: http 80/TCP
TargetPort: 8000/TCP
Endpoints:
Wo Sie nachsehenWas 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/info
0.0s $ kubectl port-forward service/api-unready 8080:80
0.7s kubectl: Forwarding from 127.0.0.1:8080 -> 8000
0.7s kubectl: Forwarding from [::1]:8080 -> 8000
1.3s kubectl: Handling connection for 8080
1.5s GET /api/info -> 200 pod started 14:19:54.66
2.5s forward stopped
> kubectl exec client -- python /c/call.py http://api-unready/api/info 2
1 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-service
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
api-service-mvjnn IPv4 <unset> <unset> 20m
> kubectl exec client -- python /c/call.py http://api-service/api/info 2
1 FAIL URLError <urlopen error [Errno 111] Connection refused>
2 FAIL URLError <urlopen error [Errno 111] Connection refused>
> python pf.py service/api-service /healthz
0.0s $ kubectl port-forward service/api-service 8080:80
60.3s kubectl: error: timed out waiting for the condition
61.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> = none
kubectl get endpointslices -l kubernetes.io/service-name=api-service
# 2. Which of them are ready? The Endpoints line
kubectl describe service api-service
# 3. Call it the way your other apps do: from a pod, by its name
kubectl exec client -- python /c/call.py http://api-service/api/info 6
  1. 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.
  2. 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.
  3. 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 --kill
0.0s $ kubectl port-forward service/api-service 8080:80
0.6s kubectl: Forwarding from 127.0.0.1:8080 -> 8000
1.2s GET /api/info -> 200 pod started 13:58:45.05
1.4s GET /api/info -> 200 pod started 13:58:45.05
1.6s deleting the pod behind the forward: api-deployment-695945c854-nx6qj
8.0s kubectl: error: lost connection to pod
8.0s GET /api/info -> FAIL ConnectionResetError [WinError 10054] An existing connection was forcibly closed
13.1s GET /api/info -> FAIL URLError <urlopen error [WinError 10061] No connection could be made
18.2s GET /api/info -> FAIL URLError <urlopen error [WinError 10061] No connection could be made
20.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 8
14:20:43 200 pod started 14:16:35.66
14:20:44 200 pod started 14:16:35.84
14:20:45 200 pod started 14:20:34.35
14:20:46 200 pod started 14:16:35.66
14:20:47 200 pod started 14:20:34.35
14:20:48 200 pod started 14:16:35.66
14:20:49 200 pod started 14:20:34.35
14: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-forward ist nur für TCP-Ports implementiert.

Weiterführende Links

Video-Transkript (Englisch)

I tested a Service on Azure Kubernetes Service with kubectl port-forward, the way the Microsoft Learn module does. 200, OK. Then a pod inside the cluster called the same Service. Connection refused. Twice.

In this video, you'll see what port-forward really connects to, and how to test a Service for real. At the end: what happens to an open port-forward when its pod goes away.

Where does port-forward actually go? In the Microsoft Learn module's exercise, a small Python API runs on Azure Kubernetes Service, behind a Service: a stable address that spreads calls over the app's pods. The exercise checks it with port-forward: kubectl opens local port 8080 and forwards it to the Service's port 80. Forwarding. And a request to localhost answered 200, healthy. So I scaled the app to three replicas with kubectl scale: three pods behind the same Service, each with its own start time. Scaled. Then I sent six requests through port-forward. The app answers with its start time, so every answer says which pod replied. How many pods answered the six requests? One. All six answers came from the pod that started at 13:58. Then the same six calls from inside the cluster, from a client pod I had started there: a tiny Python script that calls the Service by its name. Three different pods. The Service spread them out. Here's why. kubectl doesn't send your request to the Service. It opens a tunnel through the cluster's API server, the front door for every kubectl command, to the kubelet, the agent on the node, and from there into one pod. It reads the Service's selector, the label rule that says which pods belong to it, and picks one matching pod. That's all it uses the Service for. port-forward tests one pod. The Service is everything in front of the pods.

What if the pod isn't ready? A readiness probe is the check that decides whether a pod gets traffic. So I deployed a second app with its own Service, and with the exercise's broken probe: a check that asks for a path that doesn't exist. Created, both. I watched its pod with a small script. Running, 0/1. It runs, but it never gets ready. Then I asked for its endpoint slice, the list of addresses behind the Service. Does it show up in the endpoint slice? Yes. One address, the pod's. So I asked the Service itself, with kubectl describe. Endpoints: empty. The slice lists every pod that matches the labels, ready or not, and marks the ones that aren't. The Endpoints line is stricter: ready pods only, the ones that get traffic. Then the same two paths as before. port-forward to the new Service, with the same command. 200, OK. And through the Service's name, from the client pod inside the cluster? Connection refused, twice. port-forward doesn't care whether a pod is ready. It only asks whether the pod is running. The readiness probe exists to keep traffic away from a pod like this, and port-forward walks right past it. This series runs one Azure service at a time, for real. Subscribe to follow the series.

So does port-forward ignore the Service completely? Not quite. The exercise has a second break: it changes the Service's selector to a label that no pod has. The pods are healthy and ready; the Service just stops looking for them. Configured. Then I listed the Service's endpoint slice. Unset. No pod matches, so there's no address at all. From inside the cluster, the client got the same as before. Refused. And port-forward, with the same command as the exercise? It waited a minute for a pod to pick, then gave up: timed out waiting for the condition. Both failed, for different reasons. The Service had no endpoint, and port-forward had no pod to pick. Only the endpoint slice told me why.

So how do you test a Service for real? One: the endpoint slices, by the Service's name label. Unset means no pod matches: check the labels on both sides. An address there means a pod matches, not that it's ready. Two: describe the Service. Its Endpoints line lists only the ready ones, the ones that get traffic. Empty with pods running means a readiness problem: look at the pod's events. Three: call it from inside the cluster, from a pod, by the Service's name. Mine was the small Python client; any pod with curl does the same. That's the path your other apps use: the cluster's DNS turns the name into the Service's address, and the Service spreads the call over every ready pod. If those three look right and the call still fails, look at the ports: the Service's port, and the targetPort, the port the container listens on. Keep port-forward for what it is: a quick, private line to one pod, for TCP only, no UDP. Great for looking at one pod from your laptop. Not proof that the Service works. And because the tunnel goes through the API server, it also skips the cluster's own network rules. Who may use it is worth restricting.

To sum it up: port-forward uses the Service only to find one pod, and then talks to that pod directly, ready or not. The Service sends calls only to ready pods, and spreads them. To test the Service, call it from inside the cluster.

And that open port-forward? What happens when its one pod goes away? I opened it again, got two answers from the pod that started at 13:58, and then deleted exactly that pod. Lost connection to pod. The next requests failed, and kubectl had quit. The forward had died with its pod. It doesn't move on to another one, even though two more pods were still behind the same Service. Meanwhile, the client inside the cluster kept calling the Service. 200, every second, from the other pods, and from the new one that replaced the deleted pod.

The path from outside, through the load balancer to a ready pod, is in the video on screen: from kubectl apply to the first answer. If this was useful, subscribe for more practical .NET and Azure.

Brauchen Sie das in Ihrer eigenen Azure-Umgebung?

Ich entwickle, migriere und betreibe .NET-Plattformen auf Azure, mit Azure DevOps und Terraform. Wenn Ihr Team das sauber aufsetzen möchte, sehen Sie sich mein Angebot an.

Zu den Leistungen