Featured image of post Test de CKE, le Kubernetes managé chez Clever Cloud (partie 2 - stockage)

Test de CKE, le Kubernetes managé chez Clever Cloud (partie 2 - stockage)

Ecrit par ~ zwindler ~

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é :

PodCPURAM
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 VolumeSnapshot et crée les VolumeSnapshotContent correspondants
  • Le csi-snapshotter (sidecar dans le provisioner) détecte les VolumeSnapshotContent et 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

Licensed under CC BY-SA 4.0

Vous aimez ce blog ou cet article ? Partagez-le avec vos amis !   Twitter Linkedin email Facebook

Vous pouvez également vous abonner à la mailing list des articles ici

L'intégralité du contenu appartenant à Denis Germain (alias zwindler) présent sur ce blog, incluant les textes, le code, les images, les schémas et les supports de talks de conf, sont distribués sous la licence CC BY-SA 4.0.

Les autres contenus (thème du blog, police de caractères, logos d'entreprises, articles invités...) restent soumis à leur propre licence ou à défaut, au droit d'auteur. Plus d'informations dans les Mentions Légales

Généré avec Hugo
Thème Stack conçu par Jimmy