Rappel de la partie 1
Dans la partie 1, on avait vu comment créer un cluster CKE, parcouru son anatomie (Exherbo Linux, Cilium, Materia etcd, konnectivity…), mesuré les temps de boot (~57s pour le control plane, excellent), testé les NodeGroups, et activé le CSI.
Dans cette deuxième partie, on va creuser ce qui touche au stockage et à des cas d’usages “un peu” avancés : cycle de vie des volumes, resize online, snapshots, et déploiement de vCluster avec données persistantes.
Activer le CSI et monter son premier volume
Comme on l’a vu en partie 1, le CSI n’est pas activé par défaut, mais un petit coup de CLI corrige ça rapidement :
clever k8s add-persistent-storage moncluster
Cette commande déploie le provisioner Ceph RBD et crée une StorageClass csi-rbd-sc qui devient la classe par défaut.
$ kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION
csi-rbd-sc (default) rbd.csi.ceph.com Delete Immediate true
On notera au passage ALLOWVOLUMEEXPANSION: true : on y reviendra.
Créons un PVC tout bête :
# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mon-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: csi-rbd-sc
$ kubectl apply -f pvc.yaml
$ kubectl get pvc mon-pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES
mon-pvc Bound pvc-2c2f058c-dc85-4006-865c-3437856462f5 1Gi RWO
Le binding prend environ 30 secondes (le temps que le provisioner crée l’image RBD dans le pool Ceph et l’attache). Rien de spécial, c’est du Kubernetes standard.
On monte le volume dans un pod, on écrit un fichier, on supprime le pod, on le recrée : les données sont toujours là. Jusque là, pas de surprise.
vCluster : un cas d’usage concret du stockage persistant
Maintenant qu’on a du stockage persistant, on peut faire des choses intéressantes. Généralement mon troll préféré, c’est de déployer Wordpress parce que évidemment Kubernetes c’est la meilleure plateforme pour déployer un Wordpress (/s).
Bon, cette fois ci j’ai décidé d’innover un peu et on va déployer vCluster (un cluster Kubernetes virtuel qui tourne dans votre cluster).
Note : à quoi ça sert, me direz vous ? Et bien, les usecases sont pas aussi clairs qu’on pourrait penser. vCluster vous donne un cluster Kubernetes complet, isolé, mais qui partage l’infrastructure du cluster hôte, y compris le CSI. En théorie ça permet de donner le cluster-admin à des gens sans donner les clés de l’infra complète à tout le monde. Je suis pas hyper convaincu par cette approche. Un usecase qui me parait moins dangereux, c’est pour des usages de type CI sans devoir monter un cluster complet, mais il y a des limitations aussi.
Installation :
vcluster create test-vcluster -n vcluster-test
Deux options pour s’y connecter :
Option 1 —> port-forward (le plus simple) :
$ vcluster connect test-vcluster -n vcluster-test -- kubectl get nodes
NAME STATUS ROLES AGE
control-plane-c367382a-22b1-4cef-b0cd-372bf83263c4-node Ready <none> 2m
vcluster connect monte un tunnel port-forward via l’API server (comme kubectl port-forward) et exécute la commande dans le contexte du vCluster. Ça marche sur CKE comme sur n’importe quel cluster Kubernetes.
Option 2 —> LoadBalancer (pour un accès durable sans tunnel) :
vcluster delete test-vcluster -n vcluster-test
vcluster create test-vcluster -n vcluster-test --expose --connect=false
Le flag --expose crée un Service LoadBalancer. On récupère l’IP et on génère un kubeconfig autonome :
$ LB_IP=$(kubectl get svc -n vcluster-test test-vcluster -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ vcluster connect test-vcluster -n vcluster-test \
--server https://$LB_IP:443 \
--insecure --print > /tmp/kube-vcluster.config
Le flag --insecure évite les soucis de certificat auto-signé, avec les risques de sécu que ça implique. A vos risques et périls… 💀
$ kubectl --kubeconfig /tmp/kube-vcluster.config get nodes
NAME STATUS ROLES AGE
test-vcluster Ready <none> 2m
Ce node test-vcluster contient tout notre cluster virtuel. Maintenant, la partie intéressante : créer un PVC dans le vCluster :
$ kubectl --kubeconfig /tmp/kube-vcluster.config apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: csi-rbd-sc
EOF
C’est là où c’est sympa : le vCluster délègue la création du volume au cluster hôte, qui le provisionne via Ceph RBD. On a pas besoin de reconfigurer le CSI, les storage classes, les secrets, etc. C’est totalement transparent.
Petite vérification côté CKE :
$ kubectl get pvc -A | grep test-vcluster
vcluster-test pvc-<...> Bound 1Gi RWO csi-rbd-sc
Le PVC est bien créé dans le namespace du vCluster sur le cluster hôte.
Overhead mesuré :
| Pod | CPU | RAM |
|---|---|---|
test-vcluster-0 | ~80m | ~342 Mi |
| coredns (vCluster) | ~2m | ~13 Mi |
C’est tout à fait raisonnable. 342 MiB pour un control plane K8s complet, c’est dans les clous.
Conclusion vCluster : ça marche, y compris avec de la persistance. Si vous avez besoin d’isoler des environnements tout en gardant accès au CSI, vCluster sur CKE est une option fonctionnelle.
Note : il est aussi possible d’exposer le vCluster via un LoadBalancer (flag --expose) pour un accès direct sans tunnel. Pratique si le vCluster doit rester accessible après la fermeture de votre terminal.
Volume expansion : agrandir un volume à chaud
Vous avez une base de données qui grossit, vous avez provisionné 1Gi au départ et maintenant c’est juste. Avec allowVolumeExpansion: true sur la StorageClass, on peut agrandir le volume sans downtime.
kubectl patch pvc mon-pvc -p '{"spec":{"resources":{"requests":{"storage":"2Gi"}}}}'
Côté Ceph, l’image RBD est redimensionnée immédiatement. Le PV passe à 2Gi :
$ kubectl get pv
NAME CAPACITY ACCESS MODES STATUS CLAIM
pvc-<...> 2Gi RWO Bound default/mon-pvc
Mais le PVC côté utilisateur reste à 1Gi et le filesystem XFS n’a pas encore été agrandi. Le kubelet finira par détecter le changement et déclencher le NodeExpandVolume tout seul (la synchronisation est périodique), mais on peut accélérer en supprimant et recréant le pod qui monte le volume :
$ kubectl delete pod mon-app --force --grace-period=0
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: mon-app
spec:
containers:
- name: app
image: alpine
command: ["sleep", "3600"]
volumeMounts:
- mountPath: /data
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: mon-pvc
EOF
Dès que le volume est monté, le kubelet détecte qu’il a grandi et appelle le driver CSI pour un NodeExpandVolume. Le filesystem XFS est agrandi à chaud (xfs_growfs) et le PVC passe effectivement à 2Gi :
$ kubectl exec mon-app -- df -h /data
Filesystem Size Used Avail Use% Mounted on
/dev/rbd0 1.9G 47M 1.9G 1% /data
$ kubectl get pvc mon-pvc
NAME STATUS CAPACITY ACCESS MODES
mon-pvc Bound 2Gi RWO
VolumeSnapshots : le plus technique
Dernière fonctionnalité, et pas la plus simple : les snapshots de volumes.
Le driver Ceph RBD supporte les snapshots nativement. On le vérifie en regardant les logs du sidecar csi-snapshotter au moment de la création :
$ kubectl logs -n kube-system deployment/csi-rbdplugin-provisioner -c csi-snapshotter --tail=5
I0524 19:50:53.470970 snapshot_controller.go:339] createSnapshotWrapper: Creating snapshot for content snapcontent-<...> through the plugin ...
Pas d’erreur côté driver, la demande est acceptée et traitée.
Mais sur CKE, rien n’est pré-installé pour utiliser les snapshots. Il faut assembler les briques soi-même :
Étape 1 : installer les CRDs et le snapshot-controller
Les CRDs volumesnapshot* ne sont pas déployées sur CKE. Il faut les installer depuis le projet upstream external-snapshotter, ainsi que le snapshot-controller :
VERSION=v8.4.0
# CRDs
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$VERSION/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$VERSION/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$VERSION/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
# Snapshot-controller (RBAC + deployment)
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$VERSION/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$VERSION/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
Deux composants travaillent ensemble :
- Le snapshot-controller (standalone) écoute les objets
VolumeSnapshotet crée lesVolumeSnapshotContentcorrespondants - Le csi-snapshotter (sidecar dans le provisioner) détecte les
VolumeSnapshotContentet appelle le driver Ceph RBD pour créer/supprimer les snapshots
Les deux sont nécessaires.
Étape 2 : le piège des GroupSnapshot CRDs
Une fois les CRDs installées, vous créez votre VolumeSnapshotClass, votre VolumeSnapshot, et… rien ne se passe. Le VolumeSnapshotContent reste readyToUse: false indéfiniment.
Si vous regardez les logs du sidecar csi-snapshotter dans le provisioner, vous ne voyez aucune tentative de création de snapshot.
Pourquoi ? Parce que le sidecar est lancé avec un feature gate activé :
--feature-gates=CSIVolumeGroupSnapshot=true
Ce feature gate active le support des VolumeGroupSnapshot (la possibilité de snapshotter plusieurs volumes en une fois). Mais le sidecar attend les CRDs correspondantes pour initialiser son cache. Sans ces CRDs, le cache sync est bloqué, et le controller ne traite aucun VolumeSnapshot, pas même les snapshots unitaires (normaux, pas “group” quoi).
La solution : installer les CRDs groupsnapshot* :
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$VERSION/client/config/crd/groupsnapshot.storage.k8s.io_volumegroupsnapshotclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$VERSION/client/config/crd/groupsnapshot.storage.k8s.io_volumegroupsnapshotcontents.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$VERSION/client/config/crd/groupsnapshot.storage.k8s.io_volumegroupsnapshots.yaml
Puis redémarrer le provisioner pour que le sidecar recharge sa config. Attention : le provisioner a une anti-affinity qui empêche deux pods de cohabiter sur le même node. Sur un cluster ALL_IN_ONE (1 node), ça bloque le rollout. Il faut supprimer directement l’ancien pod :
kubectl delete pod -n kube-system -l app=csi-rbdplugin-provisioner --force
Ce problème est spécifique aux clusters ALL_IN_ONE. Sur DEDICATED_COMPUTE ou DISTRIBUTED (multi-nodes), l’anti-affinity ne sera probablement pas bloquante.
Étape 3 : le secret Ceph dans le VolumeSnapshotClass
Une fois le sidecar relancé, les logs montrent… une erreur :
rpc error: code = Internal desc = provided secret is empty
Le driver Ceph RBD a besoin des credentials Ceph pour créer un snapshot, exactement comme pour créer un volume. Vous les trouverez dans le secret csi-rbd-secret du namespace kube-system. La StorageClass les référence déjà pour le provisionning :
parameters:
csi.storage.k8s.io/provisioner-secret-name: csi-rbd-secret
csi.storage.k8s.io/provisioner-secret-namespace: kube-system
Mais ces paramètres ne sont pas reportés automatiquement dans le VolumeSnapshotClass. Il faut les ajouter manuellement :
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-rbd-snapclass
driver: rbd.csi.ceph.com
deletionPolicy: Delete
parameters:
clusterID: <votre-cluster-id>
csi.storage.k8s.io/snapshotter-secret-name: csi-rbd-secret
csi.storage.k8s.io/snapshotter-secret-namespace: kube-system
Le clusterID est celui de la StorageClass (visible dans kubectl get sc csi-rbd-sc -o yaml).
Étape 4 : le snapshot fonctionne
$ kubectl apply -f - <<EOF
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: mon-snapshot
spec:
volumeSnapshotClassName: csi-rbd-snapclass
source:
persistentVolumeClaimName: mon-pvc
EOF
$ kubectl get volumesnapshot mon-snapshot
NAME READYTOUSE SOURCEPVC RESTORESIZE AGE
mon-snapshot true mon-pvc 1Gi 6s
READYTOUSE: true immédiatement. Le snapshot Ceph RBD est créé en quelques secondes.
Étape 5 : restaurer un volume depuis un snapshot
Pour être VRAIMENT sûr que ça fonctionne, on peut faire un test (et oui, ça fonctionne)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-restored
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
dataSource:
name: mon-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
storageClassName: csi-rbd-sc
$ kubectl apply -f pvc-restored.yaml
$ kubectl get pvc pvc-restored
NAME STATUS VOLUME CAPACITY
pvc-restored Bound pvc-... 1Gi
Bilan global
Deuxième partie, deuxième bilan. Qu’est-ce qu’on retient ?
Les points forts :
- CSI Ceph RBD : là encore, un bon choix. Ceph, ça fonctionne bien dans Kubernetes. Provisionning, persistence, resize online — tout fonctionne et ce n’est pas une surprise.
- vCluster + CSI : pour faire plaisir à Akanoa, j’ai fait du kubeception (Kubernetes in Kubernetes) et ça m’a permis de tester une feature de vCluster que j’avais jamais essayée => la délégation transparente du CSI vers le CSI de l’hôte
Les points d’attention :
- Les snapshots ne sont pas “out of the box” : CRDs manquantes, feature gate inattendu, secret à recopier. Je ferai un petit email aux copain⋅es parce que ça me parait dommage de pas l’ajouter
Encore merci à l’équipe Clever Cloud pour m’avoir donné accès en avant-première et pour avoir pris le temps d’échanger sur mes retours. Je ne sais pas s’il y aura un 3ème article ou non. Wait and see ;-P
