• English icon
  • German icon
Erklärvideo

Was nach kubectl apply auf Azure Kubernetes Service passiert

· Reihe: Container auf Azure

azureakskuberneteskubectlreadiness-probedevops

Das Video ist auf Englisch.

Ich habe eine App mit einem einzigen Befehl auf Azure Kubernetes Service deployt: kubectl apply. Eine Sekunde später kam: created. Die erste echte Antwort meiner App kam 34 Sekunden danach.

Dieser Explainer verfolgt genau dieses eine Deployment Sekunde für Sekunde: was „created“ wirklich verspricht, warum ein Pod mit dem Status Running noch keinen Traffic bekommt, wo AKS die öffentliche IP anlegt und warum ein bereiter Pod trotzdem nicht sofort erreichbar ist. Alles unten stammt aus einem echten Lauf auf meinem eigenen AKS-Cluster. Das Video ist auf Englisch.

Das Wichtigste

  • Created heißt gespeichert. kubectl apply kehrt zurück, sobald der API-Server Ihr Manifest geprüft und als gewünschten Zustand gespeichert hat. Die eigentliche Arbeit erledigen danach Controller, Scheduler und Kubelet.
  • Running ist nicht ready. Ein Pod mit dem Status Running hat einen gestarteten Container. Der Service schickt Traffic nur an Pods, deren Readiness-Probe erfolgreich war (READY 1/1). In meinem Lauf: Running nach etwa 4 Sekunden, ready nach 25.
  • Die öffentliche IP liegt in einer Ressourcengruppe, die Sie nicht angelegt haben. Ein Service vom Typ LoadBalancer lässt AKS eine öffentliche IP am Azure Load Balancer des Clusters anlegen, beides in der Node-Ressourcengruppe MC_<Ressourcengruppe>_<Cluster>_<Region>, die AKS gehört und mit dem Cluster gelöscht wird.
  • Ready ist nicht erreichbar. Der Azure Load Balancer prüft jeden Node auf dem Node-Port des Service, standardmäßig alle fünf Sekunden, und leitet erst weiter, wenn diese Prüfung besteht. In meinem Lauf kam die erste Antwort von außen neun Sekunden, nachdem der Pod bereit war.

Was created bedeutet

kubectl schickt Ihr YAML an den API-Server des Clusters, die Rezeption von Kubernetes. Der API-Server prüft die Objekte, speichert sie als gewünschten Zustand und antwortet sofort. Diese Antwort ist das Wort „created“:

> kubectl apply -f k8s/
deployment.apps/aks-api created
service/aks-api-service created

Etwa eine Sekunde, und noch lief nichts. Dann übernehmen Controller: kleine Schleifen, die vergleichen, was Sie angefordert haben, mit dem, was existiert, und die Differenz beheben. Der Controller des Deployments legt ein ReplicaSet an, dessen einzige Aufgabe es ist, die richtige Anzahl Pods laufen zu lassen. Ein Pod ist die Hülle um Ihren Container; das ReplicaSet legt ihn an. Der Scheduler wählt einen Node für den Pod (eine der virtuellen Maschinen des Clusters), und das Kubelet, der Agent auf diesem Node, zieht das Image aus Ihrer Registry und startet den Container. Jeder Name baut auf dem vorherigen auf:

> kubectl get deploy,rs,pods -l app=aks-api
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/aks-api 1/1 1 1 35s
NAME DESIRED CURRENT READY AGE
replicaset.apps/aks-api-74ffd6567d 1 1 1 36s
NAME READY STATUS RESTARTS AGE
pod/aks-api-74ffd6567d-6lht7 1/1 Running 0 36s

Created ist der Bon an der Schiene, nicht der Teller auf dem Tisch.

Running und ready

Direkt nach dem Apply habe ich einen kleinen Watcher gestartet, der nur Änderungen ausgibt. Nach etwa vier Sekunden zeigte der Pod Running 0/1: Der Container war gestartet. Der Service schickte ihm aber noch keinen Traffic, denn ready ist eine eigene Prüfung.

