TP Kubernetes : placer ses Pods avec Node Affinity, Pod Affinity et Pod Anti-Affinity

Durée estimée : 2 h 30 à 3 h Niveau : débutant à intermédiaire, suite des TP « Pod, Deployment, Service » et « WordPress » Prérequis : minikube et kubectl installés, notions de Pod, Deployment et labels

Objectifs

À la fin de ce TP, vous saurez :

  1. créer un cluster minikube à plusieurs nœuds et étiqueter ces nœuds ;
  2. expliquer comment le scheduler choisit le nœud d'un Pod ;
  3. imposer ou préférer certains nœuds avec nodeSelector et Node Affinity ;
  4. rapprocher des Pods entre eux avec la Pod Affinity ;
  5. éloigner des Pods les uns des autres avec la Pod Anti-Affinity ;
  6. comprendre le rôle du topologyKey (nœud, zone) ;
  7. diagnostiquer un Pod bloqué en Pending à cause d'une règle de placement.

Sommaire


Partie 0 : Préparation du cluster multi-nœuds

0.1 Pourquoi plusieurs nœuds ?

Les règles d'affinité décident sur quel nœud un Pod doit tourner. Avec un seul nœud, il n'y a aucun choix possible. On va donc créer un cluster de 3 nœuds dans un profil minikube séparé, pour ne pas toucher au cluster des TP précédents.

Ressources nécessaires : environ 6 Go de RAM libres et 6 CPU (3 nœuds de 2 CPU et 2 Go).

0.2 Créer le cluster

minikube start -p affinite --nodes=3 --driver=docker --cpus=2 --memory=2048

Comptez 3 à 6 minutes. Vérifiez :

minikube profile list
kubectl config current-context
kubectl get nodes -o wide

Résultat attendu :

NAME           STATUS   ROLES           AGE   VERSION
affinite       Ready    control-plane   3m    v1.3x.x
affinite-m02   Ready    <none>          2m    v1.3x.x
affinite-m03   Ready    <none>          1m    v1.3x.x
Profil minikube : l'option -p affinite crée un cluster indépendant nommé affinite. kubectl bascule automatiquement dessus. Pour revenir plus tard au cluster des TP précédents : kubectl config use-context minikube.

Sur minikube, le nœud control-plane accepte aussi les Pods applicatifs. On a donc bien 3 nœuds utilisables.

0.3 Donner une « carte d'identité » aux nœuds

Les règles d'affinité s'appuient sur les labels des nœuds. On va simuler un petit datacenter :

Nœud Zone Type de disque
affinite zone-a hdd
affinite-m02 zone-a ssd
affinite-m03 zone-b ssd
kubectl label node affinite     topology.kubernetes.io/zone=zone-a disktype=hdd
kubectl label node affinite-m02 topology.kubernetes.io/zone=zone-a disktype=ssd
kubectl label node affinite-m03 topology.kubernetes.io/zone=zone-b disktype=ssd

Affichez les labels sous forme de colonnes :

kubectl get nodes -L topology.kubernetes.io/zone,disktype
Labels standards et labels personnalisés :topology.kubernetes.io/zone est un label standard. Chez un fournisseur cloud, il est posé automatiquement sur chaque nœud (par exemple eu-west-3a). On le pose ici à la main.kubernetes.io/hostname est posé automatiquement par Kubernetes sur chaque nœud, avec le nom du nœud.disktype est un label personnalisé : vous pouvez inventer les vôtres (gpu, env, client...).

Voyez tous les labels d'un nœud :

kubectl get node affinite-m02 --show-labels

0.4 Préparer le dossier et un raccourci d'affichage

mkdir -p ~/tp-affinite && cd ~/tp-affinite

Tout au long du TP, on voudra savoir quel Pod tourne sur quel nœud. Créez cet alias :

alias kpn='kubectl get pods -o custom-columns=POD:.metadata.name,NOEUD:.spec.nodeName,STATUT:.status.phase'
kpn
Ajoutez la ligne alias kpn=... dans votre ~/.bashrc si vous voulez la conserver pour une autre session.

Partie 1 : Comment le scheduler place un Pod

