Publié le 23 octobre 2019, mis à jour le 18 décembre 2023.

Utiliser des volumes sur Kubernetes est une chose, disposer de volumes persistants en est une autre. En particulier si vous souhaitez avoir des volumes en Kubernetes : Volumes persistants NFS en ReadWriteMany. En effet, même les classes de stockage nativement proposées par GCP ou AWS ne le permettent pas.

Dans cet article nous allons voir comment il est possible de mettre en place un montage persistant dynamique de type NFS avec accès en ReadWriteMany:

  • Montage : donner à un conteneur l'accès à un stockage externe
  • Persistant : ce stockage externe continue d'exister après l'arrêt du conteneur.
  • Dynamique : la création et la gestion du cycle de vie de ce stockage externe ne sont pas gérés par l'utilisateur.
  • NFS : Le stockage externe sera mis à disposition via le protocole Network File System
  • ReadWriteMany : accessible en écriture à plusieurs conteneurs simultanément

La figure suivante résume schématiquement ce que nous allons obtenir en terme de ressources Kubernetes :

schema-ressources-kubernetes

Attribution dynamique de stockage externe

Pour réaliser une attribution dynamique d'un espace de stockage externe (PersistentVolume ou PV) de type NFS, nous avons besoin d'un contrôleur de stockage NFS (aussi appelé "provisioner").

En effet, un contrôleur de stockage peut recevoir, par l'intermédiaire d'une classe de stockage (StorageClass), des demandes d'espaces de stockage (PersistentVolumeClaim, ou PVC). Il y répond en créant un PV et en l'attribuant (Bind) à la PVC.

En termes de ressources Kubernetes, on obtient un workflow tel que celui-ci :

  • Une PVC référence une StorageClass
  • La StorageClass est prise en charge par un contrôleur
  • Le contrôleur provisionne un PV et le "bind" à la PVC

Note : selon l'infrastructure Kubernetes mise en place, il est possible qu'un contrôleur de stockage NFS soit déjà nativement présent. Si ce n'est pas le cas, alors il est nécessaire d'en installer un.

Déploiement d'un contrôleur NFS sur Kubernetes

