• English icon
  • German icon
Erklärvideo

Meine Pods laufen. Warum antwortet der Service nicht?

· Reihe: Container auf Azure

azureakskuberneteskubernetes-servicekubectldevops

Das Video ist auf Englisch.

Beide Pods meldeten Running, eins von eins bereit. Der Service hatte eine öffentliche IP. Und jeder Aufruf an diese IP lief in einen Timeout: zehn hintereinander, ohne einen einzigen Fehler in irgendeinem Status.

Dieser Explainer macht einen Kubernetes Service auf Azure Kubernetes Service zweimal kaputt, auf meinem eigenen Cluster, und zeigt, was einen Service wirklich mit seinen Pods verbindet: die Labels, die er auswählt, der Port, den er aufruft, und die Adresse, unter der Sie ihn erreichen. Das Video ist auf Englisch.

Das Wichtigste

  • Ein Service folgt Labels, nicht Namen. Er schaut nie auf Ihr Deployment. Sein Selector ist eine Liste von Labels, und nur Pods, die jedes davon mit demselben Wert tragen, kommen auf seine Endpoint-Liste.
  • Ein fehlendes Label bleibt stumm. Passt kein Pod, ist die Endpoint-Liste leer: Die Pods sind Running und bereit, der Service hat seine IP, und keine Warnung und kein Event sagt Ihnen etwas.
  • Den Target-Port prüft niemand. port ist das, was Aufrufer verwenden, targetPort der Port, auf dem der Pod lauscht. Ein falscher Target-Port lässt die Pods auf der Liste, und Aufrufe bekommen erst „connection refused“, dann „timed out“, sobald der Health-Check des Azure Load Balancers den Node aufgibt.
  • Im Cluster den Namen, von außen den Load Balancer. ClusterIP liefert einen Namen und eine interne Adresse, NodePort einen Port auf jedem Node (der auf AKS keine öffentliche IP hat), LoadBalancer die öffentliche IP.

Labels, nicht Namen

Ein Service ist eine feste Adresse vor Ihren Pods und gibt jeden Aufruf an einen von ihnen weiter. Die meisten stellen sich vor, er zeige auf ein Deployment. Tut er nicht: Er hat einen Selector, eine kurze Liste von Labels. Labels sind Namensschilder aus Schlüssel und Wert, die das Pod-Template jedem Pod anheftet. Kubernetes vergleicht die Labels jedes Pods mit dem Selector; ein Pod, der alle trägt, kommt auf die Endpoint-Liste des Service (seine Adresse und sein Port), und der Service schickt Aufrufe nur an die bereiten Pods auf dieser Liste.

Für den Lauf habe ich den Service aus dem Microsoft-Learn-Modul genommen, der zwei Labels auswählt, app und version, und ihn vor mein eigenes Deployment web gestellt, dessen Pod-Template nur app: web setzt:

# Service
spec:
type: LoadBalancer
selector:
app: web
version: v1
ports:
- port: 80
targetPort: http
---
# Deployment: das Pod-Template (die kaputte Version)
template:
metadata:
labels:
app: web

Alles sah gut aus:

> kubectl get pods,svc -l app=web
NAME READY STATUS RESTARTS AGE
pod/web-6555d997fb-vpl2j 1/1 Running 0 53s
pod/web-6555d997fb-xhnn2 1/1 Running 0 53s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/web LoadBalancer 10.0.160.151 203.0.113.12 80:31713/TCP 53s

Nur die Beschreibung des Service zeigte das Problem:

> kubectl describe svc web
...
Selector: app=web,version=v1
Type: LoadBalancer
...
TargetPort: http/TCP
NodePort: http 31713/TCP
Endpoints:
...

Das Learn-Modul zeigt für diesen Fall Endpoints: <none>; mit kubectl 1.36 blieb die Zeile einfach leer. Die Labels der Pods:

> kubectl get pods -l app=web --show-labels
NAME ... LABELS
web-6555d997fb-vpl2j ... app=web,pod-template-hash=6555d997fb,...
web-6555d997fb-xhnn2 ... app=web,pod-template-hash=6555d997fb,...

Kein version. Ein fehlendes Label, und keiner der beiden Pods kam auf die Liste. Die Korrektur ist eine Zeile im Pod-Template, version: v1. Ein geändertes Template bedeutet neue Pods: Das Deployment rollte zwei neue mit beiden Labels aus und entfernte die alten:

> python watch.py up web --secs 180
21.5s web-654b6b7b85-sjwzj Running 1/1
22.0s svc web ready endpoints 1
32.0s GET http://203.0.113.12/ 200 web-654b6b7b85-sjwzj

Zwei Ports

Ein Service hat zwei Ports. port ist das, was Aufrufer verwenden (80). targetPort ist der Port, auf dem der Pod lauscht. In meinem Manifest ist der Target-Port ein Name, http, und der Container gibt seinem Port genau diesen Namen:

# Deployment: der Container
ports:
- name: http
containerPort: 8000

So folgt der Service dem Container. Dann habe ich es absichtlich kaputt gemacht: targetPort: 8080, eine Zahl aus vielen Beispielen, auch aus dem des Moduls, bei einer App, die auf 8000 lauscht. Die Labels passten weiter, also blieben beide Pods auf der Liste. Vier Aufrufe im Abstand von drei Sekunden:

> python watch.py get web --tries 4 --gap 3
2.4s GET http://203.0.113.12/ connection refused
7.7s GET http://203.0.113.12/ connection refused
12.8s GET http://203.0.113.12/ connection refused
20.8s GET http://203.0.113.12/ timed out

Refused heißt: Der Aufruf hat einen Pod erreicht, und auf 8080 lauschte niemand; Kubernetes prüft diesen Port nie. Der Timeout kommt vom Azure Load Balancer. Ihre Pods laufen auf Nodes, den virtuellen Maschinen des Clusters; für einen Service mit öffentlicher IP öffnet jeder Node einen eigenen Port, den Node-Port, und reicht weiter, was dort ankommt, an den Target-Port. Der Load Balancer prüft diesen Node-Port auf jedem Node, standardmäßig alle fünf Sekunden (mit externalTrafficPolicy: Cluster, dem Standard-Probe-Modus von AKS). Sein Check landet auf demselben falschen Port und schlägt fehl, und nach zwei Fehlschlägen schickt der Load Balancer dem Node nichts mehr.

Derselbe Check erklärt den ersten Fehler: Bei leerer Endpoint-Liste schlug auch der Check fehl, deshalb liefen diese Aufrufe nur in Timeouts. Und nach der Label-Korrektur kam die erste Antwort zehn Sekunden nachdem der Pod bereit war: Der Check musste erst wieder bestehen. Zurück zu targetPort: http, und nach etwa acht Sekunden antwortete die App wieder.

Geben Sie dem Target-Port den Namen des Container-Ports, dann folgt der Service Ihrem Container.

Welche Adresse

Der Service-Typ entscheidet, wer ihn erreicht, und die drei Typen bauen aufeinander auf:

TypWas er hinzufügtErreichbar von
ClusterIP (Standard)eine interne Adresse und einen DNS-Namenim Cluster
NodePorteinen Port auf jedem Node, 30000–32767aus dem Netzwerk der Nodes
LoadBalancereinen Azure Load Balancer mit öffentlicher IP vor den Node-Portsaus dem Internet

Ich habe einen ClusterIP- und einen NodePort-Service für dieselben Pods angelegt:

> kubectl get svc
NAME TYPE EXTERNAL-IP PORT(S)
web LoadBalancer 203.0.113.12 80:31713/TCP
web-internal ClusterIP <none> 80/TCP
web-np NodePort <none> 80:30412/TCP

Im Cluster rufen Sie einen Service über seinen Namen auf. Aus einem meiner eigenen Pods:

> kubectl exec deploy/web -- python get.py http://web-internal
200 {"pod": "web-654b6b7b85-sjwzj", "version": "v1"}

Der volle Name web-internal.default.svc.cluster.local antwortete ebenfalls. Kein Port in der Adresse heißt 80, der Port des Service. Das Beispiel im Modul setzt den Target-Port in die Adresse (…svc.cluster.local:8080); Aufrufer verwenden immer den Port des Service.

Das Modul sagt außerdem: Einen NodePort erreicht man von außen über die IP des Nodes und den Node-Port. Mein Node hatte keine öffentliche Adresse:

> kubectl get nodes -o wide
NAME INTERNAL-IP EXTERNAL-IP ...
aks-nodepool1-24214868-vmss000000 10.224.0.4 <none> ...