Quand un Pod est créé, il n'a pas encore de nœud. Le scheduler (un composant du plan de contrôle) lui en choisit un en deux étapes :

                 Pod sans nœud
                      │
                      ▼
   ┌────────────────────────────────────────┐
   │ 1. FILTRAGE                             │  élimine les nœuds qui ne respectent pas
   │    règles "required" / nodeSelector     │  les contraintes OBLIGATOIRES
   └──────────────────┬─────────────────────┘
                      ▼
   ┌────────────────────────────────────────┐
   │ 2. NOTATION (scoring)                   │  classe les nœuds restants selon les
   │    règles "preferred", charge, etc.     │  PRÉFÉRENCES et d'autres critères
   └──────────────────┬─────────────────────┘
                      ▼
          Le nœud ayant la meilleure note
  • Si aucun nœud ne passe le filtrage, le Pod reste en Pending. Le scheduler réessaiera dès que la situation du cluster change (nouveau nœud, nouveau label, Pod supprimé...).
  • Les préférences ne bloquent jamais un Pod : elles influencent seulement la note.

Les trois mécanismes de ce TP :

Mécanisme Question posée S'appuie sur
Node Affinity « Sur quel type de nœud ce Pod doit-il tourner ? » les labels des nœuds
Pod Affinity « À côté de quels autres Pods ce Pod doit-il tourner ? » les labels des Pods déjà en place
Pod Anti-Affinity « Loin de quels Pods ce Pod doit-il tourner ? » les labels des Pods déjà en place

Chacun existe en deux versions :

Version Nom complet Effet
Obligatoire requiredDuringSchedulingIgnoredDuringExecution filtrage : si impossible, le Pod reste Pending
Préférentielle preferredDuringSchedulingIgnoredDuringExecution notation : le scheduler fait au mieux
Lire le nom : « required during scheduling » signifie que la règle est vérifiée au moment du placement. « Ignored during execution » signifie qu'elle n'est plus vérifiée ensuite : si les labels changent après coup, un Pod déjà en place n'est pas déplacé. On le vérifiera en partie 3.

Partie 2 : Échauffement avec nodeSelector

nodeSelector est la forme la plus simple : « ce Pod ne peut tourner que sur un nœud portant exactement ces labels ».

Créez nodeselector.yaml :

apiVersion: v1
kind: Pod
metadata:
  name: ns-ssd
spec:
  nodeSelector:
    disktype: ssd                # uniquement les nœuds avec disktype=ssd
  containers:
    - name: nginx
      image: nginx:1.27-alpine
kubectl apply -f nodeselector.yaml
kpn

Le Pod est sur affinite-m02 ou affinite-m03, jamais sur affinite (disque hdd).

Limites de nodeSelector : on ne peut exprimer qu'une égalité stricte, avec un ET entre les labels. Impossible de dire « ssd OU nvme », « pas en zone-b » ou « de préférence ». C'est ce que permet la Node Affinity.

kubectl delete -f nodeselector.yaml

Partie 3 : Node Affinity obligatoire

3.1 Premier exemple

Objectif : un Pod sur un nœud ssd qui n'est pas en zone-b.

Créez na-required.yaml :

apiVersion: v1
kind: Pod
metadata:
  name: na-required
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:                    # toutes les expressions d'un terme : ET
              - key: disktype
                operator: In
                values: ["ssd"]
              - key: topology.kubernetes.io/zone
                operator: NotIn
                values: ["zone-b"]
  containers:
    - name: nginx
      image: nginx:1.27-alpine
kubectl apply -f na-required.yaml
kpn

Un seul nœud remplit les deux conditions : affinite-m02.

Structure à retenir :nodeSelectorTerms est une liste de termes : il suffit qu'un terme soit satisfait (OU).Dans un terme, matchExpressions est une liste d'expressions : toutes doivent être satisfaites (ET).

Opérateurs disponibles :

Opérateur Signification Exemple
In la valeur du label fait partie de la liste disktype In [ssd, nvme]
NotIn la valeur ne fait pas partie de la liste zone NotIn [zone-b]
Exists le label existe, quelle que soit sa valeur gpu Exists
DoesNotExist le label n'existe pas maintenance DoesNotExist
Gt / Lt valeur numérique supérieure / inférieure cpu-gen Gt 3
NotIn et DoesNotExist permettent de repousser des Pods de certains nœuds. Pour interdire un nœud à tous les Pods sauf exceptions, on utilise plutôt les taints et tolerations (hors programme de ce TP).

3.2 Expérience : un Pod qui ne trouve pas de nœud

Créez na-gpu.yaml, un Pod qui exige un nœud avec GPU :