Das Deployment-Manifest des Learn-Moduls enthält eine Readiness-Probe:

readinessProbe:
httpGet:
path: /readyz
port: http # the container port named http = 8000
initialDelaySeconds: 15
periodSeconds: 10

Das Kubelet ruft die App auf /readyz auf. Zwei Zahlen bestimmen den Zeitpunkt: wie lange die Probe vor dem ersten Aufruf wartet (initialDelaySeconds) und wie oft sie danach aufruft (periodSeconds). Solange kein Aufruf erfolgreich ist, ist der Pod nicht bereit, und die Liste der Endpoints des Service (die Pods, an die er Traffic schicken darf) bleibt leer. In meinem Lauf hatte er bis 25 Sekunden null bereite Endpoints, dann einen.

Running heißt, der Prozess ist gestartet. Ready heißt, die Probe hat ja gesagt. Nur bereite Pods bekommen Traffic.

Die öffentliche IP

Der Service im Manifest hat den Typ LoadBalancer:

spec:
type: LoadBalancer
selector:
app: aks-api
version: v1
ports:
- port: 80
targetPort: http

Für diesen Typ fordert AKS bei Azure eine öffentliche IP am Load Balancer des Clusters an und richtet sie auf den Service. Die externe IP stand etwa sechzehn Sekunden auf <pending>:

> kubectl get svc aks-api-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
aks-api-service LoadBalancer 10.0.88.69 203.0.113.10 80:31652/TCP 36s

Ich hatte für diesen Cluster genau eine Ressourcengruppe angelegt. Dort liegt die IP nicht. Sie liegt in der Node-Ressourcengruppe des Clusters, die AKS neben meiner angelegt hat:

> az aks show -g rg-demo -n aks-demo --query nodeResourceGroup -o tsv
MC_rg-demo_aks-demo_westeurope
> az resource list -g MC_rg-demo_aks-demo_westeurope --query "[].type" -o tsv
Microsoft.Network/publicIPAddresses
Microsoft.Network/loadBalancers
Microsoft.ManagedIdentity/userAssignedIdentities
Microsoft.Network/networkSecurityGroups
Microsoft.Network/virtualNetworks
Microsoft.Compute/virtualMachineScaleSets
Microsoft.Network/publicIPAddresses

Zwei öffentliche IPs (eine für meinen Service, eine für den ausgehenden Traffic des Clusters), der Load Balancer, das Scale Set hinter meinem Node, ein Netzwerk, eine Sicherheitsgruppe und eine Identität. AKS löscht diese Gruppe mit dem Cluster; ändern Sie dort nichts von Hand.

Der Weg hinein: Clients rufen Port 80 auf der öffentlichen IP auf. Der Load Balancer gibt den Aufruf an den Node weiter, auf einem Node-Port, den der Service dort geöffnet hat (31652 in 80:31652/TCP). Der Service reicht ihn an den Port http des Pods weiter, 8000.

Ready und erreichbar

Zwischen sechzehn Sekunden (die IP existierte) und fünfundzwanzig (der Pod war bereit) bekam ein Aufruf von außen gar nichts: keinen Fehler, nur ein Timeout. Und auch nachdem der Pod bereit war, dauerte es noch neun Sekunden:

> python watch.py up aks-api --secs 300
...
15.8s svc aks-api-service EXTERNAL-IP 203.0.113.10
18.1s GET http://203.0.113.10/ timed out
25.0s svc aks-api-service ready endpoints 1
34.4s GET http://203.0.113.10/ 200 aks-api-74ffd6567d-6lht7

Der Grund sitzt vor dem Cluster. Der Azure Load Balancer prüft jeden Node auf dem Node-Port des Service, standardmäßig alle fünf Sekunden. Solange hinter diesem Port kein bereiter Pod sitzt, schlägt die Prüfung fehl, und der Load Balancer schickt nichts an den Node. Auch nachdem der Pod bereit ist, muss der Load Balancer die Prüfung erst bestehen sehen.

