2. Pods
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Code complet : code/kubernetes/l02, avec ses commandes dans l02/run.sh. Chaque sortie ci-dessous vient d’une exécution de ce script ; check.sh les compare avec expected.
Les commandes sont montrées telles que le script les exécute, en Bash. Sous Windows, exécutez-les dans Git Bash ou WSL, ou dans PowerShell sans les filtres | grep, | sort et | head, qui ne font que raccourcir la sortie.
Un pod est la plus petite chose que Kubernetes ordonnance : un ou plusieurs conteneurs qui partagent une adresse réseau, peuvent partager des volumes, et sont placés ensemble sur le même nœud. La plupart des pods contiennent un seul conteneur, et on crée rarement des pods à la main ; un Deployment le fait pour vous (leçon 3). Mais chaque réglage qui décide si votre application démarre, reçoit du trafic et survit se trouve dans le modèle de pod : cette leçon regarde donc d’abord les pods seuls.
Les images
Section intitulée « Les images »Cette leçon et les deux suivantes exécutent les deux API construites dans la leçon 4 du cours Conteneurs WSL : une API minimale ASP.NET Core et une API Spring Boot WebFlux, qui répondent toutes deux à GET / sur le port 8080 par un petit document JSON. images.sh les construit à partir des Containerfiles de ce cours, récupère deux images auxiliaires, et charge le tout dans le nœud kind. Sous Windows, lancez-le depuis Git Bash ou WSL.
bash images.shLes manifestes utilisent imagePullPolicy: Never : le nœud doit déjà avoir l’image, et Kubernetes n’essaie jamais de registre. Avec un registre, vous pousseriez les images et garderiez la valeur par défaut, IfNotPresent pour une image étiquetée.
Un pod et son cycle de vie
Section intitulée « Un pod et son cycle de vie »l02/pod.yaml exécute l’API C# avec des ressources et deux sondes, expliquées dans les sections suivantes :
# Leçon 2 : un pod qui exécute l'API C# construite dans le cours Conteneurs WSL, avec requests, limits et deux sondes.apiVersion: v1kind: Podmetadata: name: csharp-api labels: app: csharp-apispec: containers: - name: api image: csharp-api:1.0 # kind charge l'image dans le nœud : ne jamais essayer un registre imagePullPolicy: Never ports: - containerPort: 8080 resources: requests: cpu: 100m memory: 64Mi limits: memory: 256Mi # N'envoyer du trafic qu'une fois que GET / répond readinessProbe: httpGet: path: / port: 8080 periodSeconds: 5 # Redémarrer le conteneur si GET / ne répond plus livenessProbe: httpGet: path: / port: 8080 periodSeconds: 10 failureThreshold: 3> kubectl apply -n l02 -f l02/pod.yamlpod/csharp-api created> kubectl wait -n l02 --for=condition=Ready pod/csharp-api --timeout=120spod/csharp-api condition met> kubectl get pod -n l02 csharp-apiNAME READY STATUS RESTARTS AGEcsharp-api 1/1 Running 0 1sSTATUS résume deux choses distinctes. La phase du pod vaut Pending, Running, Succeeded, Failed ou Unknown. Les conditions du pod disent quelles étapes il a franchies, et kubectl wait attend l’une d’elles :
> kubectl get pod -n l02 csharp-api -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}'PodReadyToStartContainers=TrueInitialized=TrueReady=TrueContainersReady=TruePodScheduled=TrueDans l’ordre où elles arrivent : l’ordonnanceur a choisi un nœud (PodScheduled), le kubelet a créé le bac à sable et le réseau du pod (PodReadyToStartContainers), les conteneurs init ont terminé (Initialized), chaque conteneur a passé sa sonde readiness (ContainersReady), et le pod est donc Ready. Seul un pod Ready reçoit du trafic d’un Service.
La sortie standard du conteneur est son journal, comme avec docker logs. ASP.NET Core écrit ses lignes de démarrage :
> kubectl logs -n l02 csharp-apiinfo: Microsoft.Hosting.Lifetime[14] Now listening on: http://[::]:8080info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down.info: Microsoft.Hosting.Lifetime[0] Hosting environment: Productioninfo: Microsoft.Hosting.Lifetime[0] Content root path: /appEt les événements racontent ce qu’a fait le kubelet, c’est le premier endroit où regarder quand un pod ne démarre pas :
> kubectl get events -n l02 --field-selector involvedObject.name=csharp-api,type=Normal -o custom-columns=TYPE:.type,REASON:.reason,MESSAGE:.message --sort-by=.metadata.creationTimestampTYPE REASON MESSAGENormal Scheduled Successfully assigned l02/csharp-api to learn-k8s-control-planeNormal Pulled Container image "csharp-api:1.0" already present on machine and can be accessed by the podNormal Created Container createdNormal Started Container startedLors de certaines exécutions, un Warning apparaît aussi, avec l’adresse IP du pod : Readiness probe failed: Get "http://10.244.0.44:8080/": dial tcp 10.244.0.44:8080: connect: connection refused. C’est pour cette raison que la commande ci-dessus ne garde que les événements Normal. La première sonde est partie avant que Kestrel écoute. C’est sans conséquence, et cela montre pourquoi la readiness compte : le processus tourne, mais l’application ne sert pas encore.
Trois sondes
Section intitulée « Trois sondes »Une sonde (probe) est une vérification que le kubelet exécute contre un conteneur, en HTTP, en TCP, en gRPC ou par une commande. Kubernetes en a trois sortes, et elles répondent à trois questions différentes :
| Sonde | Question | En cas d’échec | ASP.NET Core | Spring Boot |
|---|---|---|---|---|
| liveness | Le processus est-il bloqué sans espoir ? | le kubelet redémarre le conteneur | un health check étiqueté live avec MapHealthChecks |
/actuator/health/liveness |
| readiness | Peut-il prendre des requêtes maintenant ? | le pod est retiré des Services, rien ne redémarre | un check étiqueté ready, dépendances comprises |
/actuator/health/readiness |
| startup | A-t-il fini de démarrer ? | liveness et readiness attendent ; après failureThreshold échecs, le conteneur est redémarré |
L’Actuator de Spring Boot expose les deux groupes quand il tourne sur Kubernetes. ASP.NET Core vous laisse faire la séparation. Les deux API du cours n’ont ni l’un ni l’autre, donc les sondes appellent ici GET /. Cela suffit pour voir le mécanisme, mais un vrai point de terminaison liveness ne devrait pas vérifier une base de données : si la base tombe, redémarrer tous les pods ne la fait pas revenir.
Une sonde liveness qui échoue
Section intitulée « Une sonde liveness qui échoue »l02/liveness-fail.yaml sonde /healthz, un chemin que l’API n’a pas, toutes les 2 secondes, et abandonne après 2 échecs :
livenessProbe: httpGet: path: /healthz port: 8080 periodSeconds: 2 failureThreshold: 2> kubectl get pod -n l02 liveness-failNAME READY STATUS RESTARTS AGEliveness-fail 1/1 Running 1 (2s ago) 6s> kubectl get events -n l02 --field-selector involvedObject.name=liveness-fail,type=Warning -o custom-columns=TYPE:.type,REASON:.reason,MESSAGE:.message --no-headers | grep -v 'connection refused' | sort -uWarning Unhealthy Liveness probe failed: HTTP probe failed with statuscode: 404> kubectl get events -n l02 --field-selector involvedObject.name=liveness-fail,reason=Killing -o custom-columns=TYPE:.type,REASON:.reason,MESSAGE:.message --no-headers | sort -uNormal Killing Container api failed liveness probe, will be restartedUne sonde HTTP réussit sur tout statut de 200 à 399 ; le 404 est un échec, avec le message construit dans le http.go du kubelet et enregistré comme événement par prober.go. L’état précédent du conteneur est moins évident :
> kubectl get pod -n l02 liveness-fail -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason} {.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'Completed 0Le kubelet arrête un conteneur avec SIGTERM, et ASP.NET Core le traite comme un arrêt propre : le conteneur tué est donc sorti avec le code 0. Un compteur de redémarrages qui ne cesse de grimper avec la raison Completed est souvent une sonde liveness, pas un plantage. Après quelques redémarrages, le kubelet attend de plus en plus longtemps avant le suivant : sur le cluster de l’auteur, STATUS affichait CrashLoopBackOff dès le deuxième redémarrage, 16 secondes après la création du pod.
Une sonde readiness qui échoue
Section intitulée « Une sonde readiness qui échoue »La même erreur sur la sonde readiness, dans l02/readiness-fail.yaml, donne un autre résultat :
> kubectl get pod -n l02 readiness-failNAME READY STATUS RESTARTS AGEreadiness-fail 0/1 Running 0 1s> kubectl get pod -n l02 readiness-fail -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}'PodReadyToStartContainers=TrueInitialized=TrueReady=FalseContainersReady=FalsePodScheduled=True> kubectl get events -n l02 --field-selector involvedObject.name=readiness-fail,type=Warning -o custom-columns=TYPE:.type,REASON:.reason,MESSAGE:.message --no-headers | grep -v 'connection refused' | sort -uWarning Unhealthy Readiness probe failed: HTTP probe failed with statuscode: 404Running, zéro redémarrage, et jamais Ready. Rien n’est cassé du point de vue du kubelet ; le pod ne reçoit simplement jamais de trafic. La leçon 4 montre l’autre côté : un tel pod est listé dans l’EndpointSlice de son Service, marqué non prêt.
Une sonde startup pour la JVM
Section intitulée « Une sonde startup pour la JVM »L’API Spring Boot a besoin de quelques secondes pour démarrer. Une sonde liveness avec un court délai la tuerait avant qu’elle réponde ; un long initialDelaySeconds retarderait la détection des vraies pannes pendant toute la vie du pod. Une sonde startup règle les deux. l02/startup.yaml vérifie toutes les secondes, jusqu’à 60 fois, et la sonde liveness ne commence qu’après le premier succès :
startupProbe: httpGet: path: / port: 8080 periodSeconds: 1 failureThreshold: 60 livenessProbe: httpGet: path: / port: 8080 periodSeconds: 10> kubectl wait -n l02 --for=condition=Ready pod/java-reactor-api --timeout=180spod/java-reactor-api condition met> kubectl get events -n l02 --field-selector involvedObject.name=java-reactor-api -o custom-columns=TYPE:.type,REASON:.reason,COUNT:.count,MESSAGE:.message --sort-by=.metadata.creationTimestampTYPE REASON COUNT MESSAGENormal Scheduled 1 Successfully assigned l02/java-reactor-api to learn-k8s-control-planeNormal Pulled 1 Container image "java-reactor-api:1.0" already present on machine and can be accessed by the podNormal Created 1 Container createdNormal Started 1 Container startedWarning Unhealthy 1 Startup probe failed: Get "http://10.244.0.196:8080/": dial tcp 10.244.0.196:8080: connect: connection refusedLors de cette exécution, le conteneur a démarré à 13:18:36 et le pod était prêt à 13:18:38 : deux secondes, avec une sonde startup en échec qui n’a rien coûté. Sur un nœud chargé, la même JVM peut mettre dix fois plus longtemps, d’où un budget de 60 secondes et non de 3. Les durées changent d’une exécution à l’autre, donc check.sh enregistre cette sortie sans la comparer.
Conteneurs init et sidecars
Section intitulée « Conteneurs init et sidecars »Les conteneurs init s’exécutent l’un après l’autre, chacun jusqu’au bout, avant que les conteneurs normaux démarrent : une migration, un fichier de configuration, l’attente d’une dépendance. Depuis Kubernetes 1.33, où la fonctionnalité est devenue stable, un conteneur init avec restartPolicy: Always est plutôt un sidecar natif : il démarre dans la séquence init, mais continue de tourner à côté de l’application, et s’arrête après elle. Les expéditeurs de logs, les proxys et les renouveleurs de certificats sont des sidecars typiques.
l02/init-sidecar.yaml a les deux. setup écrit un fichier de configuration dans un volume emptyDir partagé, log-shipper suit un fichier de log, et app lit la configuration et ajoute une ligne au log chaque seconde :
spec: # sh tourne en PID 1 et ignore SIGTERM : sans ceci, la suppression du pod attend les 30 s par défaut avant SIGKILL terminationGracePeriodSeconds: 5 initContainers: - name: setup image: busybox:1.38.0 command: ["sh", "-c", "echo 'greeting=hello' > /config/app.conf && echo setup done"] volumeMounts: - name: config mountPath: /config - name: log-shipper image: busybox:1.38.0 restartPolicy: Always # Suit le fichier qu'écrit l'application ; un vrai sidecar l'enverrait quelque part command: ["sh", "-c", "touch /logs/app.log && tail -F /logs/app.log"] volumeMounts: - name: logs mountPath: /logs containers: - name: app image: busybox:1.38.0 command: ["sh", "-c", ". /config/app.conf; i=0; while true; do i=$((i+1)); echo \"$greeting $i\" >> /logs/app.log; sleep 1; done"] volumeMounts: - name: config mountPath: /config - name: logs mountPath: /logs volumes: - name: config emptyDir: {} - name: logs emptyDir: {}> kubectl get pod -n l02 init-sidecarNAME READY STATUS RESTARTS AGEinit-sidecar 2/2 Running 0 5s> kubectl get pod -n l02 init-sidecar -o jsonpath='{range .status.initContainerStatuses[*]}{.name}: started={.started} ready={.ready} terminated={.state.terminated.reason}{"\n"}{end}{range .status.containerStatuses[*]}{.name}: started={.started} ready={.ready}{"\n"}{end}'setup: started=false ready=true terminated=Completedlog-shipper: started=true ready=true terminated=app: started=true ready=true> kubectl logs -n l02 init-sidecar -c setupsetup done> kubectl logs -n l02 init-sidecar -c log-shipper | head -3hello 1hello 2hello 3READY 2/2 compte le sidecar et l’application, pas setup, qui a terminé. -c choisit un conteneur dans un pod à plusieurs conteneurs. Le sidecar a vu la première ligne parce qu’il tournait déjà quand l’application l’a écrite : cet ordre de démarrage est ce que garantit le sidecar natif, et ce que ne garantit pas un second conteneur ordinaire dans containers.
Requests, limits et QoS
Section intitulée « Requests, limits et QoS »Chaque conteneur peut déclarer, pour le CPU et la mémoire :
- une request : ce que l’ordonnanceur lui réserve sur un nœud. Un pod n’est placé que là où la somme des requests tient.
- une limit : le maximum qu’il peut utiliser. Au-delà de sa limite CPU, un conteneur est bridé ; au-delà de sa limite mémoire, le noyau le tue.
Le CPU se compte en cœurs (100m est un dixième de cœur), la mémoire en octets (64Mi). Le premier pod demandait 100m et 64Mi avec une limite mémoire de 256Mi, et aucune limite CPU : il peut utiliser le CPU inoccupé du nœud, mais pas plus de mémoire que prévu. C’est un choix courant pour les API web ; la page sur la gestion des ressources décrit les options.
Les requests et les limits décident de la classe QoS du pod, qui décide quels pods le kubelet expulse en premier quand un nœud manque de mémoire. l02/qos.yaml crée un pod de chaque classe :
> kubectl get pods -n l02 guaranteed burstable besteffort -o custom-columns=NAME:.metadata.name,QOS:.status.qosClassNAME QOSguaranteed Guaranteedburstable Burstablebesteffort BestEffortLa règle, telle qu’écrite dans qos.go : un pod est BestEffort si aucun conteneur n’a de request ni de limit CPU ou mémoire, Guaranteed si chaque conteneur a des requests CPU et mémoire égales à ses limits, et Burstable sinon. Les pods BestEffort partent en premier sous pression mémoire, les Guaranteed en dernier.
Les deux runtimes lisent la limite. .NET dimensionne son tas géré par le ramasse-miettes d’après la limite mémoire du conteneur (GCHeapHardLimit vaut 75 % de celle-ci par défaut), et la JVM d’après -XX:MaxRAMPercentage, 25 % de la limite par défaut. Une limite copiée d’un autre service peut affamer l’un et gaspiller de la mémoire pour l’autre.
Depuis Kubernetes 1.35, les requests et les limits peuvent aussi être modifiées sur un pod en cours d’exécution sans le redémarrer ; la leçon 9, sur la mise à l’échelle, y revient.
Un cas pratique : le guide de déploiement de GA
Section intitulée « Un cas pratique : le guide de déploiement de GA »Guitar Alchemist tourne sur Docker Compose et .NET Aspire, et un chantier Kubernetes est prévu, pas commencé. Son guide de déploiement contient déjà un Deployment pour l’API, avec une sonde liveness sur /health et une sonde readiness sur /ready, toutes deux sur le port 7001. Comparé au code du même commit :
- le Dockerfile de l’API expose le port 8080, la valeur par défaut des images
aspnetde .NET, et fixeASPNETCORE_ENVIRONMENT=Production; - les service defaults d’Aspire ne mappent
/healthet/aliveque dans l’environnement Development, un choix du modèle pour des raisons de sécurité ; - aucun point de terminaison
/readyn’existe ; le contrôleur de santé propre à l’API répond sous/api/Health.
Telles qu’elles sont écrites, les sondes échoueraient contre l’image : rien n’écoute sur 7001, et même sur 8080 les chemins renvoient 404. D’après cette leçon, le résultat attendu est un conteneur redémarré par sa sonde liveness et un pod jamais Ready, mais je n’ai pas déployé GA pour le vérifier (à vérifier). Le guide ne déclare pas non plus de ressources, donc le pod serait BestEffort. La correction concerne les deux côtés : mapper un point de terminaison de santé live et un ready en Production, sans les détails dont parle l’avertissement du modèle, et pointer les sondes dessus sur 8080.
À retenir
Section intitulée « À retenir »- Un pod est un ou plusieurs conteneurs placés ensemble ; sa phase dit où il en est de sa vie, ses conditions disent quelles étapes il a franchies, et
Readydécide s’il reçoit du trafic. kubectl logs,kubectl get eventset lelastStatedu conteneur répondent à la plupart des questions « pourquoi ne démarre-t-il pas ».- La liveness redémarre un conteneur bloqué, la readiness sort un pod du trafic sans le redémarrer, et la startup protège les démarrages lents sans affaiblir les deux autres.
- Un redémarrage par liveness d’une application ASP.NET Core apparaît comme
Completed, code de sortie 0 : l’application s’est arrêtée proprement quand on le lui a demandé. - Les conteneurs init s’exécutent jusqu’au bout, dans l’ordre ; un sidecar natif (
restartPolicy: Always) démarre avant l’application et tourne à côté d’elle. - Les requests placent les pods, les limits les plafonnent, et ensemble elles fixent la classe QoS qui décide de l’ordre d’expulsion.
Exercices
Section intitulée « Exercices »1. l02/pod.yaml est Burstable. Écrivez une variante Guaranteed, appliquez-la, et vérifiez sa classe.
Solution
Les requests doivent égaler les limits pour le CPU comme pour la mémoire, dans chaque conteneur : l02/pod-guaranteed.yaml.
resources: requests: cpu: 100m memory: 128Mi limits: cpu: 100m memory: 128Mi> kubectl apply -n l02 -f l02/pod-guaranteed.yamlpod/csharp-api-guaranteed created> kubectl get pod -n l02 csharp-api-guaranteed -o jsonpath='{.status.qosClass}{"\n"}'GuaranteedLe prix est la limite CPU : sous charge, ce pod est bridé à un dixième de cœur même si le nœud est inoccupé.
2. Donnez à l’API C# une limite mémoire de 8Mi (l02/oom.yaml). Que se passe-t-il, et où le voyez-vous ?
Solution
> kubectl apply -n l02 -f l02/oom.yamlpod/oom created> kubectl get pod -n l02 oomNAME READY STATUS RESTARTS AGEoom 1/1 Running 1 (2s ago) 2s> kubectl get pod -n l02 oom -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason} {.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'OOMKilled 137Le noyau tue le processus dès qu’il dépasse la limite : raison OOMKilled, code de sortie 137 (128 + 9, SIGKILL). Le kubelet le redémarre, et quelques secondes plus tard STATUS affiche OOMKilled. STATUS change d’une seconde à l’autre, donc c’est lastState qu’il faut lire.
En préparant cet exercice, une limite de 16Mi a d’abord semblé suffire : l’API démarrait et continuait de tourner. Puis un kubectl exec dans ce conteneur, pour lire sa consommation mémoire, lui a fait dépasser la limite, et l’API a été OOMKilled. Un processus lancé avec kubectl exec s’exécute dans le cgroup du conteneur et compte dans sa limite.
3. À quoi ressemble un pod dont le conteneur init échoue ? Essayez l02/init-fail.yaml, dont le conteneur setup affiche une erreur et sort avec le code 1.
Solution
> kubectl apply -n l02 -f l02/init-fail.yamlpod/init-fail created> kubectl get pod -n l02 init-failNAME READY STATUS RESTARTS AGEinit-fail 0/1 Init:Error 1 (2s ago) 2s> kubectl get pod -n l02 init-fail -o jsonpath='setup: exit code {.status.initContainerStatuses[0].lastState.terminated.exitCode}{"\n"}app: {.status.containerStatuses[0].state.waiting.reason}{"\n"}'setup: exit code 1app: PodInitializing> kubectl logs -n l02 init-fail -c setupconfig server unreachableInit:Error signifie qu’un conteneur init est sorti en erreur ; pendant qu’il s’exécute à nouveau, STATUS affiche Init:0/1, zéro conteneur init terminé sur un. Avec la restartPolicy: Always par défaut du pod, le kubelet relance setup avec un délai croissant, et app attend en PodInitializing jusqu’à ce qu’il réussisse. Le journal du conteneur init lui-même donne la raison.
- Documentation de Kubernetes : Pods, Cycle de vie d’un pod, Sondes liveness, readiness et startup et comment les configurer, Conteneurs init, Conteneurs sidecar, Gestion des ressources des pods et des conteneurs, Classes de qualité de service des pods
- Code source en v1.37.0 :
pkg/probe/http/http.go,pkg/kubelet/prober/prober.go,qos.go - Health checks dans ASP.NET Core, Sondes Kubernetes de Spring Boot, Configuration du ramasse-miettes .NET
- kind : problèmes connus, ticket #3795