apiVersion: v1
kind: Pod
metadata:
  name: na-gpu
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: gpu
                operator: Exists
  containers:
    - name: nginx
      image: nginx:1.27-alpine
kubectl apply -f na-gpu.yaml
kpn
kubectl describe pod na-gpu | tail -n 5

Le Pod est Pending. Dans Events, un message du type :

0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector.

Le scheduler explique pour chaque nœud pourquoi il a été éliminé. C'est le message à savoir lire.

Ajoutez un GPU (fictif) sur un nœud :

kubectl label node affinite-m03 gpu=nvidia
kpn

Quelques secondes plus tard, le Pod passe en Running sur affinite-m03 : le scheduler a réessayé dès que le cluster a changé.

3.3 Expérience : « Ignored During Execution »

Retirez le label GPU (le - final supprime un label) :

kubectl label node affinite-m03 gpu-
kubectl get nodes -L gpu
kpn

Le Pod na-gpu continue de tourner sur un nœud qui ne respecte plus sa règle. La règle n'est vérifiée qu'au placement. Si le Pod est supprimé, il ne pourra pas être replacé :

kubectl delete pod na-gpu
kubectl apply -f na-gpu.yaml
kpn

Il reste Pending. Nettoyez :

kubectl delete -f na-gpu.yaml -f na-required.yaml

✅ Point de contrôle 3

  1. Dans nodeSelectorTerms, deux termes sont-ils combinés par ET ou par OU ? Et deux matchExpressions d'un même terme ?
  2. Écrivez l'expression qui accepte les nœuds dont disktype vaut ssd ou nvme.
  3. Un Pod est Pending. Quelle commande et quelle section consultez-vous en premier ?

Partie 4 : Node Affinity préférentielle

4.1 Notion

Une règle preferred exprime un souhait. Chaque préférence a un poids (weight, de 1 à 100) : si un nœud satisfait la préférence, son poids est ajouté à sa note. Le Pod est ensuite placé sur le nœud le mieux noté.

4.2 Préférer une zone

Créez na-preferred.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pref-zone
spec:
  replicas: 6
  selector:
    matchLabels:
      app: pref-zone
  template:
    metadata:
      labels:
        app: pref-zone
    spec:
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 80                        # préférence forte
              preference:
                matchExpressions:
                  - key: topology.kubernetes.io/zone
                    operator: In
                    values: ["zone-b"]
            - weight: 20                        # préférence faible
              preference:
                matchExpressions:
                  - key: disktype
                    operator: In
                    values: ["ssd"]
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
kubectl apply -f na-preferred.yaml
kubectl rollout status deployment pref-zone
kpn

Comptez les Pods par nœud :

kubectl get pods -l app=pref-zone -o jsonpath='{range .items[*]}{.spec.nodeName}{"\n"}{end}' | sort | uniq -c

La majorité des Pods est sur affinite-m03 (zone-b + ssd = 100 points). Il est possible que quelques Pods atterrissent ailleurs : le scheduler prend aussi en compte d'autres critères, comme l'équilibrage de charge entre nœuds. Une préférence n'est pas une garantie.

Calcul des notes pour ce Deployment :

4.3 Expérience : required contre preferred face à une panne

Simulons l'indisponibilité de affinite-m03. cordon marque un nœud comme « non planifiable » (les Pods existants restent, aucun nouveau n'arrive) :

kubectl cordon affinite-m03
kubectl get nodes
kubectl rollout restart deployment pref-zone
kubectl rollout status deployment pref-zone
kpn

Tous les Pods ont été recréés sur affinite et affinite-m02, en privilégiant affinite-m02 (ssd). Avec une règle preferred, l'application a continué de fonctionner.

Faites le même test avec une règle obligatoire :

cat > na-required-zone.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: req-zone
spec:
  replicas: 2
  selector:
    matchLabels:
      app: req-zone
  template:
    metadata:
      labels:
        app: req-zone
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: topology.kubernetes.io/zone
                    operator: In
                    values: ["zone-b"]
      containers:
        - name: nginx
          image: nginx:1.27-alpine
EOF
kubectl apply -f na-required-zone.yaml
kpn

Les Pods req-zone sont Pending : le seul nœud de zone-b est indisponible. Remettez le nœud en service :

kubectl uncordon affinite-m03
kpn

Les Pods req-zone démarrent immédiatement sur affinite-m03.