Es gibt also drei Zeitpunkte: created (1 s), ready (25 s) und erreichbar (34 s). Nur der letzte beantwortet Ihre Nutzer.

Die fünfzehn Sekunden in meinem eigenen Manifest

Das Manifest, das ich deployt habe, stammte direkt aus dem Modul. Seine Readiness-Probe wartet fünfzehn Sekunden, und das Kubelet ruft nur im Zehn-Sekunden-Takt auf, also kam der erste Aufruf nach fünfundzwanzig. Meine kleine App braucht so lange nicht. Ich habe dieselbe App noch einmal mit initialDelaySeconds: 1 deployt:

> python watch.py up aks-api-fast --secs 300
...
11.0s aks-api-fast-7f69b58c5-4jwsz Running 1/1
14.9s svc aks-api-fast-service EXTERNAL-IP 203.0.113.11
18.1s GET http://203.0.113.11/ 200 aks-api-fast-7f69b58c5-4jwsz

Bereit nach elf Sekunden statt fünfundzwanzig, die erste Antwort nach achtzehn statt vierunddreißig. Setzen Sie diese Verzögerung auf das, was Ihre App wirklich braucht, nicht auf eine kopierte Zahl. Braucht eine App wirklich lange zum Starten, ist eine Startup-Probe das sauberere Werkzeug als eine lange Anfangsverzögerung.

Was in 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. Der Cluster hatte einen Node Standard_D2s_v5, den Free-Tarif und die Registry per --attach-acr angebunden, wie in der Übung des Moduls. Die App ist eine kleine Python-API nach dem Vorbild der Übung (Port 8000, /healthz, /readyz); die App der Übung ruft zusätzlich ein Modell in Microsoft Foundry auf, das dieser Lauf weggelassen hat.

  • Mein erster Versuch, den Cluster in Germany West Central anzulegen, schlug fehl: (AKSCapacityHeavyUsage) Creating a new cluster is unavailable at this time in region germanywestcentral. In West Europe klappte es; der Cluster brauchte 3 min 54 s.
  • kubectl events erzählte dieselbe Geschichte aus Sicht des Clusters: Scheduled und Pulling direkt nach dem Apply, das Image gezogen und der Container gestartet etwa vier Sekunden später, und EnsuredLoadBalancer etwa sechzehn Sekunden nach EnsuringLoadBalancer.
  • Der Load Balancer hatte eine Regel für den Service, nach ihm benannt: <hash>-TCP-80.
  • Die Uhr des Watchers startet etwa eine halbe Sekunde, nachdem Apply zurückkam; im Video sind die Zahlen deshalb gerundet.

Was das Video weglässt

  • Requests und Limits, die anderen Service-Typen (ClusterIP, NodePort) und Pod-Zustände, die nie starten (ImagePullBackOff, CrashLoopBackOff): Diese Themen bekommen eigene Videos in dieser Reihe.
  • Die Liveness-Probe: Das Video zu Health Probes bei Container Apps erklärt Liveness und Readiness.
  • externalTrafficPolicy: Local und den Shared-Health-Probe-Modus des AKS Load Balancers (Preview): Beides ändert, wie der Load Balancer Nodes prüft.

Weiterlesen

Video-Transkript (Englisch)

I deployed an app to Azure Kubernetes Service with one command, kubectl apply. One second later, it said: created. The first real answer from my app came thirty-four seconds after that. So what was the cluster doing all that time?

In this video, you'll see what happens between created and the first answer on AKS, with real timings from my cluster. And at the end: the fifteen seconds that were in my own manifest.

