Comprendre le stockage dans Kubernetes

Comment mettre des ressources de stockage externes à la disposition des pods à l'aide de volumes. Comprendre le provisionnement dynamique de ressources de stockage via les ressources StorageClass, PersistentVolume et PersistentVolumeClaim.

Comprendre le stockage dans Kubernetes

Vue d'ensemble

Le système de fichiers des conteneurs est éphémère. Tout ce qui y est stocké pendant l'exécution disparaît après le redémarrage du conteneur. Pour éviter cela et préserver les données des conteneurs du pod Kubernetes après le redémarrage, nous pouvons utiliser les « Volumes ».

Quand on regarde la spécification du pods Kubernetes, il existe un champ « volumes » permettant de déclarer la liste des volumes appartenant à un pod. Chaque conteneur du pod peut ensuite monter un ou plusieurs des volumes déclarés grâce au champ « containers.volumeMounts ». Voici un exemple de manifeste « pod.spec » à titre d'illustration :

(...)
    spec:
      containers:
      - name: nginx
        image: nginx
        volumeMounts:
          - name: websites
            mountPath: /websites
      volumes:
        - name: websites
          emptyDir:     # A temporary directory that shares a pod's lifetime
            medium: ""  # Use the nodes default storage medium to back this dir
                        # Value can also be 'Memory' to use nodes RAM as backend

Les volumes permettent de mettre à disposition des conteneurs de nos pods, des ressources de stockage externes. Kubernetes prend en charge différents types de volumes. Dans l'exemple ci-dessus, nous avons utilisé « emptyDir », qui n'est pas un type de stockage persistant. Il permet de partager des données temporaires entre les conteneurs d'un pod. Pour une liste complète des types de volumes disponibles, consultez la page volumes.

De plus, pour obtenir une liste des options disponibles que nous pouvons utiliser lors du montage de volumes à l'intérieur des conteneurs d'un pod, nous pouvons consulter containers.volumeMounts.

Il est également possible de faire en sorte que les pods utilisent une ressource « PersitentVolume (PV) » existante en référençant le « PersitentVolumeClaim (PVC) » associé dans « pod.spec.volumes » comme suit :

(...)
    spec:
      containers:
      - name: nginx
        image: nginx
        volumeMounts:
          - name: websites
            mountPath: /websites
      volumes:
        - name: websites
          persistentVolumeClaim:
            claimName: websites
            readOnly: false

Nous parlerons de « PV » et de « PVC » dans les sections suivantes.

Provisionnement dynamique de ressources de stockage