AKS-Nodes bekommen keine öffentliche IP, solange Sie es nicht einschalten (--enable-node-public-ip). Ein Node-Port funktioniert innerhalb des Netzwerks, und er ist die Tür, die der Load Balancer benutzt. Im Cluster rufen Sie den Namen auf, von außen den Load Balancer. Für öffentliche HTTP-Anwendungen empfiehlt Microsoft einen Ingress-Controller oder die Gateway API vor ClusterIP-Services statt einer öffentlichen IP pro Service.

Die eine Zeile, die man zuerst lesen sollte

Die Endpoints-Zeile von kubectl describe service sagt nicht nur, ob die Liste leer ist; sie zeigt auch den Port. Als mein Target-Port falsch war, habe ich dieselbe Liste direkt abgefragt, dort, wo Kubernetes sie ablegt, in Endpoint-Slices:

> kubectl get endpointslices -l kubernetes.io/service-name=web
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
web-cdxkl IPv4 8080 10.244.0.154,10.244.0.165 110s

Zwei Pod-Adressen und Port 8080 für eine App auf 8000. Leere Liste: Labels prüfen. Falscher Port: Target-Port prüfen. Eine Zeile, beide Fehler.

Was bei meinem Lauf passiert ist

Getestet am 26. Oktober 2026 in meinem eigenen Azure-Abonnement (AKS 1.36 in westeurope, Azure CLI 2.88.0, kubectl 1.36); danach wurde alles gelöscht. Ein AKS-Cluster diente vier Videos dieser Serie; diese Seite verwendet seine Service-Schritte. Der Cluster hatte einen Node Standard_D2s_v5 und den Free-Tarif; die App ist eine kleine Python-API nach dem Vorbild der Übung im Modul (Port 8000, /healthz, /readyz), ohne das Modell in Microsoft Foundry aus der Übung.

  • Mit dem fehlenden Label stoppte der Watcher nach „10 failed GETs in a row while the pods are ready“ bei 52 Sekunden; die Pods waren seit 22,8 Sekunden bereit.
  • kubectl describe svc web zeigte nur zwei Normal-Events (EnsuringLoadBalancer, EnsuredLoadBalancer): nichts über den leeren Selector-Treffer.
  • Nach der Label-Korrektur rollte das Deployment Pods mit dem neuen Template-Hash aus (654b6b7b85); der erste neue Pod war nach 21,5 Sekunden bereit und nach 22,0 auf der Endpoint-Liste.
  • Mit targetPort: 8080 kam der Wechsel von refused zu timed out zwischen etwa 13 und 16 Sekunden nach dem Start des Watchers, der direkt nach dem Apply lief.

Was das Video weglässt

  • ExternalName-Services, Network Policies und Fehler in kube-proxy oder DNS (der Troubleshooting-Leitfaden von Microsoft deckt sie ab).
  • externalTrafficPolicy: Local und den Shared-Health-Probe-Modus des AKS Load Balancers (Vorschau), die ändern, wie der Load Balancer Nodes prüft.
  • Debug-Pods (kubectl run) und ephemere Debug-Container; der Lauf nutzte kubectl exec in den eigenen Pod der App.

Weiterführende Links

Video-Transkript (Englisch)

Both pods said Running, one of one ready. The Service had a public IP. And every call I made to that IP timed out. Ten calls in a row, and not one error in any status. So where did my requests go?

In this video, you'll see how a Service on AKS finds its pods, which port it calls, and which address you call. And at the end: the one line I should have read first.