What did created actually promise? Most people read created as: my app is running. It promises much less. kubectl sends your YAML to the cluster's API server, the front desk of Kubernetes. The API server checks it, stores it as the desired state, and answers right away. That answer is the word created. I applied the folder with the two files from the Microsoft Learn module, a Deployment and a Service, with kubectl apply. Deployment created, service created, in about one second. Nothing was running yet. So who actually starts your container? Controllers: small loops that compare what you asked for with what exists, and fix the difference. A pod is the wrapper around your container. The Deployment's controller creates a ReplicaSet, whose only job is to keep the right number of pods running. The ReplicaSet creates the pod. The scheduler picks a node for that pod, one of the cluster's virtual machines. And the kubelet, the agent on that node, pulls the image from your registry and starts the container. I showed how images get into a registry in the registry video. To see that chain, I listed the deployment, its replica set and its pod. One of each, and every name builds on the one before. Created is the ticket on the rail, not the plate on the table.

When does a running pod get traffic? Right after the apply, I started a small watcher that prints only what changes, with the seconds since it started. At four seconds, the pod said Running. The container had started. So the pod is running. Will the Service send it traffic? Not yet. Look at the ready column: zero of one. A Service only sends traffic to pods that are ready, and ready is a separate check. The manifest has a readiness probe. The kubelet calls the app on a path, /readyz, on the container's port. The manifest gives that port a name, http. Two numbers decide when: how long the probe waits before its first call, and how often it calls after that. Until a call succeeds, the pod is not ready. Keep those two numbers in mind. The same watcher also counted the Service's ready endpoints, the pods it may send traffic to. Zero, until twenty-five seconds. Then one. I showed what readiness probes can and can't catch in the health probe video. Running means the process started. Ready means the probe said yes. Only ready pods get traffic. This series runs one Azure service at a time, for real. Subscribe to follow the series.

Where does the public IP come from? The Service in my manifest has the type LoadBalancer. For that type, AKS asks Azure for a public IP on the cluster's load balancer, and points it at the Service. Then I asked for the service itself, the way the module shows it. Type LoadBalancer, and an external IP. In my run, the external IP said pending for about sixteen seconds. Then Azure had built it. I created one resource group for this cluster. So where did Azure put that public IP? Not in mine. I asked AKS for the cluster's node resource group, the place where it keeps the cluster's Azure resources. MC_, then my resource group, the cluster's name and the region. Then I listed every resource in that node resource group, by type, to see what Azure had built for my cluster. Two public IPs: one for my Service, one for traffic going out. A load balancer. The scale set behind my node. A network, a security group and an identity. AKS created this second group with the cluster, and it deletes it with the cluster. Look, but don't change things there by hand. The way in tells the rest. Clients call port eighty on the public IP. The load balancer hands the call to the node, on a port the Service opened there, a node port. And the Service passes it on to the pod, on port eight thousand. Your Service asks, Azure builds the door, and AKS owns it.

What happens to a call before the pod is ready? In my run, the public IP existed from sixteen seconds. The pod was ready at twenty-five. In between, my watcher kept calling the IP. What does a call from outside get? Nothing at all. Not even an error. Here's the watcher again, from the moment the IP existed. Timed out at eighteen seconds. Ready at twenty-five. The first answer at thirty-four. The reason sits in front of your cluster. The Azure load balancer checks every node on the Service's node port, every five seconds by default. As long as no ready pod sits behind that port, the check fails, and the load balancer sends nothing to the node. After the pod is ready, the load balancer still has to see that check pass. In my run, that took another nine seconds. So there are three moments: created, ready and reachable. Only the last one answers your users.

To sum it up: created means the API server stored your manifest. Running means the container started. Ready means the readiness probe said yes, and only ready pods get traffic. The public IP comes from Azure, in the node resource group that AKS owns. And reachable means the load balancer has seen your pod too.

And the fifteen seconds in my own manifest? The manifest I deployed came straight from the module. Its readiness probe waits fifteen seconds, and it only calls on a ten-second beat. In my run, its first call landed at twenty-five. My little app doesn't need that long. So I deployed the same app again, with the first call after one second, and ran the watcher again. Ready at eleven seconds instead of twenty-five. The first answer at eighteen instead of thirty-four. Set that delay to what your app really needs, not to a number you copied.

The video on screen now shows what a readiness probe can catch, and what it can't. 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