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.
portist das, was Aufrufer verwenden,targetPortder 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:
# Servicespec:type: LoadBalancerselector:app: webversion: v1ports:- port: 80targetPort: http---# Deployment: das Pod-Template (die kaputte Version)template:metadata:labels:app: web
Alles sah gut aus:
> kubectl get pods,svc -l app=webNAME READY STATUS RESTARTS AGEpod/web-6555d997fb-vpl2j 1/1 Running 0 53spod/web-6555d997fb-xhnn2 1/1 Running 0 53sNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEservice/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=v1Type: LoadBalancer...TargetPort: http/TCPNodePort: http 31713/TCPEndpoints:...
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-labelsNAME ... LABELSweb-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 18021.5s web-654b6b7b85-sjwzj Running 1/122.0s svc web ready endpoints 132.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 Containerports:- name: httpcontainerPort: 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 32.4s GET http://203.0.113.12/ connection refused7.7s GET http://203.0.113.12/ connection refused12.8s GET http://203.0.113.12/ connection refused20.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:
| Typ | Was er hinzufügt | Erreichbar von |
|---|---|---|
| ClusterIP (Standard) | eine interne Adresse und einen DNS-Namen | im Cluster |
| NodePort | einen Port auf jedem Node, 30000–32767 | aus dem Netzwerk der Nodes |
| LoadBalancer | einen Azure Load Balancer mit öffentlicher IP vor den Node-Ports | aus dem Internet |
Ich habe einen ClusterIP- und einen NodePort-Service für dieselben Pods angelegt:
> kubectl get svcNAME TYPE EXTERNAL-IP PORT(S)web LoadBalancer 203.0.113.12 80:31713/TCPweb-internal ClusterIP <none> 80/TCPweb-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-internal200 {"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 wideNAME 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=webNAME ADDRESSTYPE PORTS ENDPOINTS AGEweb-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 webzeigte 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: 8080kam 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: Localund 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 nutztekubectl execin den eigenen Pod der App.