Le moyen le plus simple de procéder est d'utiliser un Chart Helm. Cela repose donc sur :

  • la disponibilité de Helm dans votre cluster Kubernetes (dont l'installation est non couverte ici).
  • l'accès à une connexion à Internet

Vous pouvez alors installer le contrôleur comme suit :

$ helm install stable/nfs-server-provisioner --name nfs-provisioner -f values.yaml

Cette commande permet d'installer un nouveau contrôleur NFS dans votre cluster Kubernetes, nommé nfs-provisioner, à partir du fichier de variables values.yaml.

Il est donc bien sûr nécessaire de créer au préalable ce fichier de variables. L'exemple suivant devrait suffire dans la plupart des cas :

Cet exemple de configuration précise le nom à donner à la StorageClass (celui qui sera utilisé pour la référencer depuis les PVC), et active la persistance, avec une taille demandée de 1 Gio.

Attention : comme les pods, le contrôleur a lui-même besoin d'un stockage persistant.

Car en effet, comme je l'ai déjà mentionné, nous sommes en train de déployer le contrôleur dans notre cluster. Il sera donc, comme tout déploiement dans Kubernetes, déployé sous forme de Deployment, ReplicaSet et Pod. De fait, par défaut il n'aura pas de stockage persistant.

En réalité, par défaut, le Chart Helm est quand même prévu pour utiliser un volume externe persistant, de type EmptyDir. Mais EmptyDir n’est persistant que pour la durée de vie du pod courant. Que se passe-t-il si le pod est détruit et reconstruit sur un autre noeud du cluster ? Dans ce cas, toutes les données du contrôleur sont perdues...

Et qu'advient-il des applications qui des volumes gérés par ce contrôleur ? C'est un problème sur lequel nous reviendront un peu plus tard, mais elle n’auront plus accès à leur stockage. Il convient donc d'éviter cette situation.

Pour l'heure, l'important c'est qu'il faut TOUJOURS définir de la persistance pour le contrôleur NFS au-delà de la terminaison du pod, via la définition des trois paramètres suivants :

  • enabled : true
  • storageclass : la vous choisissez le nom de la StorageClass que vous souhaitez que le provisioner utilise en backend
  • size : une taille qui vous paraît judicieuse vis-à-vis de votre scénario

Vous pouvez ensuite suite exécuter la commande Helm citée un peu plus tôt.

Fonctionnement du contrôleur

Le contrôleur monte son volume externe dans /export/ et crée à l'intérieur une arborescence du type :

Ici le répertoire pvc-* correspond à un PV, exporté en NFS et attaché à une PVC. Ce contrôleur n'en a qu'un, mais il pourrait bien sûr en avoir d'autres.

Et le fichier nfs-provisioner.identity contient un identifiant unique qui est associé sous forme d'annotation à l'objet Kubernetes de type PV. C'est lui qui permet à un contrôleur de savoir quels PVs lui appartiennent, et à quels PVs il ne doit pas toucher.

Grâce à ça il peut gérer le cycle de vie de ses PVs, et laisser tranquille les PVs des autres.

Un dernier point à noter ici est que le contrôleur, lorsqu'il s'arrête, et ce quelle qu'en soit la raison, ne modifie pas les objets PV qu'il a créé. L'intérêt c'est que quand il redémarrera (potentiellement sur un autre nœud, on ne l'oublie pas), alors il pourra poursuivra la gestion de ces PV, dont nos applications auront toujours besoin.

Stockage non persistant du contrôleur

J'ai déjà bien insisté précédemment, le stockage externe du contrôleur doit être persistant au-delà de la durée de vie du pod.

Pour bien l'avoir en tête nous allons ici imaginer que ce ne soit pas le cas, et que le contrôleur soit configuré pour stocker ses données via un PV de type EmptyDir, et qu'il soit détruit et reconstruit sur un autre nœud.

Il redémarre donc "vierge", mais les objets PVs n'ont pas disparu.

Comme les PV n'ont pas disparus, les PVC associées sont toujours là et nos applications vont essayer de les utiliser. Ce qui va simplement échouer...

En effet, ces PV référencent maintenant la nouvelle instance de notre provisioner (car au niveau de Kubernetes, le provisioner s'appelle pareil que l'ancien), lequel a perdu son stockage, et donc toute sa "mémoire des PVs".

Ainsi, non seulement le répertoire que les applications vont essayer de monter n'existe plus, ce qui va empêcher la création des pods associés. Mais en plus le contrôleur a maintenant changé d'identifiant (puisque celui-ci est généré aléatoirement), et il ne reconnaît donc même pas le PV actuel, qui ne sera donc pas détruit...

C'est pour éviter ce genre de scénario qu'il est impératif que le stockage du contrôleur soit persistant.

Si ça arrive, que faire ?

Si vous vous retrouvez dans la situation mentionnée ci-dessus, il n'y a pas grand chose d'autre à faire que de recréer la PVC ("delete" puis "create" donc) afin de forcer l'allocation d'un nouveau PV, lequel sera reconnu par le contrôleur.

Sauf que pour des raisons de sécurité Kubernetes empêche la suppression d’une PVC si elle est en cours d'utilisation par un conteneur...

Vous allez donc devoir procéder comme suit :

  1. Mettre à jour les applications qui utilisent cette PVC pour qu'elle ne l'utilise plus (par exemple basculer temporairement sur un volume type EmptyDir)
  2. Détruire la PVC
  3. Détruire le PV : vous n'avez pas le choix, puisque le contrôleur qui l'a géré à disparu, et le stockage associé également...
  4. Re-créer la PVC, avec le même nom
  5. Un PV va automatiquement lui être attribué par le contrôleur
  6. Faire un rollback de l'application pour qu'elle utilise à nouveau la PVC et bénéficie donc d’un volume persistant de longue durée

Comme vous pouvez le constater, c'est une situation qu'il convient d'éviter si on peut :-)

Utilisation du contrôleur

Après toutes ces explications, on commence à avoir hâte d’utiliser notre nouveau contrôleur de stockage pour déployer des ressources Kubernetes qui l’utilisent.

Pour cela c'est simple car avec le déploiement du Chart Helm une StorageClass a été créée automatiquement, et s'appelle "nfs" dans notre cas (mais vous pouvez bien sûr changer ce nom).

Il suffit donc de créer un fichier yaml pour la PVC, tel que :

Puis de générer l'objet Kubernetes associé :

$ kubectl create -f nfs_claim.yaml

Et finalement de l'utiliser dans la définition d'un objet Kubernetes qui inclut la définition d'un pod, par exemple :

Puis là encore de créer l'objet Kubernetes :

$ kubectl create -f mon_objet_kube.yaml

Vous pouvez à présent vérifier avec un “kubectl describe” que votre objet s’est bien vu affecter un volume persistant NFS, lequel sera accessible en écriture par des conteneurs appartenant à plusieurs pods.

Vous disposez à présent dans votre cluster Kubernetes d’un contrôleur de stockage NFS. Celui-ci permet de créer facilement des volumes persistant NFS accessible en écriture par plusieurs pods simultanément.

Ce contrôleur est par ailleurs configuré pour utiliser lui-même un stockage persistant externe au cluster afin de ne pas perdre les données en cas de destruction de son pod, par exemple lors d’une mise à jour de Kubernetes.