Dans mon précédent expresso, j’ai présenté l’usage de local-static-provisioner. Je voulais également vous parler d’une mésaventure qui m’est arrivée durant mes expérimentations, mais afin de ne pas alourdir le mini-article, j’ai préféré vous en parler dans une page dédiée.

Lorsque j’ai commencé à bricoler avec local-static-provisionner, je me suis beaucoup inspiré à propos de la documentation Talos à propos de local-path-provisioner. Sur cette page, Sidero recommande un patch dans la configuration Talos pour que le kubelet puisse accéder aux dossiers utilisés pour créer des PVCs :

machine:
  kubelet:
    extraMounts:
      - destination: /var/mnt
        type: bind
        source: /var/mnt
        options:
          - bind
          - rshared
          - rw

Pour rappel : nous créons ce point de montage entre le kubelet et l’hôte pour qu’il puisse vérifier que les dossiers utilisés existent et soient accessibles pour créer des PVCs. C’est une étape obligatoire lorsqu’on utilise des PV de type local.

L’option bind permet de monter un répertoire ou un fichier à un autre emplacement, sans altérer le contenu du système de fichiers. C’est l’option qu’on utilise dans la majorité des cas.

Ne vous souciez pas de rshared et rw, ces options sont essentielles, mais ne sont pas intéressantes pour notre étude du jour.

Jusque-là, tout a l’air d’aller. Nous pouvons ajouter notre patch pour que Talos puisse monter notre disque additionnel dans le dossier /var/mnt/sdb :

machine:
  disks:
      - device: /dev/sdb
        partitions:
          - mountpoint: /var/mnt/sdb

J’installe local-static-provisionner et je crée un petit pod avec ma storageClass slow-storage (celle liée au PV de mon disque).

apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
  - name: nginx
    image: nginx:latest
    securityContext:
     privileged: true
    volumeMounts:
    - mountPath: "/usr/share/nginx/html"
      name: nginx-pvc
  volumes:
  - name: nginx-pvc
    persistentVolumeClaim:
      claimName: nginx-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: slow-storage

Logiquement, nous avons bel et bien notre conteneur. Vérifions que notre PVC est bien monté, et créons un fichier à l’intérieur de ce volume :

$ kubectl exec pods/nginx-pod -c nginx -- mount | grep nginx
/dev/mapper/sda6-encrypted on /usr/share/nginx/html type xfs (rw,noatime,attr2,inode64,logbufs=8,logbsize=32k,prjquota)
$ kubectl exec pods/nginx-pod -c nginx -- touch /usr/share/nginx/html/coffee

Supprimons le pod et le PVC et recréons-les avant de vérifier le contenu du volume :

$ kubectl exec pods/nginx-pod -c nginx -- ls /usr/share/nginx/html
coffee

weird

Vous ne rêvez pas : nous retrouvons bien le fichier que nous avons créé même après avoir supprimé ce volume. En y réfléchissant bien, ce n’est pas totalement anormal : comme nous utilisons un PV de type local il reflète le contenu du filesystem de notre nœud (en l’occurrence, ici, c’est le point de montage du disque sdb).

Maintenant, si je vous annonce que local-static-provisionner se charge de nettoyer les fichiers des PVCs supprimés, nous sommes bien faces à un problème. Allons voir le responsable de cette tâche :

$ kubectl get pods -n local-static-provisioner
NAME                                                   READY   STATUS    RESTARTS        AGE
local-static-provisioner-r2h4g                          1/1     Running   0               58m
$ kubectl describe -n local-static-provisioner pods local-static-provisioner-r2h4g
# [...]
Volumes:
  provisioner-config:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      local-pv-local-static-provisioner-config
    Optional:  false
  provisioner-dev:
    Type:          HostPath (bare host directory volume)
    Path:          /dev
    HostPathType:
  slow-storage:
    Type:          HostPath (bare host directory volume)
    Path:          /var/mnt/
    HostPathType:
# [...]

Il possède un volume de type HostPath monté sur /var/mnt/. C’est bien ce que nous avons configuré dans notre fichier Talos. Il devrait alors pouvoir supprimer ce fameux fichier coffee présent dans le volume, non ?