Les ressources de stockage (représentées par l'objet « PersitentVolume ») peuvent être provisionnées dynamiquement grâce aux « StorageClass » et « PersitentVolumeClaim ». Voici un schéma décrivant ce provisionnement dynamique. Les paragraphes suivants vous donneront plus d'explications à ce sujet.

Provisionnement des ressources de stockage dynamique k8s.drawio-1

StorageClass

Les ressources StorageClass sont créées par un administrateur de cluster Kubernetes et définissent le type de ressources de stockage (par exemple NFS, disques persistants GCE...) qui peuvent être automatiquement provisionnées une fois demandées par les utilisateurs.

La ressource de stockage sous-jacente est créée par un provisionneur de volume dont le nom est spécifié dans la classe de stockage. Ce provisionneur appelle une API de plugin de volume pour créer la ressource de stockage sous-jacente.

Voici un exemple de manifeste de ressource « StorageClass » :

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs
provisioner: example.com/nfs # External provisioner
parameters:
  server: nfs-server.example.com
  path: /share
  readOnly: "false"
# Allow expansion of the volumes created from this StorageClass
allowVolumeExpansion: true  
# Reclaim policy of the volumes created from this StorageClass
# Determines what happens to the volume when released
reclaimPolicy: Recycle # Cleanup the volume data and make it 
                       # available for use by another claim
                       # Other possible values:
                       #  - Delete: delete the volume (Default)
                       #  - Retain: preserve the volume and its data. The volume
                                   # won't be available for use by another claim

Les provisionneurs pris en charge nativement par Kubernetes sont des provisionneurs internes et les autres des provisionneurs externes.

La page des provisionneurs de volumes Kubernetes présente certains provisionneurs de volumes Kubernetes et leurs plugins de volume associés. Elle indique également si les provisionneurs sont internes et propose des liens vers des exemples de manifestes de ressources StorageClass pour certains d'entre eux.

PersistentVolumeClaim (PVC)

Une demande utilisateur pour provisionner des ressources de stockage à partir d'une « StorageClass » spécifique est réalisée via la ressource PersitentVolumeClaim (« PVC »).

Voici un exemple de manifeste de « PVC » :

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: nfs
  resources:
    requests:
      storage: 3Gi # requested volume size

Si « pvc.spec.storageClassName » n'est pas spécifié ou est vide, la « StorageClass » par défaut du cluster Kubernetes est utilisée.

Chaque « provisionneur » de volume disponible dans le cluster dispose d'un « contrôleur » qui surveille périodiquement les ressources « PersitentVolumeClaim ».

Voici un aperçu simplifié de la manière dont la demande « PVC » est satisfaite par le « provisionneur » :

  • Pour chaque « PVC » créé dans le cluster Kubernetes, le contrôleur consulte le champ « spec.storageClassName ». Il vérifie ensuite l'existence de la ressource « StorageClass » spécifiée. Si elle n'existe pas, aucune action n'est effectuée.
  • Si elle existe et que « StorageClass.provisioner » correspond au nom du provisionneur, le « provisionneur » essaie de trouver un volume (ressourcePersitentVolume ) satisfaisant la requête (même « StorageClass », mode d'accès, taille de stockage supérieure ou égale à ce qui se trouve à l'intérieur du « PVC »...) qui n'est pas déjà associée (ou attachée) à un « PVC »
  • Si le volume est trouvé, il est lié au « PVC » et la requête est satisfaite. S'il est introuvable, le provisionneur tente de créer le volume à l'aide des paramètres spécifiés dans la « StorageClass ».
  • Le provisionneur appelle le plugin de volume approprié afin de créer la ressource de stockage sous-jacente. Si le plugin de volume parvient à créer la ressource de stockage sous-jacente (un disque persistant GCE par exemple), le provisionneur crée la ressource « PersistentVolume (PV) » associée et la lie au « PVC ».
  • Si le plugin de volume ne parvient pas à créer la ressource de stockage sous-jacente, le provisionneur renvoie une erreur à l'utilisateur.

Comme on peut le constater, une fois qu'une requête « PersitentVolumeClaim (PVC) » est satisfaite, une liaison est établie entre le « PVC » et le « PersistentVolume (PV) ». Cette liaison bidirectionnelle est réalisée comme suit :

  • Le « PV » fait référence au « PVC » :
    • « pv.spec.claimRef.name » contient le nom du « PVC »
    • « pv.spec.claimRef.namespace » contient l'espace de nom dans lequel réside le « PVC »
  • Le « PVC » fait référence au « PV » :
    • « pvc.spec.volumeName » contient le nom du « PV »

PersistentVolume (PV)

Le PersistentVolume (« PV ») est un objet représentant la ressource de stockage sous-jacente qui sera réellement utilisée par les pods pour stocker des données.

Voici comment la documentation Kubernetes du PV le définit :

Un volume persistant (PV) est un élément de stockage du cluster qui a été provisionné par un administrateur ou provisionné de manière dynamique à l'aide de classes de stockage. Il s'agit d'une ressource du cluster, tout comme un nœud est une ressource de cluster. Les PV sont des plugins de volume comme les Volumes, mais ont un cycle de vie indépendant de tout pod individuel qui utilise le PV. Cet objet d'API capture les détails de l'implémentation du stockage, qu'il s'agisse de NFS, d'iSCSI ou d'un système de stockage spécifique au fournisseur de cloud

Voici un exemple de manifeste de « PV » :

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs
spec:
  # Name of the StorageClass to which this PV belongs
  storageClassName: nfs
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: nfs-server.example.com
    path: /share
    readOnly: "false"
  volumeMode: Filesystem # or Block. Default: Filesystem
  persistentVolumeReclaimPolicy: Delete # Default for dynamically created PVs

Lors de l'utilisation du provisionnement dynamique de stockage à l'aide de « StorageClass » comme indiqué précédemment (lisez les sections StorageClass et PersitentVolumeClaim), la ressource « PV » est automatiquement créée pour refléter la ressource de stockage sous-jacente provisionnée, puis liée à la demande utilisateur « PVC ».

Provisionnement statique des ressources de stockage

Les ressources « PV » peuvent également être provisionnées manuellement par les administrateurs. Voici un exemple de manifeste permettant de créer une ressource « PV » :

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: nfs-server.example.com
    path: /share
    readOnly: "false"
  volumeMode: Filesystem # or Block. Default: Filesystem
  persistentVolumeReclaimPolicy: Retain # Default for manually created PVs

Si cela se fait de cette façon et qu'un utilisateur souhaite utiliser le « PV » pré-provisionné, il doit créer un « PVC » qui ressemble à ceci :

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 2Gi # requested volume size
  volumeName: nfs # Directly specify the name of the PV to use

Nous aurions également pu utiliser le champ « spec.storageClassName » dans les manifestes de ressources « PV » et « PVC » pour spécifier un nom de ressource « StorageClass » associé à un « provisioner » spécifique. Ce « provisioner » spécifique n'effectue pas de provisionnement dynamique.

Cette option permet d'activer l'extension de volume via le champ « allowVolumeExpansion » de la ressource « StorageClass », car il n'existe pas de champ équivalent pour la ressource « PV ». Voici un exemple de manifeste pour la création de cette ressource « StorageClass » :

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local
provisioner: kubernetes.io/no-provisioner
allowVolumeExpansion: true  

Exemples

Provisionnement dynamique des disques persistants GCE pour les pods GKE

Voici un exemple d'objet « StorageClass » permettant de provisionner dynamiquement des disques persistants dans GCP. Voir classe de stockage gce-pd pour en savoir plus.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: kubernetes.io/gce-pd # Internal provisioner
parameters:
  type: pd-standard
  fstype: ext4
  replication-type: none

Une fois l'objet « StorageClass » créé dans un cluster GKE, la création d'un « PersistentVolumeClaim » référençant le nom de cette « StorageClass » (« standard ») dans son paramètre « spec.storageClassName » crée automatiquement un disque persistant GCE (Google Compute Engine) avec les caractéristiques définies dans la « StorageClass ». Une fois le disque persistant GCE provisionné, la ressource « PersistentVolume » associée est créée et liée à la ressource « PersistentVolumeClaim ».

La taille souhaitée pour la ressource de stockage est spécifiée dans le champ « spec.resources.requests.storage » de la ressource « PersitentVolumeClaim ». Voici un exemple de manifeste d'une ressource « PersitentVolumeClaim » utilisant la « StorageClass » précédente :

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: gce-pd
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: standard
  resources:
    requests:
      storage: 5Gi # requested volume size

Lors de l'utilisation de StatefulSets pour gérer des pods avec de la persistance, la ressource « PersitentVolumeClaim » peut être créée dynamiquement pour chaque pod du « StatefulSet » à l'aide du champ « spec.volumeClaimTemplates » comme suit :

(...)
  volumeClaimTemplates:
  - metadata:
      name: pvc
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "standard"
      resources:
        requests:
          storage: 2Gi # size of the disk

Un nouveau disque persistant GCE (ayant les caractéristiques définies dans la « StorageClass » « standard ») sera automatiquement provisionné pour chaque pod du « StatefulSet ».

Provisionnement de stockage NFS dynamique

Le projet serveur-nfs-ganesha-et-provisionneur-externe permet de déployer facilement un serveur NFS et son provisionneur externe associé. Une fois déployé, la création de « PVC » faisant référence à la « StorageClass » du serveur NFS entraîne automatiquement les opérations suivantes :

  • création d'un export NFS dédié pour le « PVC »
  • création d'un « PV » utilisant l'export NFS dédié
  • liaison entre le « PV » et le « PVC »
Déployer le serveur NFS et le provisionneur externe
  • Prérequis: Helm
  • Notez également que pour cet exemple, nous utilisons le service Kubernetes géré standard de Google Cloud Platform (Google Kubernetes Engine)
  • Sur Google Kubernetes Engine (GKE), la classe de stockage standard permet de provisionner dynamiquement des disques durs sur Google Cloud Platform. Le serveur NFS utilisera cette classe de stockage pour provisionner un disque afin de garantir la persistance des données.
# Add the Helm charts repository
$ helm repo add nfs-ganesha-server-and-external-provisioner https://kubernetes-sigs.github.io/nfs-ganesha-server-and-external-provisioner/
"nfs-ganesha-server-and-external-provisioner" has been added to your repositories

# Update the Helm charts repository
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "nfs-ganesha-server-and-external-provisioner" chart repository
Update Complete. ⎈Happy Helming!⎈

Voici le contenu du fichier « values.yml » que nous avons utilisé pour le déploiement :

replicaCount: 1

persistence:
  enabled: true
  accessMode: ReadWriteOnce
  storageClass: standard
  size: 5Gi

storageClass:
  create: true
  defaultClass: false
  name: nfs
  allowVolumeExpansion: true

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 100m
    memory: 128Mi

N'hésitez pas à ajuster la configuration selon vos besoins, en utilisant la page des paramètres de configuration du provisionneur de serveur nfs. Exécutons maintenant la commande d'installation afin de déployer le serveur NFS et son provisionneur externe associé :

$ helm upgrade --install testnfs-nfs-provisionner -n testnfs nfs-ganesha-server-and-external-provisioner/nfs-server-provisioner -f values.yml --create-namespace

Le nom de la Release « Helm » est « testnfs-nfs-provisionner » et les ressources associées seront créées dans le namespace « testnfs ». Ce namespace sera créé s'il n'existe pas. La commande d'installation ci-dessus est idempotente et peut également être utilisée pour mettre à jour la Release « Helm » après une modification de configuration.

Un aperçu de certaines des ressources créées

Voici les ressources créées après le déploiement :

$ kubectl get all -n testnfs
NAME                                                    READY   STATUS    RESTARTS   AGE
pod/testnfs-nfs-provisionner-nfs-server-provisioner-0   1/1     Running   0          3m36s

NAME                                                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                                                                                     AGE
service/testnfs-nfs-provisionner-nfs-server-provisioner   ClusterIP   10.200.113.168   <none>        2049/TCP,2049/UDP,32803/TCP,32803/UDP,20048/TCP,20048/UDP,875/TCP,875/UDP,111/TCP,111/UDP,662/TCP,662/UDP   3m37s

NAME                                                               READY   AGE
statefulset.apps/testnfs-nfs-provisionner-nfs-server-provisioner   1/1     3m37s
$ kubectl describe sc/nfs -n testnfs
Name:                  nfs
IsDefaultClass:        No
Annotations:           meta.helm.sh/release-name=nfs-provisionner,meta.helm.sh/release-namespace=testnfs
Provisioner:           cluster.local/nfs-provisionner-nfs-server-provisioner
Parameters:            <none>
AllowVolumeExpansion:  True
MountOptions:
  vers=3
  retrans=2
  timeo=30
ReclaimPolicy:      Delete
VolumeBindingMode:  Immediate
Events:             <none>
$ kubectl get pvc -n testnfs
NAME                                                     STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-testnfs-nfs-provisionner-nfs-server-provisioner-0   Bound    pvc-ca361607-af1d-4125-9d76-2235669e0eb0   5Gi        RWO            standard       108s
$ kubectl get pv/pvc-ca361607-af1d-4125-9d76-2235669e0eb0 -n testnfs
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                                            STORAGECLASS   REASON   AGE
pvc-ca361607-af1d-4125-9d76-2235669e0eb0   5Gi        RWO            Delete           Bound    testnfs/data-testnfs-nfs-provisionner-nfs-server-provisioner-0   standard                109s

$ kubectl describe pv/pvc-ca361607-af1d-4125-9d76-2235669e0eb0 -n testnfs
Name:              pvc-ca361607-af1d-4125-9d76-2235669e0eb0
Labels:            topology.kubernetes.io/region=europe-west1
                   topology.kubernetes.io/zone=europe-west1-c
Annotations:       pv.kubernetes.io/migrated-to: pd.csi.storage.gke.io
                   pv.kubernetes.io/provisioned-by: kubernetes.io/gce-pd
                   volume.kubernetes.io/provisioner-deletion-secret-name:
                   volume.kubernetes.io/provisioner-deletion-secret-namespace:
Finalizers:        [kubernetes.io/pv-protection external-attacher/pd-csi-storage-gke-io]
StorageClass:      standard
Status:            Bound
Claim:             testnfs/data-testnfs-nfs-provisionner-nfs-server-provisioner-0
Reclaim Policy:    Delete
Access Modes:      RWO
VolumeMode:        Filesystem
Capacity:          5Gi
Node Affinity:
  Required Terms:
    Term 0:        topology.kubernetes.io/zone in [europe-west1-c]
                   topology.kubernetes.io/region in [europe-west1]
Message:
Source:
    Type:       GCEPersistentDisk (a Persistent Disk resource in Google Compute Engine)
    PDName:     pvc-ca361607-af1d-4125-9d76-2235669e0eb0
    FSType:     ext4
    Partition:  0
    ReadOnly:   false
Events:         <none>
Utilisation du serveur NFS

Testons maintenant le provisionnement dynamique de stockage à partir du serveur NFS pour nous assurer que tout fonctionne correctement.

Pour cela, nous commençons par effectuer une requête de stockage NFS en créant un « PVC » avec « nfs » comme « StorageClass » et « 100Mi » comme taille de stockage. Nous définissons également le mode d'accès au stockage demandé sur « ReadWriteMany », car nous utilisons un stockage NFS et souhaitons que nos charges de travail puissent effectuer plusieurs lectures et écritures sur le système de fichiers. Voici le manifeste du « PVC » :

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: nfstest-pvc
  namespace: testnfs
spec:
  storageClassName: "nfs"
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Mi

Nous créons ensuite un « Deployment » avec « 2 replicas » et faisons en sorte que chaque pod replica utilise le même export de serveur NFS via le « PVC » créé précédemment. L'export NFS sera monté dans le chemin « /nfs » à l'intérieur des conteneurs des pods. Voici le manifeste du « Deployment » :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: testnfs
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      volumes:
        - name: nfs
          persistentVolumeClaim:
            claimName: nfstest-pvc
            readOnly: false
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
        volumeMounts:
          - name: nfs
            mountPath: /nfs

Vérifions maintenant que tout fonctionne correctement après avoir appliqué les manifestes « PVC » et « Deployment » précédents :

# PVC properly created and bounded
$ kubectl get pvc/nfstest-pvc -n testnfs
NAME          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nfstest-pvc   Bound    pvc-e5c89579-8e17-492a-953d-eb1643a32538   100Mi      RWX            testnfs        78s
# Get created PV details
$ kubectl get pv/pvc-e5c89579-8e17-492a-953d-eb1643a32538 -n testnfs -o yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  annotations:
    EXPORT_block: "\nEXPORT\n{\n\tExport_Id = 1;\n\tPath = /export/pvc-e5c89579-8e17-492a-953d-eb1643a32538;\n\tPseudo
      = /export/pvc-e5c89579-8e17-492a-953d-eb1643a32538;\n\tAccess_Type = RW;\n\tSquash
      = no_root_squash;\n\tSecType = sys;\n\tFilesystem_id = 1.1;\n\tFSAL {\n\t\tName
      = VFS;\n\t}\n}\n"
    Export_Id: "1"
    Project_Id: "0"
    Project_block: ""
    Provisioner_Id: 7de05b4f-5d9e-494e-9c03-f67efe86efd0
    kubernetes.io/createdby: nfs-dynamic-provisioner
    pv.kubernetes.io/provisioned-by: cluster.local/testnfs-nfs-provisionner-nfs-server-provisioner
  creationTimestamp: "*****"
  finalizers:
  - kubernetes.io/pv-protection
  name: pvc-e5c89579-8e17-492a-953d-eb1643a32538
  resourceVersion: "809393219"
  uid: fd3e20be-c31d-49a0-b268-12eadd390169
