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.
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.
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