À retenir : required protège une contrainte réelle (licence, matériel, conformité) au prix d'une indisponibilité possible. preferred optimise le placement sans jamais empêcher l'application de démarrer. Dans le doute, préférez preferred.
kubectl delete -f na-preferred.yaml -f na-required-zone.yaml

✅ Point de contrôle 4

  1. Que se passe-t-il pour un Pod dont aucune préférence ne peut être satisfaite ?
  2. À quoi sert le champ weight ?
  3. Que fait kubectl cordon ? Et kubectl uncordon ?

Partie 5 : Pod Anti-Affinity

5.1 Notion

La Pod Anti-Affinity répond à la question : « ne me place pas là où tournent déjà certains Pods ».

Cas d'usage principal : la haute disponibilité. Si les 3 réplicas d'une application tournent sur le même nœud et que ce nœud tombe, l'application est totalement indisponible. On veut donc un réplica par nœud.

Une règle de Pod Affinity ou Anti-Affinity comporte deux éléments :

  • labelSelector : quels Pods sont concernés (« les Pods app=web ») ;
  • topologyKey : à quelle échelle on compare, désignée par un label de nœud.
topologyKey Signification pour l'anti-affinité
kubernetes.io/hostname pas deux Pods sur le même nœud
topology.kubernetes.io/zone pas deux Pods dans la même zone
Comment le scheduler raisonne : il regroupe les nœuds qui ont la même valeur pour le label topologyKey. Avec kubernetes.io/hostname, chaque nœud forme son propre groupe. Avec topology.kubernetes.io/zone, affinite et affinite-m02 forment un seul groupe (zone-a), affinite-m03 un autre (zone-b).

5.2 Un réplica par nœud

Créez anti-web.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: web                          # s'éloigner des autres Pods app=web...
              topologyKey: kubernetes.io/hostname   # ...à l'échelle du nœud
      containers:
        - name: nginx
          image: nginx:1.27-alpine
Le Deployment s'applique la règle à lui-même : chaque Pod app=web refuse un nœud qui héberge déjà un Pod app=web.
kubectl apply -f anti-web.yaml
kubectl rollout status deployment web
kpn

Chaque Pod est sur un nœud différent.

5.3 Expérience : un réplica de trop

kubectl scale deployment web --replicas=4
kpn
kubectl describe $(kubectl get pods -l app=web --field-selector=status.phase=Pending -o name | head -n 1) | tail -n 4

Le 4e Pod est Pending, avec un message du type :

0/3 nodes are available: 3 node(s) didn't match pod anti-affinity rules.

Avec une anti-affinité obligatoire par nœud, le nombre maximum de réplicas est égal au nombre de nœuds.

Revenez à 3 réplicas :

kubectl scale deployment web --replicas=3

5.4 Expérience : le piège de la mise à jour

Déclenchez une mise à jour du Deployment (recréation de tous les Pods) :

kubectl rollout restart deployment web
kpn
kubectl rollout status deployment web --timeout=30s

La mise à jour reste bloquée. Pourquoi ?

  • Par défaut, un Deployment crée d'abord un nouveau Pod (maxSurge) avant de supprimer un ancien.
  • Ce nouveau Pod porte app=web, tout comme les 3 anciens qui occupent les 3 nœuds.
  • Il ne trouve aucun nœud libre et reste Pending. Aucun ancien Pod n'est supprimé tant qu'il n'est pas prêt : blocage.

Corrigez en demandant de supprimer un ancien Pod avant d'en créer un nouveau. Ajoutez dans anti-web.yaml, au niveau de spec (à côté de replicas) :

  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 0            # ne jamais dépasser le nombre de réplicas
      maxUnavailable: 1      # supprimer un ancien Pod à la fois
kubectl apply -f anti-web.yaml
kubectl rollout status deployment web
kpn

La mise à jour se termine : les Pods sont remplacés un par un.

5.5 Anti-affinité à l'échelle de la zone

Supprimez le Deployment puis modifiez anti-web.yaml : remplacez topologyKey: kubernetes.io/hostname par topologyKey: topology.kubernetes.io/zone.

kubectl delete deployment web
# modifier le topologyKey dans anti-web.yaml, puis :
kubectl apply -f anti-web.yaml
kpn

Avant de regarder le résultat, prédisez combien de Pods seront Running.

Réponse : 2. Il n'y a que deux zones, et la règle interdit deux Pods app=web dans la même zone. Un Pod tourne en zone-a (sur affinite ou affinite-m02), un en zone-b, le troisième reste Pending.

