Placement des pods : taints, tolerations, nodeSelector, affinité
Comment on contrĂŽle oĂč un pod atterrit sur le cluster. NĂ© de #114 (segmentation en node groups core / observability), ce guide explique les mĂ©canismes et le pourquoi de nos choix, en particulier la toleration
operator: Existsdes DaemonSets.
L'idée en une phrase
Par dĂ©faut, le scheduler Kubernetes place un pod sur n'importe quel nĆud qui a la place. Pour un cluster segmentĂ© (trafic d'un cĂŽtĂ©, observabilitĂ© de l'autre), ça ne suffit pas : on a besoin de repousser, autoriser et forcer des placements. Trois leviers, Ă ne surtout pas confondre.
flowchart LR
POD["Pod Ă placer"] --> Q{Le nĆud a-t-il<br/>un taint ?}
Q -->|non| OK1["Placé si la place<br/>+ le nodeSelector collent"]
Q -->|oui| T{Le pod<br/>tolĂšre-t-il<br/>ce taint ?}
T -->|non| REJ["REJETĂ de ce nĆud"]
T -->|oui| OK2["AutorisĂ© sur ce nĆud<br/>(puis nodeSelector dĂ©cide)"]
1. Les trois leviers (et leur rĂŽle exact)
| Levier | Posé sur | Verbe | Effet |
|---|---|---|---|
| taint | le nĆud | repousse | aucun pod ne vient, sauf ceux qui le tolĂšrent |
| toleration | le pod | autorise | le pod a le droit d'aller sur un nĆud tachĂ© (ne l'y attire pas) |
| nodeSelector | le pod | force | le pod doit aller sur un nĆud qui porte ce label |
Le piĂšge mental classique : une toleration n'attire pas un pod, elle lĂšve juste un
barrage. Un pod qui tolĂšre le taint observability peut aller sur le nĆud obs, mais
il peut tout aussi bien rester sur un autre nĆud sans taint. Pour le forcer sur le
nĆud obs, il faut en plus un nodeSelector. Taint + toleration + nodeSelector forment
le trio complet d'un placement dédié.
Pourquoi le nĆud est tachĂ© et pas seulement Ă©tiquetĂ©
Un label seul (sans taint) dirait « ce nĆud est l'obs » mais n'empĂȘcherait personne d'autre de s'y installer. Le taint est ce qui en fait une chasse gardĂ©e : sans lui, le scheduler bourrerait le nĆud obs avec n'importe quoi dĂšs que le nĆud core est plein, et on reperdrait l'isolation qu'on a payĂ©e (#114).
2. Anatomie d'un taint
# PosĂ© sur un nĆud (chez nous : par le managed node group EKS, via Terraform)
kubectl taint nodes <node> workload=observability:NoSchedule
# âââ key âââ âââ value âââ âââ effect âââ
Trois effects, du plus doux au plus dur :
| Effect | Sur un pod sans toleration |
|---|---|
PreferNoSchedule |
le scheduler Ă©vite ce nĆud si possible (prĂ©fĂ©rence molle) |
NoSchedule |
le pod ne sera pas schedulĂ© sur ce nĆud (mais ceux dĂ©jĂ lĂ restent) |
NoExecute |
le pod est refusé ET les pods déjà présents sont évincés |
Notre choix #114 : NoSchedule. Le cluster est greenfield Ă chaque aws-start
(infra éphémÚre détruite le soir) : il n'y a jamais de pod « déjà là à évincer » au
moment oĂč les nĆuds montent. NoExecute serait inutilement violent ; PreferNoSchedule
trop laxiste (on veut une vraie chasse gardée, pas une préférence). NoSchedule est le
point d'équilibre.
3. Anatomie d'une toleration : Equal vs Exists
Une toleration matche un taint. C'est le champ operator qui change toute la logique
de matching.
operator: Equal (match exact)
tolerations:
- key: workload
operator: Equal # défaut si omis
value: observability
effect: NoSchedule
TolĂšre uniquement le taint workload=observability:NoSchedule. Il faut que key,
value et effect correspondent. C'est ce qu'on met sur les workloads épinglés à l'obs
(prometheus, grafana, alertmanager, loki, kube-state-metrics, prometheus-operator), en
binĂŽme avec leur nodeSelector.
operator: Exists (match sur l'existence)
Exists ne regarde pas la value, seulement la présence d'un key :
| Forme | TolĂšre... |
|---|---|
operator: Exists + key: workload |
tout taint dont le key = workload, quelle que soit la value |
operator: Exists + key: workload + effect: NoSchedule |
idem, mais limité à l'effect NoSchedule |
operator: Exists SANS key |
TOUS les taints : n'importe quel key, n'importe quelle value, n'importe quel effect |
C'est cette derniĂšre forme, le joker absolu, qu'on utilise pour les DaemonSets :
prometheus-node-exporter:
tolerations:
- operator: Exists # <-- aucun key, aucune value : "je tolĂšre n'importe quel taint"
« operator: Exists, mais quel key/value doit exister ? »
Aucun, et c'est tout l'intĂ©rĂȘt. Sans key, le Exists ne teste l'existence de
rien en particulier : il dit littéralement « peu importe ce qui est taché sur le
nĆud, j'y vais ». Si on prĂ©cisait un key, alors il faudrait que ce key existe
sur le taint du nĆud pour que la toleration matche. Le joker nu, lui, matche tout.
4. nodeSelector vs nodeAffinity
Deux façons de forcer un pod vers une catĂ©gorie de nĆud :
nodeSelector(ce qu'on utilise) : le plus simple, une Ă©galitĂ© de labels stricte.Hard : si aucun nĆud ne porte ce label, le pod restenodeSelector: workload: observabilityPending. C'est voulu pour nous (un pod obs DOIT ĂȘtre sur le nĆud obs, sinon on a un problĂšme Ă voir, pas Ă masquer).nodeAffinity: plus riche (opĂ©rateursIn/NotIn/Exists, rĂšglesrequireddures oupreferredsouples avec un poids). Utile pour « de prĂ©fĂ©rence sur tel type, mais sinon ailleurs ». Overkill ici : on veut du dur et du simple. NotĂ© pour l'entretien (un node group app dĂ©diĂ© avec une prĂ©fĂ©rence souple serait un cas nodeAffinity).
5. Le cas DaemonSet : toleration OUI, nodeSelector NON
Un DaemonSet dĂ©ploie un pod par nĆud (agents de nĆud : mĂ©triques, logs, CNI, CSI). Sa logique de placement est l'inverse d'un Deployment :
flowchart TB
subgraph CORE["nĆud core (sans taint)"]
NE1["node-exporter"]
AL1["alloy"]
end
subgraph OBS["nĆud observability (taint NoSchedule)"]
NE2["node-exporter"]
AL2["alloy"]
end
DS["DaemonSet<br/>toleration: operator Exists<br/>(pas de nodeSelector)"]
DS --> NE1
DS --> NE2
- Pas de nodeSelector : un DaemonSet doit tourner partout. Lui mettre un
nodeSelector: workload=observabilityle confinerait au nĆud obs et on perdrait les mĂ©triques / logs du nĆud core. C'est le piĂšge n°1. - Une toleration quand mĂȘme : sans elle, le pod du DaemonSet serait repoussĂ© du nĆud obs tachĂ©, et on perdrait la tĂ©lĂ©mĂ©trie du nĆud obs cette fois. Donc il faut tolĂ©rer le taint.
- Pourquoi le joker
Existset pas la toleration précise : les deux marchent pour le taint d'aujourd'hui. Mais le joker est future-proof :
| Toleration sur le DaemonSet | node-exporter tourne sur... | Si on ajoute un 3e node group taché autrement |
|---|---|---|
Equal workload=observability (précise) |
core + obs | PAS sur le nouveau nĆud â trou de mĂ©triques |
Exists (joker, notre choix) |
tous les nĆuds | sur le nouveau nĆud aussi, sans rien changer |
La rĂšgle Ă retenir
Pour un agent de nĆud (node-exporter, alloy, et plus largement tout DaemonSet
d'infra), on tolĂšre tout (operator: Exists nu) et on ne met jamais de
nodeSelector. C'est le pattern idiomatique : un agent de nĆud doit voir chaque
nĆud, prĂ©sent ou futur, quelle que soit sa raison d'ĂȘtre tachĂ©e.
6. Nos choix concrets pour #114
| Workload | Type K8s | nodeSelector | toleration | Pourquoi |
|---|---|---|---|---|
| prometheus | StatefulSet | workload=observability |
Equal workload=obs |
gros conso mémoire, à isoler du trafic |
| grafana | Deployment | workload=observability |
Equal workload=obs |
idem |
| alertmanager | StatefulSet | workload=observability |
Equal workload=obs |
partie monitoring |
| kube-state-metrics | Deployment | workload=observability |
Equal workload=obs |
partie monitoring |
| prometheus-operator | Deployment | workload=observability |
Equal workload=obs |
partie monitoring |
| loki | StatefulSet | workload=observability |
Equal workload=obs |
logs, jetable |
| node-exporter | DaemonSet | aucun | Exists (joker) |
mĂ©triques par nĆud â partout |
| alloy | DaemonSet | aucun | Exists (joker) |
logs par nĆud â partout |
| fastapi, envoy, argocd, cert-manager, ESO, external-dns | divers | aucun | aucune | le taint obs les repousse tout seul â ils restent sur core |
Le dernier point est subtil : les workloads core ne portent rien. Ils n'ont pas besoin de tolĂ©rer le nĆud obs (ils n'y vont pas) ni d'un nodeSelector pour core (le taint obs suffit Ă les y maintenir, par Ă©limination). Moins de YAML, et c'est correct.
Le gain Spot, chiffré (mesuré le 2026-06-26, eu-west-3)
Le nĆud obs est en Spot : c'est ce qui rend acceptable de le prendre plus gros
(t3.large) pour absorber l'observabilité et Tempo à venir. Prix relevés via
aws ec2 describe-spot-price-history sur les 3 types du node group :
| Type | Spot (le moins cher) | On-demand (â) | Ăconomie |
|---|---|---|---|
t3a.large |
0,0284 $/h (eu-west-3c) | ~0,085 $/h | -67 % |
t3.large |
0,0306 $/h (eu-west-3c) | ~0,094 $/h | -67 % |
m5.large |
0,0323 $/h (eu-west-3b) | ~0,112 $/h | -71 % |
Deux lectures : (1) l'économie ~-70 % annoncée est confirmée par la mesure ; (2)
l'Ă©cart entre tous les prix est serrĂ© (0,028 â 0,041 $/h), signe d'un marchĂ© Spot
stable sur ces types en eu-west-3 â peu d'interruptions attendues. C'est
prĂ©cisĂ©ment l'intĂ©rĂȘt de diversifier les types : AWS pioche le moins cher et le
plus disponible (ici t3a.large en 3c) sans qu'on ait Ă choisir Ă la main.
7. Comment vérifier en live
# Les labels des nĆuds
kubectl get nodes -L workload
# Les taints (sur quel nĆud)
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints
# OĂč chaque pod a atterri
kubectl get pods -A -o wide
# Pourquoi un pod est Pending (lire les events : taint non toléré ? selector ?)
kubectl describe pod <pod> | sed -n '/Events/,$p'
# VĂ©rifier qu'un DaemonSet couvre bien tous les nĆuds
kubectl get pods -A -o wide | grep -E "node-exporter|alloy" # 1 par nĆud
8. Grille entretien
- « DiffĂ©rence entre un taint et un nodeSelector ? » â le taint repousse (sur le nĆud), le nodeSelector force (sur le pod) ; ils se complĂštent avec la toleration qui autorise.
- «
operator: Existssans key, ça tolĂšre quoi ? » â tout taint, c'est le joker des DaemonSets. - « Pourquoi un DaemonSet n'a pas de nodeSelector ? » â il doit tourner sur chaque nĆud ; un selector le confinerait et on perdrait la tĂ©lĂ©mĂ©trie ailleurs.
- « NoSchedule vs NoExecute ? » â NoExecute Ă©vince en plus les pods dĂ©jĂ prĂ©sents.
- « Un pod obs est Pending, par oĂč tu regardes ? » â
describeles events : taint non tolĂ©rĂ©, label de nĆud absent, ou capacitĂ© insuffisante (cf capacitĂ©).
Pour aller plus loin
- La stratégie de capacité qui a motivé la segmentation : guide capacité et ADR 026.
- Pourquoi la gouvernance des ressources rend la capacité nécessaire : ADR 025.
- Autoscaling des nĆuds Ă la demande (Karpenter) : Sprint 6.