TL;DR
Un volume hostPath stocke ses données sur le système de fichiers d’un nœud précis. Si le Pod qui consomme ces données démarre ailleurs, il ne retrouve pas le même contenu.
La propriété spec.nodeAffinity d’un PersistentVolume décrit les nœuds depuis lesquels ce volume peut être utilisé. Lorsqu’un Pod monte un PVC lié à ce PV, le scheduler doit respecter cette contrainte. Dans le lab de l’épisode 040, le PV pv-affinity est attaché au hostname k0c3. Si le Pod PostgreSQL peut être planifié, il ne peut donc l’être que sur k0c3.
Les points essentiels sont les suivants :
- l’affinité est déclarée sur le PV, pas sur le PVC ;
- pour un PV,
nodeAffinityest une contrainte obligatoire avecrequired; - un PVC
Boundne garantit pas que le Pod pourra être planifié ; hostPathest adapté à cette démonstration, pas à un stockage partagé entre plusieurs nœuds ;- une mauvaise valeur de hostname laisse le Pod en
Pending; - la
nodeAffinityd’un PV existant ne se corrige pas comme un simple champ modifiable.
La vidéo de référence
Cette partie de la série Kubernetes v2 termine le bloc consacré aux affinités. Les épisodes précédents appliquaient les règles de placement aux Pods : affinité avec un nœud, rapprochement entre Pods, préférences pondérées et anti-affinité.
L’épisode 040 inverse le point de vue. Le placement ne part plus seulement du Pod. Il part des données. Le volume indique où il est accessible, puis le scheduler place le Pod consommateur au bon endroit.
La démonstration de la vidéo suit trois étapes :
- créer un PV
hostPathlimité au nœudk0c3; - le consommer avec un PVC et un Deployment PostgreSQL ;
- remplacer
k0c3par un nœud inexistant,k0c6, afin d’observer un Pod bloqué enPending.
La vidéo complète les notes du chapitre 040 avec des commandes d’observation et un test d’échec volontaire.
La série complète est disponible dans la playlist Kubernetes v2 : formation et tutoriels pour débuter.
PersistentVolume nodeAffinity : qu’est-ce que c’est ?
Un PersistentVolume, ou PV, représente une capacité de stockage mise à disposition du cluster. Un PersistentVolumeClaim, ou PVC, exprime la demande d’une application : une capacité, un mode d’accès et éventuellement une classe de stockage.
Le Pod ne référence pas directement le PV dans ce lab. Il référence le PVC pvc-volume. Kubernetes lie ce PVC au PV compatible pv-affinity, puis tient compte des contraintes du volume pour planifier le Pod.
La propriété spec.nodeAffinity du PV limite les nœuds depuis lesquels le volume est accessible. La documentation Kubernetes sur la node affinity des PersistentVolumes précise que les Pods utilisant un PV ne sont planifiés que sur les nœuds sélectionnés par cette règle.
Le cœur du manifeste est ici :
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k0c3
Cette expression signifie : le volume peut être utilisé depuis un nœud dont le label kubernetes.io/hostname vaut k0c3.
Le vrai problème que l’affinité du volume résout
Imaginons un cluster avec plusieurs workers. PostgreSQL écrit ses fichiers dans /mnt/data sur k0c3. Si le Pod est recréé sur k0c2 avec un autre répertoire local portant le même chemin, il ne retrouve pas les données de k0c3. Le chemin semble identique dans le manifeste, mais le disque sous-jacent ne l’est pas.
hostPath n’est pas un stockage distribué. Il monte dans le Pod un fichier ou un répertoire appartenant au nœud hôte. La documentation Kubernetes consacrée à hostPath rappelle aussi ses risques de sécurité et les différences possibles entre les nœuds.
Sans contrainte topologique, trois événements peuvent donc se produire :
- le Pod initial écrit ses données sur un premier nœud ;
- le Pod est supprimé ou recréé ;
- le nouveau Pod démarre sur un autre nœud et voit un autre contenu local.
Pour une base de données, ce scénario peut donner l’impression que les données ont disparu. L’affinité portée par le PV évite ce déplacement incohérent : le stockage impose au Pod le nœud compatible.
Affinité du Pod et affinité du PV : deux contrats différents
L’épisode 037 contient un exemple de nodeAffinity placé dans le template d’un Deployment :
spec:
template:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: zone
operator: In
values:
- eu-west1
Ici, le workload demande lui-même un emplacement. Dans l’épisode 040, la contrainte appartient au stockage :
apiVersion: v1
kind: PersistentVolume
spec:
nodeAffinity:
required:
nodeSelectorTerms: []
| Mécanisme | Ressource qui porte la règle | Objectif |
|---|---|---|
spec.template.spec.affinity.nodeAffinity |
Pod ou contrôleur de Pods | Placer le workload selon les labels des nœuds |
spec.nodeAffinity |
PersistentVolume | Limiter les nœuds capables d’accéder au volume |
spec.template.spec.affinity.podAffinity |
Pod ou contrôleur de Pods | Rapprocher des Pods selon leurs labels et leur topologie |
spec.template.spec.affinity.podAntiAffinity |
Pod ou contrôleur de Pods | Éloigner des Pods selon leurs labels et leur topologie |
Ces contraintes peuvent se cumuler. Le scheduler doit alors trouver un nœud qui satisfait à la fois les règles du Pod et celles de tous les volumes qu’il consomme. Si les contraintes se contredisent, aucun placement n’est possible.
Pourquoi l’affinité d’un PV est uniquement required
L’affinité d’un Pod peut exprimer une obligation ou une préférence. Une préférence autorise le scheduler à choisir un autre nœud si le placement idéal n’est pas disponible.
Cette souplesse n’aurait pas le même sens pour un volume local. Si les données sont physiquement accessibles depuis k0c3, placer « de préférence » le Pod sur k0c3 puis l’autoriser à démarrer ailleurs ne rend pas les données accessibles depuis cet autre nœud.
La référence de l’API PersistentVolume expose donc une règle required. La contrainte décrit une propriété d’accès au stockage, pas une simple préférence d’organisation.
Le modèle mental minimal
Le flux complet peut être lu de bas en haut :
/mnt/data sur k0c3
↓
PersistentVolume pv-affinity
↓ nodeAffinity: hostname In [k0c3]
PersistentVolumeClaim pvc-volume
↓ claimName: pvc-volume
Deployment postgres
↓
Pod PostgreSQL éligible uniquement sur k0c3
Chaque ressource a une responsabilité précise :
- l’administrateur déclare le PV et la topologie du stockage ;
- le PVC demande
1GienReadWriteOncedans la classemanual; - le Deployment consomme le PVC sans connaître le chemin physique ni le nœud ;
- le binder associe le PVC à un PV compatible ;
- le scheduler place le Pod sur un nœud compatible avec le volume.
Le YAML décrit l’intention. Le binding et le scheduling produisent l’état réellement observé dans le cluster.
Démonstration : créer le PersistentVolume avec nodeAffinity
Le lab commence par un PV de 1Gi, conservé avec la politique Retain et associé au nom de classe manual :
kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-affinity
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k0c3
hostPath:
path: "/mnt/data"
EOF
Deux champs doivent être lus ensemble :
hostPath.path: /mnt/datadésigne le répertoire présent sur le nœud ;nodeAffinityindique que ce répertoire utile au PV se trouve surk0c3.
Le manifeste original ne renseigne pas hostPath.type. Kubernetes n’effectue donc pas de contrôle préalable sur l’existence ou le type du chemin. Pour rendre le lab déterministe, préparez /mnt/data avec des permissions adaptées sur k0c3, ou étudiez explicitement type: DirectoryOrCreate avant l’application du manifeste. Ce choix dépend du comportement attendu et ne doit pas être ajouté machinalement à un exemple de production.
Le hostname doit correspondre à un label réellement présent dans le cluster. On peut le vérifier avant de créer le PV :
kubectl get nodes --show-labels
Pour une lecture plus ciblée :
kubectl get nodes -L kubernetes.io/hostname
Créer le PersistentVolumeClaim
Le PVC demande la même classe, le même mode d’accès et une capacité compatible :
kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-volume
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
EOF
Dans ce chapitre, le PV est créé manuellement. Il s’agit donc de provisionnement statique. La valeur manual sert à faire correspondre le PV et le PVC. Le lab ne déploie pas de provisioner dynamique.
On peut observer le binding :
kubectl get pv,pvc
kubectl describe pv pv-affinity
kubectl describe pvc pvc-volume
La documentation sur le binding des PersistentVolumes est essentielle pour interpréter le résultat. Un PVC peut être lié à un PV alors que la topologie du volume rend encore le Pod impossible à planifier. Bound valide l’association du claim au volume, pas l’existence d’un nœud satisfaisant toutes les contraintes du futur Pod.
Déployer PostgreSQL sur le volume
Le Deployment du chapitre utilise PostgreSQL 16 et monte le PVC dans le répertoire de données du serveur :
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
env:
- name: POSTGRES_USER
value: "admin"
- name: POSTGRES_PASSWORD
value: "password"
- name: POSTGRES_DB
value: "testdb"
ports:
- containerPort: 5432
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres-data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: pvc-volume
EOF
Le Deployment ne contient aucune nodeAffinity. Il ne mentionne même pas k0c3. La contrainte remonte du PV lié au PVC que le Pod consomme.
Vérifions le résultat :
kubectl get pods -o wide
Si le Pod passe à l’état Running, la colonne NODE doit afficher k0c3. Une indisponibilité du nœud, un manque de ressources, un taint non toléré ou une autre contrainte peut néanmoins le laisser en Pending. Un kubectl describe pod permet de consulter les événements produits par le scheduler.
Les identifiants PostgreSQL sont écrits en clair pour garder le lab court. Ce choix est pédagogique. Dans un autre contexte, les secrets ne doivent pas être intégrés tels quels dans le manifeste du Deployment.
Exposer PostgreSQL et vérifier les données
Les notes exposent le Deployment avec un Service interne :
kubectl expose deployment postgres --port 5432 --target-port 5432
Un client temporaire basé sur PostgreSQL 13 permet ensuite de se connecter au serveur PostgreSQL 16 :
kubectl run --rm -ti client --image=postgres:13 -- bash
psql -h postgres -U admin -d testdb
Après avoir saisi le mot de passe password, créons une table puis listons les relations :
CREATE TABLE xavki (id int);
\dt
Cette vérification montre que l’application écrit bien dans le volume monté sous /var/lib/postgresql/data. Elle ne transforme pas hostPath en stockage hautement disponible : les données restent dépendantes du disque et de la disponibilité de k0c3.
Tester une mauvaise affinité avec un nœud inexistant
La vidéo remplace volontairement k0c3 par k0c6, un hostname absent du cluster. Le PV et le PVC peuvent être créés, mais le Pod PostgreSQL ne trouve aucun nœud compatible.
Le symptôme apparaît avec :
kubectl get pods
kubectl describe pod -l app=postgres
kubectl get events --sort-by=.metadata.creationTimestamp
Le Pod reste en Pending. Pour diagnostiquer ce cas, il faut comparer :
- les valeurs de
spec.nodeAffinitydu PV ; - les labels réellement portés par les nœuds ;
- les autres contraintes éventuelles du Pod ;
- les événements du scheduler.
Un message comme volume node affinity conflict indique que les contraintes du Pod et du volume ne peuvent pas être satisfaites ensemble. Le libellé exact dépend du scénario et de la version du cluster. L’état Pending seul ne suffit donc pas à conclure : les événements restent la source de diagnostic.
Attention à l’immutabilité de nodeAffinity
Après le test avec k0c6, le premier réflexe pourrait être d’éditer le PV pour remettre k0c3. La démonstration montre que le cluster utilisé dans l’épisode refuse cette modification sur le PV existant. C’est aussi le comportement par défaut de Kubernetes.
Depuis Kubernetes 1.35, la feature gate alpha MutablePVNodeAffinity, désactivée par défaut, permet d’autoriser certaines mises à jour de ce champ. Sans activation explicite de cette fonctionnalité, il faut considérer spec.nodeAffinity comme immuable. Les feature gates alpha ne constituent pas un comportement portable à supposer sur tous les clusters.
Il faut corriger la définition puis recréer la ressource concernée, en tenant compte de la politique de rétention et des données présentes. Une suppression de PV n’est jamais une opération anodine. Avant toute action sur un environnement contenant de vraies données, il faut vérifier la politique persistentVolumeReclaimPolicy, l’état du claim et le comportement du backend de stockage.
Le lab utilise Retain. Cette politique demande une gestion manuelle du stockage après libération du PV. Elle évite qu’une suppression du claim entraîne automatiquement la destruction des données par un provisioner, mais elle ne remplace ni une sauvegarde ni une procédure de récupération.
hostPath, local volume et stockage distribué
Ces notions ne sont pas interchangeables.
| Type | Emplacement des données | Topologie | Usage à retenir |
|---|---|---|---|
hostPath |
Chemin du système de fichiers du nœud | Implicite, à encadrer dans ce lab par le PV | Démonstration ou besoins très spécifiques liés au nœud |
Volume local |
Disque, partition ou répertoire local | nodeAffinity requise sur le PV |
Stockage local géré explicitement comme tel |
| Stockage distribué ou réseau | Backend accessible selon le système choisi | Gérée par le driver et la plateforme | Données devant survivre au déplacement entre nœuds, selon les garanties du backend |
Kubernetes documente le PV hostPath pour le développement et les tests sur un cluster à nœud unique. Dans un cluster multi-nœuds, un chemin local ne devient pas partagé parce qu’il porte le même nom partout.
Les volumes locaux fournissent un modèle plus explicite pour un disque ou un répertoire attaché à un nœud. Ils restent toutefois dépendants de ce nœud. Si celui-ci est indisponible, le volume local l’est également.
Pour un besoin de mobilité ou de haute disponibilité des données, il faut étudier un backend adapté : stockage réseau, système distribué ou service fourni par l’infrastructure. Les garanties dépendent alors du driver CSI et du système de stockage, pas de nodeAffinity seule.
Provisionnement statique, dynamique et WaitForFirstConsumer
Le chapitre 040 crée explicitement le PV. C’est du provisionnement statique. À l’inverse, un provisioner dynamique crée un volume à partir d’une StorageClass lorsqu’un PVC le demande.
Pour un stockage soumis à des contraintes de zone ou de nœud, le moment du binding compte. Une StorageClass configurée avec volumeBindingMode: WaitForFirstConsumer retarde la sélection ou le provisionnement du volume jusqu’à ce qu’un Pod consommateur existe.
Le scheduler peut alors prendre en compte les contraintes du Pod avant le choix du volume. La documentation sur les StorageClasses décrit ce comportement.
WaitForFirstConsumer n’est pas un provisioner. Ce champ contrôle le moment de la décision. Le provisionnement dynamique nécessite toujours un driver ou un provisioner capable de créer le volume.
Le chapitre suivant de la série, consacré aux StatefulSets, introduit justement une StorageClass manual avec kubernetes.io/no-provisioner, WaitForFirstConsumer et plusieurs PV hostPath statiques associés à différents nœuds. Ce prolongement ne doit pas être confondu avec le lab PostgreSQL mono-réplique de l’épisode 040.
Quand utiliser PersistentVolume nodeAffinity ?
La nodeAffinity d’un PV est pertinente lorsque l’accès au stockage dépend réellement de la topologie du cluster :
- disque local attaché à un worker ;
- partition ou répertoire disponible sur un nœud précis ;
- volume accessible seulement depuis certains nœuds ou certaines zones ;
- PV statique dont l’administrateur connaît l’emplacement physique.
Elle n’apporte pas à elle seule :
- la réplication des données ;
- la sauvegarde ;
- le basculement du stockage vers un autre nœud ;
- le provisionnement dynamique ;
- la haute disponibilité d’une base de données.
L’affinité protège la cohérence du placement. Elle ne change pas les propriétés du backend.
Checklist de diagnostic
Quand un Pod utilisant un PVC reste en Pending, cette séquence réduit rapidement le champ de recherche :
kubectl get pods -o wide
kubectl describe pod <nom-du-pod>
kubectl get pvc
kubectl describe pvc pvc-volume
kubectl get pv
kubectl describe pv pv-affinity
kubectl get nodes -L kubernetes.io/hostname
kubectl get events --sort-by=.metadata.creationTimestamp
Vérifiez ensuite les correspondances :
- le PVC demande-t-il la même
storageClassNameque le PV ? - la capacité du PV satisfait-elle la demande du PVC ?
- les modes d’accès sont-ils compatibles ?
- le label de nœud utilisé dans
nodeAffinityexiste-t-il ? - une règle portée par le Pod impose-t-elle un autre nœud ou une autre zone ?
- le Pod consomme-t-il bien le claim attendu ?
Le statut du PVC, les événements du Pod et les labels des nœuds doivent être lus ensemble.
Liens utiles
- Vidéo Kubernetes 040 sur YouTube
- Playlist Kubernetes v2 de Xavki
- Dépôt des tutoriels Kubernetes v2
- Notes et manifests de l’épisode 040
- Documentation des PersistentVolumes
- Node affinity des PersistentVolumes
- Référence de l’API PersistentVolume v1
- Feature gates Kubernetes
- Documentation des volumes hostPath
- Provisionnement statique et dynamique
- Volume binding mode et WaitForFirstConsumer
- Documentation des volumes locaux
FAQ
À quoi sert nodeAffinity sur un PersistentVolume ?
Elle indique au scheduler quels nœuds peuvent accéder au volume. Un Pod qui consomme ce PV par l’intermédiaire d’un PVC doit être placé sur un nœud correspondant à la règle.
Faut-il déclarer nodeAffinity sur le PVC ?
Non. Dans ce mécanisme, la contrainte topologique appartient au PV. Le PVC décrit sa demande de stockage, puis le Pod référence le PVC.
Un PVC Bound garantit-il que le Pod va démarrer ?
Non. Bound indique que le PVC est associé à un PV compatible selon les critères de binding. Le scheduler doit encore trouver un nœud qui satisfait les contraintes du Pod et du volume.
Pourquoi le Pod reste-t-il Pending avec un mauvais hostname ?
Le scheduler ne trouve aucun nœud portant la valeur demandée par la nodeAffinity du PV. Il ne peut donc pas placer le Pod consommateur sans violer la contrainte d’accès au volume.
Existe-t-il une nodeAffinity preferred pour les PV ?
L’API du PersistentVolume expose une contrainte required. Pour un stockage lié à une topologie, démarrer le Pod sur un nœud qui ne peut pas accéder aux données n’est pas une solution de repli acceptable.
Peut-on modifier la nodeAffinity d’un PV existant ?
Le test présenté dans la vidéo montre que cette modification est refusée sur le PV existant. C’est le comportement par défaut. Kubernetes 1.35 introduit toutefois la feature gate alpha MutablePVNodeAffinity, désactivée par défaut. Sans cette activation explicite, il faut préparer la correction et recréer la ressource en respectant la politique de rétention et la procédure de gestion des données.
hostPath convient-il à un cluster Kubernetes de production ?
Le type hostPath monte directement un chemin du nœud et présente des limites de portabilité, de sécurité et de disponibilité. Kubernetes documente son usage de PV pour le développement et les tests sur un nœud unique. Un stockage de production doit être choisi selon les besoins de persistance, de réplication, de sauvegarde et de reprise.
WaitForFirstConsumer rend-il le stockage dynamique ?
Non. Il retarde le binding ou le provisionnement jusqu’à l’existence d’un Pod consommateur. La création dynamique du volume dépend toujours d’un provisioner ou d’un driver compatible.
Quelle différence entre hostPath et un volume local ?
Les deux utilisent un stockage attaché à un nœud, mais le volume local représente explicitement cette dépendance dans le modèle PV et exige une nodeAffinity. hostPath expose directement un chemin du système de fichiers avec des risques et des limites supplémentaires documentés par Kubernetes.
Conclusion
La nodeAffinity d’un PersistentVolume relie deux décisions qui ne peuvent pas être séparées : l’emplacement physique des données et le placement du Pod qui les consomme.
Dans le lab, le PV pv-affinity déclare que /mnt/data est utilisable sur k0c3. Le PVC pvc-volume fournit l’abstraction consommée par PostgreSQL. Le Deployment reste indépendant du nom du nœud, tandis que le scheduler récupère la contrainte depuis le volume. Si toutes les autres conditions sont satisfaites, le Pod ne peut être placé que sur k0c3.
Le test avec k0c6 révèle l’autre face du contrat : Kubernetes préfère laisser le Pod en Pending plutôt que de le démarrer loin de ses données. C’est précisément le comportement recherché.
Cette règle garantit la cohérence topologique, pas la haute disponibilité. La suite logique consiste à étudier les StatefulSets, les StorageClasses avec WaitForFirstConsumer et les solutions de stockage capables d’offrir les garanties attendues pour les charges avec état.
Transparence IA
Ce contenu a été partiellement rédigé ou structuré avec l aide d outils d intelligence artificielle, puis relu, corrigé et complété par l auteur avant publication.