5.6 Anti-affinité préférentielle

En pratique, on utilise souvent la version préférentielle : « répartis au mieux, mais démarre quand même s'il n'y a pas assez de nœuds ».

Supprimez le Deployment et remplacez la section affinity de anti-web.yaml par :

      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:                      # attention : niveau supplémentaire
                labelSelector:
                  matchLabels:
                    app: web
                topologyKey: kubernetes.io/hostname

Remettez aussi replicas: 5.

kubectl delete deployment web
kubectl apply -f anti-web.yaml
kubectl rollout status deployment web
kpn

Les 5 Pods tournent : les 3 premiers sont répartis un par nœud, les 2 suivants s'ajoutent là où c'est possible.

Différence de syntaxe : en version preferred, la règle est encapsulée dans podAffinityTerm avec un weight. En version required, elle est écrite directement dans la liste. Cette différence est une source d'erreurs fréquente.
kubectl delete -f anti-web.yaml

✅ Point de contrôle 5

  1. Quel est le rôle du topologyKey ?
  2. Avec une anti-affinité obligatoire topologyKey: kubernetes.io/hostname sur un cluster de 5 nœuds, combien de réplicas au maximum peuvent tourner ?
  3. Pourquoi un rollout restart peut-il se bloquer avec une anti-affinité obligatoire, et comment l'éviter ?

Partie 6 : Pod Affinity

6.1 Notion

La Pod Affinity répond à la question inverse : « place-moi là où tournent déjà certains Pods ».

Cas d'usage : rapprocher deux composants qui échangent beaucoup (une application et son cache), pour réduire la latence réseau ou éviter du trafic entre zones, souvent facturé chez les fournisseurs cloud.

6.2 Déployer le cache

Créez cache.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cache
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cache
  template:
    metadata:
      labels:
        app: cache
    spec:
      containers:
        - name: redis
          image: redis:7.4-alpine
kubectl apply -f cache.yaml
kubectl rollout status deployment cache
kpn

Notez le nœud du Pod cache. Le scheduler l'a choisi librement.

6.3 Un frontal sur le même nœud que le cache

Créez front.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: front
spec:
  replicas: 3
  selector:
    matchLabels:
      app: front
  template:
    metadata:
      labels:
        app: front
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: cache                        # se rapprocher des Pods app=cache...
              topologyKey: kubernetes.io/hostname   # ...sur le même nœud
      containers:
        - name: nginx
          image: nginx:1.27-alpine
kubectl apply -f front.yaml
kubectl rollout status deployment front
kpn

Les 3 Pods front sont sur le même nœud que le Pod cache.

Déplacez le cache et observez :

kubectl delete pod -l app=cache
kpn

Le Pod cache a peut-être été recréé sur un autre nœud, mais les Pods front n'ont pas bougé (Ignored During Execution). Seuls les nouveaux Pods front suivront le cache :

kubectl rollout restart deployment front
kubectl rollout status deployment front
kpn

6.4 Élargir à la zone

Placer toutes les instances sur un seul nœud annule la haute disponibilité. Un compromis courant : même zone que le cache, mais pas forcément le même nœud.

Dans front.yaml, remplacez topologyKey: kubernetes.io/hostname par topologyKey: topology.kubernetes.io/zone, puis :

kubectl apply -f front.yaml
kubectl rollout status deployment front
kpn

Selon la zone du cache :

  • cache en zone-a : les Pods front peuvent aller sur affinite et affinite-m02 ;
  • cache en zone-b : ils ne peuvent aller que sur affinite-m03, seul nœud de cette zone.

6.5 Expérience : l'ordre de déploiement

kubectl delete -f front.yaml -f cache.yaml
kubectl apply -f front.yaml
kpn

Les Pods front sont Pending : aucun Pod app=cache n'existe, la règle obligatoire ne peut pas être satisfaite. Déployez le cache :

kubectl apply -f cache.yaml
kpn

Les Pods front démarrent dès que le cache est placé.

À retenir : une Pod Affinity obligatoire crée une dépendance de démarrage. Si le composant cible est absent (panne, suppression, erreur de label), les nouveaux Pods ne démarrent pas. C'est pourquoi on utilise souvent la version preferred pour la Pod Affinity.

6.6 Le schéma classique : combiner affinité et anti-affinité