D’ailleurs, dans les logs de celui-ci, nous pouvons voir qu’il a bien appliqué une procédure de nettoyage :

I1001 19:34:48.940953       1 deleter.go:195] Start cleanup for pv local-pv-b6950bf1
I1001 19:34:48.941379       1 deleter.go:266] Deleting PV file volume "local-pv-b6950bf1" contents at hostpath "/var/mnt/sdb", mountpath "/var/mnt/sdb"

Allons-y étape par étape. Est-ce qu’il détecte bien le disque sdb ?

$ kubectl exec -n local-static-provisioner pods/local-pv-local-static-provisioner-r2h4g -c provisioner -- lsblk
# [...]
sdb                253:16   0   60G  0 disk
`-sdb1             253:17   0   60G  0 part  /var/mnt/sdb

Il détecte même le point de montage /var/mnt/sdb. Cohérent puisque nous avons l’hostPath sur /var/mnt/ (donc nous pouvons logiquement accéder à /var/mnt/sdb).

Finalement, ce n’était pas si difficile. Notre fichier coffee n’a plus qu’à disparaître.

$ kubectl exec -n local-static-provisioner pods/local-pv-local-static-provisioner-r2h4g -c provisioner -- rm /var/mnt/sdb/coffee
rm: cannot remove '/var/mnt/sdb/coffee': No such file or directory
command terminated with exit code 1

Oh. Ai-je fait une erreur dans le chemin ?

$ kubectl exec -n local-static-provisioner pods/local-pv-local-static-provisioner-r2h4g -c provisioner -- find /var/mnt/sdb/coffee
/var/mnt
/var/mnt/sdb

Le fichier n’est pas là. Pourtant, nous avons bien vu qu’il était présent dans le volume. Cherchons un peu plus loin en listant les points de montage dans un pod privilégié (autre que celui de local-static-provisioner car il ne possède pas la commande mount) :

$ kubectl exec -n local-static-provisioner pods/my-privilegied-pod -- mount | grep local-pv
/dev/mapper/sda6-encrypted on /var/lib/kubelet/pods/98453aa1-2e8c-4275-ba03-460872b7d4ee/volumes/kubernetes.io~local-volume/local-pv-b6950bf1 type xfs (rw,noatime,attr2,inode64,logbufs=8,logbsize=32k,prjquota)

Nous découvrons un point de montage dans /var/lib/kubelet correspondant à notre PV. C’est la procédure habituelle de kubelet pour mapper un PVC à un pod : il monte le volume dans un dossier temporaire (/var/lib/kubelet/pods/...) avant de faire hériter le pod de ce point de montage.

Ainsi, lorsque nginx-pod a été créé, le kubelet a monté le volume dans son dossier temporaire, puis nginx-pod à récupérer ce point de montage dans le dossier /usr/share/nginx/html.

En d’autres termes, notre disque sdb est monté dans /var/mnt/sdb(nœud talos). Le répertoire /var/mnt(nœud talos) est monté dans /var/mnt(kubelet). En créant un PV local avec la source /var/mnt/sdb, le kubelet monte ce répertoire dans /var/lib/kubelet/pods/... (ce même dossier est utilisé dans le volume du PVC, et donc du pod).

En image, c’est peut-être plus clair :

Talos Mount

Mais en regardant le résultat de la commande mount, le point de montage dans /var/lib/kubelet/pods/... est monté sur sda et non sdb !

C’est là que le bât blesse, d’autant plus qu’étant donné que sda est le disque utilisé par Talos, celui-ci peut être formaté à tout moment (dû à la nature de Talos comme OS immuable).

Notre mission initiale était bien de nettoyer ce fichier, mais cela veut dire qu’il n’y a aucune persistance dans les données stockées dans mes PVs (les fichiers ne sont pas sur les disques additionnels, mais sur le disque principal de mon nœud), je voulais juste supprimer un fichier, mais je me retrouve avec un problème plus profond.


Mais alors, pourquoi est-ce que le kubelet monte le volume sur sda et non sdb ? La réponse est dans une option de montage que nous avons utilisée : bind.

Je trouve la réponse dans la documentation du kernel Linux :

Bind replicates the specified mount. Rbind replicates all the mounts in the tree belonging to the specified mount.

Laissez-moi reformuler : bind monte un répertoire ou un fichier à un autre emplacement. rbind monte un répertoire ou un fichier à un autre emplacement, en répliquant tous les montages de ce répertoire.

Le souci est qu’en créant un montage via bind du dossier /var/mnt (nœud talos) vers /var/mnt (kubelet), il ne verra pas le montage de /dev/sdb dans /var/mnt/sdb. Il se contente uniquement de voir le dossier accueillant le montage, et non le montage en lui-même (ce qui explique pourquoi il peut voir le dossier /var/mnt/sdb, mais pas le fichier /var/mnt/sdb/coffee).

Une seconde preuve de ce comportement est que Talos peut voir le fichier coffee dans /var/lib/kubelet/pods/... mais pas dans /var/mnt/sdb.

Talos peut ainsi voir le fichier présent dans le répertoire du kubelet mais pas dans le point de montage initial :

$ talosctl ls -n node-disk /var/lib/kubelet/pods/98453aa1-2e8c-4275-ba03-460872b7d4ee/volumes/kubernetes.io~local-volume
NODE        NAME
node-disk   .
node-disk   local-pv-b6950bf1
node-disk   local-pv-b6950bf1/coffee

$ talosctl ls -n node-disk /var/mnt/sdb/ -r
NODE        NAME
node-disk   .

Pour résoudre ce problème, il suffit de modifier le patch Talos pour utiliser rbind à la place de bind (et oui, c’est aussi simple que ça) :

machine:
  kubelet:
    extraMounts:
      - destination: /var/mnt
        type: bind
        source: /var/mnt
        options:
          - rbind # <--- Ici
          - rshared
          - rw

Après un redémarrage du nœud, on relance le pod nginx-pod et le fichier coffee n’est plus présent (logique, puisque nous utilisons à présent le disque sdb).

Je vous propose de créer son alter-ego, tea :

$ kubectl exec pods/nginx-pod -c nginx -- touch /usr/share/nginx/html/tea

Vérifions dans le pod de local-static-provisioner que le fichier est bien présent dans le disque sdb :

$ kubectl exec -n local-static-provisioner pods/local-pv-local-static-provisioner-zb27w  -c provisioner ls /var/mnt/sdb
tea

C’est une belle avancée ! Qu’en est-il du résultat de la commande mount ?

$ kubectl exec -n local-static-provisioner pods/my-privilegied-pod -- mount | grep local-pv
/dev/sdb1 on /var/lib/kubelet/pods/b61f8420-5652-4d86-bdbb-10869389b242/volumes/kubernetes.io~local-volume/local-pv-b6950bf1 type xfs (rw,noatime,attr2,inode64,logbufs=8,logbsize=32k,prjquota)

Le point de montage est bien sur sdb, nos données sont à présent persistantes.

Coté Talos, nous pouvons voir le fichier tea dans le point de montage initial et dans le répertoire du kubelet :

$ talosctl ls -n node-disk /var/mnt/sdb
NODE        NAME
node-disk   .
node-disk   tea

$ talosctl ls -n node-disk /var/lib/kubelet/pods/b61f8420-5652-4d86-bdbb-10869389b242/volumes/kubernetes.io~local-volume/local-pv-b6950bf1
NODE        NAME
node-disk   .
node-disk   tea

Comme ultime et dernière vérification, supprimons le pod nginx-pod et le PVC nginx-pvc avant de recréer le pod et vérifier si le fichier tea est bien absent :

kubectl exec pods/nginx-pod -c nginx -- ls -l /usr/share/nginx/html/
total 0

Victoire 😎 ! Je crois que je n’ai jamais été aussi content de voir un répertoire vide.


Malgré la simplicité de la solution, il m’a fallu un certain temps pour comprendre pourquoi mes données n’étaient pas persistantes et pourquoi le nettoyage des PVs ne fonctionnait pas. C’est une erreur que je ne referai pas de sitôt et totalement évitable si j’avais pris le temps de comprendre de lire la documentation des paramètres utilisés dans mon patch Talos.