spec:
  accessModes:
  - ReadWriteMany
  capacity:
    storage: 100Mi
  claimRef:
    apiVersion: v1
    kind: PersistentVolumeClaim
    name: nfstest-pvc
    namespace: testnfs
    resourceVersion: "800467992"
    uid: e5c89579-8e17-492a-953d-eb1643a32538
  mountOptions:
  - vers=3
  - retrans=2
  - timeo=30
  nfs:
    path: /export/pvc-e5c89579-8e17-492a-953d-eb1643a32538
    server: 10.200.114.71
  persistentVolumeReclaimPolicy: Delete
  storageClassName: testnfs
  volumeMode: Filesystem
status:
  phase: Released
# Deployment pods running
$ kubectl get pods -n testnfs
NAME                                                READY   STATUS    RESTARTS   AGE 
nginx-deployment-8568f6d5df-2fvq6                   1/1     Running   0          108s
nginx-deployment-8568f6d5df-vjb2g                   1/1     Running   0          108s
(...)

L'export NFS est correctement monté dans chacun des conteneurs. Nous pouvons écrire un fichier dans l'un d'eux et vérifier qu'il est également présent dans le système de fichiers de l'autre :

$ kubectl exec -it pods/nginx-deployment-8568f6d5df-2fvq6 -n testnfs -- /bin/bash
root@nginx-deployment-8568f6d5df-2fvq6:/# df -h
Filesystem                                                      Size  Used Avail Use% Mounted on
(...)
10.200.114.71:/export/pvc-e5c89579-8e17-492a-953d-eb1643a32538  4.9G     0  4.9G   0% /nfs
(...)
root@nginx-deployment-8568f6d5df-2fvq6:/# touch /nfs/test
root@nginx-deployment-8568f6d5df-2fvq6:/# echo "test" > /nfs/test
root@nginx-deployment-8568f6d5df-2fvq6:/# cat /nfs/test 
test

$ kubectl exec -it pods/nginx-deployment-8568f6d5df-vjb2g -n testnfs -- /bin/bash
root@nginx-deployment-8568f6d5df-vjb2g:/# df -h
Filesystem                                                      Size  Used Avail Use% Mounted on
(...)
10.200.114.71:/export/pvc-e5c89579-8e17-492a-953d-eb1643a32538  4.9G     0  4.9G   0% /nfs
(...)
root@nginx-deployment-8568f6d5df-vjb2g:/# cat /nfs/test 
test