How does a Service find its pods? A Service is one stable address in front of your pods, and it passes each call on to one of them. Most people picture a Service pointing at a Deployment, the object that creates your pods, the small wrappers your containers run in. It doesn't. A Service never looks at the Deployment at all. It has a selector, a short list of labels. Labels are name tags, a key and a value, that the pod template, the stencil for new pods, puts on every pod. Kubernetes compares each pod's tags with the selector. A pod that carries all of them, with the same values, goes on the Service's endpoint list: its address and its port. The Service only sends calls to the ready pods on that list. For my run, I took the Service from the Microsoft Learn module, which selects two labels, app and version, and put it in front of my own deployment, called web. Then I asked Kubernetes for everything with the label app equals web: the pods and the service. Two pods, both one of one ready: their container passed its readiness check. The Service, with its public IP. Everything looks fine. So both pods are ready. How many of them get traffic from the Service? Zero. The endpoint list was empty. I asked Kubernetes to describe the service: its selector, its ports, and the endpoints it found. The selector wants app equals web and version v1. And the endpoints line: empty. No warning, no event, nothing red. Then I listed the two pods again, this time with every label they carry. App equals web, and a few labels Kubernetes added on its own. No version. One tag missing, and neither pod made the list. The fix is one line in the deployment's pod template: version v1. A changed template means new pods, so the deployment rolled out two new ones with both tags, and removed the old ones. I applied the fixed deployment and started a small script of mine. It watches the pods and the Service, calls the public IP, and prints only what changes. At twenty-two seconds, the first new pod was ready and on the list. At thirty-two, the first answer. A Service doesn't follow the deployment. It follows labels, and every label must match.

Which port does the Service call on your pod? A Service has two ports. The first, port, is what callers use: eighty. The second, target port, is where the pod listens. In my manifest, the target port isn't a number. It's a name: http. The container gives its port that name, and the port is eight thousand. So the Service follows the container. Then I broke it on purpose. I set the target port to eighty eighty, a number you see in many examples, the module's too. My app listens on eight thousand. The labels still match, so both pods stay on the list. What does a call get now? Connection refused. Three times. I called the public IP four times, three seconds apart, with the same script. Refused, refused, refused. Then: timed out. Refused means the call reached a pod, and nothing was listening on eighty eighty. Kubernetes never checks that port. This series runs one Azure service at a time, for real. Subscribe to follow the series. So why the timeout? Your pods run on nodes, the virtual machines of the cluster. For a Service with a public IP, like mine, every node opens a port of its own, the node port, and passes what arrives there on to the target port. The Azure load balancer checks that node port on every node, every five seconds. Its check lands on the same wrong port and fails. After two misses, the load balancer stops sending anything to the node. I showed that check in the last video. The same check explains my first break. With an empty list, the check failed too, so those calls only timed out. And after the fix, the first answer came ten seconds after the pod was ready: the check had to pass again first. Back to the name http, and eight seconds later, the app answered again. Give the target port the container port's name, and the Service follows your container.

Which address do you actually call? The Service type decides who can reach it, and the three types build on each other. ClusterIP, the default, gives the Service an internal address and a name, inside the cluster only. Every other type keeps that name. NodePort adds the node port on every node, a number between thirty thousand and thirty-two thousand seven hundred sixty-seven. And LoadBalancer adds an Azure load balancer with a public IP in front of those node ports. I added a ClusterIP and a NodePort service for the same pods, next to the LoadBalancer, and listed all services. Only the LoadBalancer has an external IP. The NodePort service got port thirty thousand four hundred twelve. Inside the cluster, you call a Service by its name. From inside one of my own pods, I called the ClusterIP service by its short name, with no port. Two hundred, from one of the pods behind it. The full name answered too: web-internal, then the namespace, default, then svc dot cluster dot local. No port in the address means eighty, the Service's port. Don't put the target port in the address, like the module's example does with eighty eighty. Callers use the Service's port. Now the module's advice for NodePort: from outside, call the node's IP and the node port. So what is my node's public IP? There isn't one. I asked Kubernetes for my node, with its internal and its external address. Internal: ten dot two two four dot zero dot four. External: none. AKS nodes get no public IP unless you turn it on. A node port works inside the network, and it's the door the load balancer uses. From the internet, you call the load balancer's IP. Inside the cluster, call the name. From outside, call the load balancer.

To sum it up: a Service finds pods by their labels, and every label in the selector must match. It calls them on the target port, and Kubernetes never checks that your app listens there. And on AKS, you call it by its name inside the cluster, or on the load balancer's IP from outside.

And the one line I should have read first? The endpoints line of kubectl describe service. It doesn't only tell you whether the list is empty. It shows the port. When my target port was wrong, I asked for that list directly. Kubernetes keeps it in endpoint slices, one set per Service. Two pod addresses, and port eighty eighty. My app listens on eight thousand. Empty list: check the labels. Wrong port: check the target port. One line, both mistakes.

The video on screen now shows what happens between kubectl apply and the first answer, second by second. 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