Skip to content

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: Exists des 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.
    nodeSelector:
      workload: observability
    
    Hard : si aucun nƓud ne porte ce label, le pod reste Pending. 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Ă©rateurs In/NotIn/Exists, rĂšgles required dures ou preferred souples 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=observability le 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 Exists et 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: Exists sans 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 ? » → describe les 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.