Lab 10.2 : Placement¶
| Durée | 75 à 90 min |
| Niveau | Guidé |
| Prérequis | Lab 8.2 réalisé (cordon, drain), Lab 10.1 réalisé, cours du chapitre 10 lu, cluster à 3 nœuds Ready |
| Acquis visés | AA6 |
!!! abstract "Objectifs" - Observer la répartition par défaut des Pods sur les nœuds - Imposer un nœud avec nodeSelector et diagnostiquer un Pod Pending - Utiliser l'affinité de nœud (obligatoire et préférée) - Répartir des réplicas avec l'anti-affinité de Pods et les contraintes de répartition - Repousser des Pods avec des taints, et réserver un nœud (taint, toleration et affinité) - Constater qu'un PodDisruptionBudget peut bloquer un drain
Contexte et schéma¶
Vous jouez avec les labels et les taints des trois nœuds pour décider où les Pods s'exécutent. Les noms des nœuds sont mémorisés dans des variables, ce qui rend le lab indépendant de leurs noms réels.
flowchart LR
P["Pod à placer"] --> F{"Filtrage"}
F -- "taint non toléré" --> X1["nœud écarté"]
F -- "nodeSelector /<br/>affinité obligatoire<br/>non respectés" --> X2["nœud écarté"]
F -- "nœuds restants" --> S["Score :<br/>préférences, répartition"]
S --> N["Nœud choisi"]
F -- "aucun nœud" --> PE["Pending"]
Ce lab modifie les nœuds
Vous ajoutez des labels et des taints aux nœuds. Le nettoyage final est indispensable : un taint oublié empêcherait les Pods des autres labs de s'exécuter sur ce nœud.
Étapes¶
Étape 1 : Préparer l'environnement¶
k create namespace tp-k8s
k config set-context --current --namespace=tp-k8s
k get nodes
NODES=($(k get nodes -o jsonpath='{.items[*].metadata.name}'))
N1=${NODES[0]}; N2=${NODES[1]}; N3=${NODES[2]}
echo "N1=$N1 N2=$N2 N3=$N3"
k get nodes --show-labels | cut -c1-200
for n in $N1 $N2 $N3; do echo "== $n"; k describe node $n | grep -E "Taints"; done
Variables
Les variables N1, N2 et N3 n'existent que dans ce terminal. Si vous en ouvrez un autre, relancez les trois lignes qui les définissent.
!!! success "Point de contrôle" - [ ] Trois nœuds Ready, et les variables N1, N2, N3 contiennent leurs noms - [ ] Vous avez relevé les taints existants (en général <none>)
Étape 2 : La répartition par défaut¶
Déployez six réplicas sans aucune contrainte :
cat <<'EOF' > web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.26-alpine
resources:
requests:
cpu: 10m
memory: 16Mi
EOF
k apply -f web.yaml
k rollout status deployment/web
k get pods -l app=web -o wide
k get pods -l app=web -o custom-columns=NODE:.spec.nodeName --no-headers | sort | uniq -c
Le scheduler équilibre naturellement les Pods sur les nœuds. Supprimez ce Deployment :
!!! success "Point de contrôle" - [ ] Les six Pods sont répartis sur plusieurs nœuds (idéalement deux par nœud)
Étape 3 : nodeSelector¶
Posez un label sur le premier nœud, puis forcez un Deployment à s'y exécuter :
k label node $N1 disque=ssd
cat <<'EOF' > db.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
replicas: 2
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
nodeSelector:
disque: ssd
containers:
- name: db
image: nginx:1.26-alpine
EOF
k apply -f db.yaml
k rollout status deployment/db
k get pods -l app=db -o wide
Les deux Pods sont sur $N1. Simulez maintenant une erreur de configuration : un sélecteur vers une valeur qui n'existe sur aucun nœud.
k patch deployment db -p '{"spec":{"template":{"spec":{"nodeSelector":{"disque":"nvme"}}}}}'
sleep 15
k get pods -l app=db -o wide
k describe pod -l app=db | grep -E "FailedScheduling" | head -n 2
Le nouveau Pod reste Pending et les anciens Pods continuent de fonctionner (rolling update, chapitre 2). Lisez le message du scheduler. Corrigez en créant le label manquant sur un nœud :
!!! success "Point de contrôle" - [ ] Avec disque=ssd, les deux Pods étaient sur $N1 - [ ] Avec disque=nvme absent, un Pod était Pending avec un message de non-correspondance au sélecteur - [ ] Après l'ajout du label sur $N2, le rollout s'est terminé et les Pods sont sur $N2
Étape 4 : Affinité de nœud¶
Posez deux labels de zone sur les nœuds 2 et 3. Le nœud 1 n'a pas de label zone.
k label node $N2 zone=a
k label node $N3 zone=b
cat <<'EOF' > app-zone.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-zone
spec:
replicas: 4
selector:
matchLabels:
app: app-zone
template:
metadata:
labels:
app: app-zone
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: zone
operator: In
values: ["a", "b"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: zone
operator: In
values: ["a"]
containers:
- name: web
image: nginx:1.26-alpine
resources:
requests:
cpu: 10m
memory: 16Mi
EOF
k apply -f app-zone.yaml
k rollout status deployment/app-zone
k get pods -l app=app-zone -o custom-columns=NODE:.spec.nodeName --no-headers | sort | uniq -c
Aucun Pod n'est sur $N1 (règle obligatoire) et la plupart sont sur $N2 (zone a, règle préférée). Constatez ensuite que la règle n'est vérifiée qu'au placement :
k label node $N2 zone-
sleep 10
k get pods -l app=app-zone -o custom-columns=NODE:.spec.nodeName --no-headers | sort | uniq -c
k label node $N2 zone=a
!!! success "Point de contrôle" - [ ] Aucun Pod app-zone sur $N1 - [ ] La majorité des Pods sur $N2 - [ ] Après suppression du label zone de $N2, les Pods déjà en place n'ont pas bougé (IgnoredDuringExecution)
Étape 5 : Anti-affinité et répartition¶
Anti-affinité obligatoire. Un réplica par nœud au maximum :
cat <<'EOF' > web-ha.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-ha
spec:
replicas: 3
selector:
matchLabels:
app: web-ha
template:
metadata:
labels:
app: web-ha
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web-ha
topologyKey: kubernetes.io/hostname
containers:
- name: web
image: nginx:1.26-alpine
resources:
requests:
cpu: 10m
memory: 16Mi
EOF
k apply -f web-ha.yaml
k rollout status deployment/web-ha
k get pods -l app=web-ha -o wide
k scale deployment web-ha --replicas=4
sleep 10
k get pods -l app=web-ha
k describe pod -l app=web-ha | grep -E "FailedScheduling" | head -n 1
Avec 3 nœuds et 3 réplicas, chaque nœud en héberge un. Le quatrième réplica reste Pending : la règle obligatoire interdit de partager un nœud. Remplacez-la par une contrainte de répartition souple :
cat <<'EOF' > web-spread.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-spread
spec:
replicas: 4
selector:
matchLabels:
app: web-spread
template:
metadata:
labels:
app: web-spread
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web-spread
containers:
- name: web
image: nginx:1.26-alpine
resources:
requests:
cpu: 10m
memory: 16Mi
EOF
k apply -f web-spread.yaml
k rollout status deployment/web-spread
k get pods -l app=web-spread -o custom-columns=NODE:.spec.nodeName --no-headers | sort | uniq -c
k delete deployment web-ha web-spread
Les quatre Pods se répartissent en 2 / 1 / 1.
!!! success "Point de contrôle" - [ ] web-ha : un Pod par nœud, et le quatrième réplica reste Pending avec un message sur l'anti-affinité - [ ] web-spread : les quatre Pods sont placés, avec un écart d'au plus 1 entre les nœuds
Étape 6 : Taints et tolerations¶
Posez un taint sur le nœud 3 :
k taint nodes $N3 dedie=batch:NoSchedule
k describe node $N3 | grep Taints
k create deployment normal --image=nginx:1.26-alpine --replicas=6
k rollout status deployment/normal
k get pods -l app=normal -o custom-columns=NODE:.spec.nodeName --no-headers | sort | uniq -c
Aucun Pod normal n'a été placé sur $N3. Les Pods déjà présents sur ce nœud (par exemple ceux de app-zone) n'ont pas été évincés : NoSchedule n'agit que sur les nouveaux Pods.
Donnez maintenant une toleration à un Deployment, sans autre règle :
cat <<'EOF' > batch-tol.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: batch-tol
spec:
replicas: 6
selector:
matchLabels:
app: batch-tol
template:
metadata:
labels:
app: batch-tol
spec:
tolerations:
- key: dedie
operator: Equal
value: batch
effect: NoSchedule
containers:
- name: web
image: nginx:1.26-alpine
resources:
requests:
cpu: 10m
memory: 16Mi
EOF
k apply -f batch-tol.yaml
k rollout status deployment/batch-tol
k get pods -l app=batch-tol -o custom-columns=NODE:.spec.nodeName --no-headers | sort | uniq -c
Certains Pods peuvent être sur $N3, mais pas tous : la toleration autorise le nœud, elle ne l'impose pas. Pour réserver $N3 à ces Pods, ajoutez un label au nœud et un nodeSelector dans le Deployment :
k label node $N3 dedie=batch
k patch deployment batch-tol -p '{"spec":{"template":{"spec":{"nodeSelector":{"dedie":"batch"}}}}}'
k rollout status deployment/batch-tol
k get pods -l app=batch-tol -o custom-columns=NODE:.spec.nodeName --no-headers | sort | uniq -c
Tous les Pods batch-tol sont désormais sur $N3, et aucun autre Pod n'y arrive. Retirez enfin le taint pour terminer cette étape :
k taint nodes $N3 dedie=batch:NoSchedule-
k describe node $N3 | grep Taints
k delete deployment normal batch-tol
!!! success "Point de contrôle" - [ ] Aucun Pod normal n'a été placé sur $N3 pendant que le taint existait - [ ] Avec la toleration seule, les Pods batch-tol n'étaient pas tous sur $N3 - [ ] Avec le taint, la toleration et le nodeSelector, ils y sont tous - [ ] Le taint est retiré (Taints: <none>)
Étape 7 (facultatif) : L'effet NoExecute¶
Perturbation brève
Un taint NoExecute évince tous les Pods du nœud qui ne le tolèrent pas, y compris des Pods système (DNS, Traefik...). Ils sont recréés sur d'autres nœuds en quelques secondes. Retirez le taint dans la minute qui suit.
k create deployment evincables --image=nginx:1.26-alpine --replicas=6
k rollout status deployment/evincables
k get pods -l app=evincables -o wide
k taint nodes $N2 urgence=oui:NoExecute
sleep 15
k get pods -l app=evincables -o wide
k taint nodes $N2 urgence=oui:NoExecute-
k delete deployment evincables
!!! success "Point de contrôle" - [ ] Les Pods de $N2 ont été supprimés puis recréés sur d'autres nœuds - [ ] Le taint urgence est retiré
Étape 8 : PodDisruptionBudget et drain¶
Déployez une application critique à 3 réplicas, protégée par un budget qui n'autorise aucune indisponibilité :
cat <<'EOF' > critique.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: critique
spec:
replicas: 3
selector:
matchLabels:
app: critique
template:
metadata:
labels:
app: critique
spec:
containers:
- name: web
image: nginx:1.26-alpine
resources:
requests:
cpu: 10m
memory: 16Mi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: critique-pdb
spec:
minAvailable: 3
selector:
matchLabels:
app: critique
EOF
k apply -f critique.yaml
k rollout status deployment/critique
k get pdb
k get pods -l app=critique -o wide
La colonne ALLOWED DISRUPTIONS vaut 0. Tentez de vider un nœud qui héberge un de ces Pods :
NODE=$(k get pod -l app=critique -o jsonpath='{.items[0].spec.nodeName}')
echo $NODE
k drain $NODE --ignore-daemonsets --delete-emptydir-data --timeout=40s
Le drain affiche Cannot evict pod as it would violate the pod's disruption budget, réessaie, puis abandonne au bout de 40 secondes. Le nœud reste isolé (SchedulingDisabled). Assouplissez le budget, puis relancez :
k delete pdb critique-pdb
cat <<'EOF' > critique-pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: critique-pdb
spec:
maxUnavailable: 1
selector:
matchLabels:
app: critique
EOF
k apply -f critique-pdb.yaml
k get pdb
k drain $NODE --ignore-daemonsets --delete-emptydir-data --timeout=90s
k get pods -l app=critique -o wide
k uncordon $NODE
k get nodes
!!! success "Point de contrôle" - [ ] Avec minAvailable: 3, le drain a été bloqué et a affiché le message de budget - [ ] Avec maxUnavailable: 1, le drain a abouti et les Pods ont été recréés sur d'autres nœuds - [ ] Le nœud est de nouveau Ready et planifiable après uncordon
Questions de réflexion¶
Quelle différence entre nodeSelector et une affinité de nœud « obligatoire » ?
Les deux imposent un filtre sur les nœuds. L'affinité est plus expressive (opérateurs In, NotIn, Exists...) et permet aussi des règles simplement préférées, ce que nodeSelector ne fait pas.
Pourquoi les Pods déjà placés n'ont-ils pas bougé quand vous avez retiré le label zone du nœud ?
Les règles d'affinité sont vérifiées au moment du placement (IgnoredDuringExecution). Un Pod en cours d'exécution n'est pas déplacé si les labels du nœud changent.
Pourquoi le quatrième réplica de web-ha restait-il Pending ?
L'anti-affinité obligatoire interdit deux Pods du même Deployment sur un nœud, et il n'y a que trois nœuds. Une règle souple (anti-affinité préférée, ou contrainte de répartition) aurait permis le placement.
Pourquoi une toleration seule ne suffit-elle pas à réserver un nœud ?
Elle autorise un Pod à aller sur un nœud tainté, mais ne l'y attire pas. Pour réserver le nœud, on combine le taint (qui repousse les autres) avec un nodeSelector ou une affinité (qui attire les bons Pods).
Qu'est-ce qui distingue NoSchedule de NoExecute ?
NoSchedule n'empêche que le placement de nouveaux Pods, ceux qui sont déjà là restent. NoExecute évince en plus les Pods en cours d'exécution qui ne tolèrent pas le taint.
Votre drain reste bloqué à cause d'un PodDisruptionBudget. Comment diagnostiquer et que décidez-vous ?
k get pdb montre ALLOWED DISRUPTIONS à 0. Selon le besoin, on augmente le nombre de réplicas, on assouplit le budget (par exemple maxUnavailable: 1), ou on attend que des Pods supplémentaires soient prêts. Supprimer le budget sans réflexion supprime la protection de la disponibilité.
Dépannage¶
| Symptôme | Pistes |
|---|---|
N1, N2 ou N3 vides |
Nouveau terminal : relancer les lignes de l'étape 1 ; vérifier que le cluster compte bien trois nœuds |
Pod Pending |
k describe pod : didn't match Pod's node affinity/selector, taint, anti-affinity ou Insufficient cpu/memory selon le cas |
| Le label n'est pas pris en compte | k get nodes --show-labels \| grep <label> ; attention aux fautes de frappe dans la clé et la valeur |
| Un Deployment ne se met pas à jour après correction d'un label | k rollout status ; le scheduler réessaie périodiquement, patienter une minute |
| Les Pods sont tous sur un seul nœud (étape 2) | Un nœud a été planifiable plus tôt, ou un taint ou un cordon subsiste : k get nodes, k describe node \| grep Taints |
drain : cannot delete Pods not managed by ... |
Pod sans contrôleur : le supprimer à la main ou ajouter --force (avec précaution) |
drain : cannot delete DaemonSet-managed Pods |
Ajouter --ignore-daemonsets |
label ... not found au nettoyage |
Le label n'existait pas : sans conséquence |
Un nœud reste SchedulingDisabled |
k uncordon <nœud> |
Nettoyage¶
Retirez les labels et les taints posés pendant le lab, puis supprimez le namespace :
k label node $N1 disque-
k label node $N2 disque- zone-
k label node $N3 zone- dedie-
k taint nodes $N3 dedie=batch:NoSchedule-
k taint nodes $N2 urgence=oui:NoExecute-
k uncordon $N1 ; k uncordon $N2 ; k uncordon $N3
k delete namespace tp-k8s
k config set-context --current --namespace=default
for n in $N1 $N2 $N3; do echo "== $n"; k describe node $n | grep -E "Taints"; done
k get nodes
Les messages « label not found » ou « taint not found » sont sans conséquence. Vérifiez que les trois nœuds sont Ready, sans taint, et que l'option SchedulingDisabled a disparu.