Voici un besoin très courant en production : « les frontaux doivent être dans la même zone que le cache et jamais deux sur le même nœud ». On combine les deux règles dans le même Pod :

      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: cache
              topologyKey: topology.kubernetes.io/zone
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: front
              topologyKey: kubernetes.io/hostname

Question : avec replicas: 3, combien de Pods front seront Running si le cache est en zone-a ? Et s'il est en zone-b ? Vérifiez en appliquant cette version dans front.yaml (pensez à ajouter la strategy de la partie 5.4).

kubectl delete -f front.yaml -f cache.yaml

✅ Point de contrôle 6

  1. Quelle différence entre Node Affinity et Pod Affinity ?
  2. Pourquoi une Pod Affinity obligatoire avec topologyKey: kubernetes.io/hostname peut-elle nuire à la haute disponibilité ?
  3. Que se passe-t-il pour les Pods front déjà en place quand le Pod cache change de nœud ?

Partie 7 : Exercice de synthèse

Vous devez déployer trois composants avec les contraintes suivantes. Utilisez l'image nginx:1.27-alpine pour les trois.

Composant Réplicas Contraintes
db 1 obligatoirement sur un nœud disktype=ssd ; de préférence en zone-b
api 2 obligatoirement dans la même zone que db ; jamais deux api sur le même nœud
worker 3 jamais sur le même nœud que db

Travail demandé :

  1. Avant d'écrire le moindre YAML, prédisez sur papier le nœud de chaque Pod.
  2. Écrivez un fichier unique synthese.yaml contenant les trois Deployments.
  3. Appliquez-le et comparez le résultat à votre prédiction avec kpn.
  4. Un Pod reste Pending. Expliquez pourquoi à l'aide de kubectl describe.
  5. Proposez trois corrections différentes qui permettent à tous les Pods de démarrer, et indiquez ce que chacune sacrifie.

Partie 8 : Nettoyage

Supprimer toutes les ressources du TP :

kubectl delete deployment --all
kubectl delete pod --all

Supprimer le cluster multi-nœuds et revenir au cluster des TP précédents :

minikube delete -p affinite
kubectl config use-context minikube

Annexe A : Aide-mémoire

Emplacement des règles

spec:                                  # spec du Pod (ou spec.template.spec d'un Deployment)
  affinity:
    nodeAffinity:      { required..., preferred... }
    podAffinity:       { required..., preferred... }
    podAntiAffinity:   { required..., preferred... }

Syntaxe comparée

Obligatoire (required...) Préférentielle (preferred...)
nodeAffinity nodeSelectorTerms: [ { matchExpressions: [...] } ] [ { weight, preference: { matchExpressions: [...] } } ]
podAffinity / podAntiAffinity [ { labelSelector, topologyKey } ] [ { weight, podAffinityTerm: { labelSelector, topologyKey } } ]

Commandes utiles

Action Commande
Ajouter un label à un nœud kubectl label node <nœud> cle=valeur
Modifier un label existant kubectl label node <nœud> cle=valeur --overwrite
Supprimer un label kubectl label node <nœud> cle-
Afficher des labels en colonnes kubectl get nodes -L cle1,cle2
Voir quel Pod est sur quel nœud kubectl get pods -o wide
Compter les Pods par nœud kubectl get pods -l app=x -o jsonpath='{range .items[*]}{.spec.nodeName}{"\n"}{end}' | sort | uniq -c
Rendre un nœud non planifiable kubectl cordon <nœud>
Le remettre en service kubectl uncordon <nœud>
Vider un nœud (maintenance) kubectl drain <nœud> --ignore-daemonsets
Documentation kubectl explain pod.spec.affinity.podAntiAffinity

Annexe B : Dépannage

Message dans kubectl describe pod Signification Piste
didn't match Pod's node affinity/selector Aucun nœud ne porte les labels exigés kubectl get nodes --show-labels, vérifier clés, valeurs et majuscules
didn't match pod anti-affinity rules Tous les nœuds (ou zones) hébergent déjà un Pod repoussé Réduire les réplicas, ajouter des nœuds, passer en preferred
didn't match pod affinity rules Aucun Pod cible (ou pas dans la bonne topologie) kubectl get pods -l <selector> -o wide, le Pod cible existe-t-il ?
node(s) were unschedulable Nœud en cordon kubectl get nodes, puis kubectl uncordon
Insufficient cpu / Insufficient memory Pas assez de ressources sur les nœuds autorisés Réduire les requests ou élargir les règles
rollout status bloqué Anti-affinité obligatoire + maxSurge par défaut maxSurge: 0, maxUnavailable: 1
Erreur unknown field "labelSelector" à l'apply Oubli de podAffinityTerm en version preferred Voir la partie 5.6

Annexe C : Corrigés

Point de contrôle 3

  1. Les termes de nodeSelectorTerms sont combinés par OU. Les matchExpressions d'un même terme par ET.
  2. kubectl describe pod <nom>, section Events.
- key: disktype
  operator: In
  values: ["ssd", "nvme"]

Point de contrôle 4

  1. Il est quand même placé, sur le nœud le mieux noté selon les autres critères du scheduler.
  2. Il pondère les préférences entre elles : le poids est ajouté à la note des nœuds qui satisfont la préférence.
  3. cordon interdit le placement de nouveaux Pods sur le nœud sans toucher aux Pods existants. uncordon lève cette interdiction.

Point de contrôle 5

  1. Il définit l'échelle de comparaison : les nœuds ayant la même valeur pour ce label forment un même domaine (un nœud, une zone...).
  2. 5 réplicas, un par nœud. Les suivants restent Pending.
  3. Le nouveau Pod créé par maxSurge ne trouve aucun nœud libre et les anciens ne sont pas supprimés tant qu'il n'est pas prêt. Solution : maxSurge: 0 et maxUnavailable: 1.

Point de contrôle 6

  1. La Node Affinity s'appuie sur les labels des nœuds. La Pod Affinity s'appuie sur les labels des Pods déjà en place.
  2. Toutes les instances se retrouvent sur le même nœud : si ce nœud tombe, l'application entière est indisponible.
  3. Rien : les règles ne sont vérifiées qu'au placement. Seuls les nouveaux Pods suivront le cache.

Question 6.6

  • Cache en zone-a : 2 Pods front Running (un sur affinite, un sur affinite-m02), le 3e Pending.
  • Cache en zone-b : 1 seul Pod front Running (sur affinite-m03), les 2 autres Pending.

Exercice de synthèse

Prédiction :

  • db : nœuds ssd possibles affinite-m02 et affinite-m03, préférence zone-b, donc affinite-m03.
  • api : même zone que db (zone-b), donc uniquement affinite-m03. L'anti-affinité interdit deux api sur ce nœud : 1 api Running, 1 api Pending.
  • worker : tous les nœuds sauf celui de db, donc affinite et affinite-m02, 3 Pods répartis sur ces deux nœuds.

synthese.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: db
spec:
  replicas: 1
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: disktype
                    operator: In
                    values: ["ssd"]
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - key: topology.kubernetes.io/zone
                    operator: In
                    values: ["zone-b"]
      containers:
        - name: nginx
          image: nginx:1.27-alpine
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 0
      maxUnavailable: 1
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: db
              topologyKey: topology.kubernetes.io/zone
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: api
              topologyKey: kubernetes.io/hostname
      containers:
        - name: nginx
          image: nginx:1.27-alpine
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker
spec:
  replicas: 3
  selector:
    matchLabels:
      app: worker
  template:
    metadata:
      labels:
        app: worker
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: db
              topologyKey: kubernetes.io/hostname
      containers:
        - name: nginx
          image: nginx:1.27-alpine
kubectl apply -f synthese.yaml
kpn
kubectl describe $(kubectl get pods -l app=api --field-selector=status.phase=Pending -o name) | tail -n 4

Explication du Pod Pending : zone-b ne contient qu'un nœud. L'affinité de zone impose ce nœud aux deux api, et l'anti-affinité par nœud en interdit un second.

Trois corrections possibles :

Correction Effet Ce qui est sacrifié
Changer la préférence de db en zone-a db va sur affinite-m02, les 2 api sur affinite et affinite-m02 Le choix initial de zone pour la base
Passer l'anti-affinité de api en preferred Les 2 api démarrent sur affinite-m03 La haute disponibilité de api (un seul nœud)
Ajouter un nœud en zone-b : minikube node add -p affinite, puis le labelliser zone-b / ssd Chaque api a son nœud en zone-b Le coût d'un nœud supplémentaire

Pour la 3e correction :

minikube node add -p affinite
kubectl get nodes
kubectl label node affinite-m04 topology.kubernetes.io/zone=zone-b disktype=ssd
kpn