Certaines applications déployées dans un cluster Kubernetes utilisent des volumes persistants (pvc). C’est de celles-là dont nous allons parler dans cet article, et plus précisément du moyen de sauvegarder ces volumes afin d’éviter une perte de données.

Par Maud Laurent, administratrice système @ Objectif Libre

Public visé : Administrateurs Kubernetes

Kubernetes : sauvegarder ses applications Stateful

Pourquoi ce concentrer sur les applications Stateful ?
Parce que les applications Stateless sont uniquement construites avec des fichiers yaml, qui peuvent être aisément archivés, versionnés dans un dépôt comme git, et déployés automatiquement par un mécanisme de CI/CD. Aucune donnée n’est sauvegardée au sein de cette application. Le peu de configurations nécessaires à son fonctionnement tiennent dans une ConfigMap ou des Secrets.

A contrario, les applications Stateful génèrent des données, elles sont souvent rattachées à des volumes et ce sont ces volumes qui contiennent toutes les informations dont elles ont besoin ou dont d’autres applications ont besoin. Et c’est elles que nous devont sauvegarder !

Les outils pour faire des sauvegardes

Pour sauvegarder des volumes depuis Kubernetes, il existe actuellement deux applications : Velero et Stash.

Velero est un outil de sauvegarde qui ne se contente pas de sauvegarder les volumes : il permet de sauvegarder la totalité de son cluster (pods, services, volumes…) avec tout un système de filtres par labels ou objets Kubernetes.

Stash est un outil se concentrant uniquement sur la sauvegarde de volume.

Ces deux applications utilisent le même support pour gérer les sauvegardes : Restic. Restic est un programme de gestion de sauvegarde facilitant l’enregistrement et la restauration des données. Il chiffre les données sauvegardées pour garantir la confidentialité et l’intégrité de ces dernières.

Restic is built to secure your data against such attackers, by encrypting it with AES-256 in counter mode and authenticating it using Poly1305-AES. (source: https://restic.net/)

Notre cas de test

Pour expliciter, prenons un exemple et sauvegardons avec ces deux outils notre application. L’application exécutée est un simple serveur nginx publié au travers d’un service en NodePort. Les logs du serveur sont enregistrés dans un volume persistant.

Le cluster utilisé est un cluster « 1 master 2 workers », avec une solution de stockage Ceph objet (s3) et bloc installée à l’aide de Rook.

L’application Nginx est déployée à l’aide des yaml suivants : un yaml de déploiement, un yaml pour le service et un yaml pour le volume. Ces fichiers sont disponibles ci-dessous.

La commande suivante crée le namespace de test, et lance les yamls de déploiement.
kubectl create ns demo-app && kubectl apply -n demo-app -f pvc-log-1Gi-no-ns.yml -f deployment-no-ns.yml -f service-no-ns.yml

Ci-dessous, le fichier yaml utilisé pour créer le déploiement de l’application sur Kubernetes.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: demo-app
  name: demo-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
      name: web-app
    spec:
      containers:
      - args:
        name: nginx-app
        image: nginx
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - mountPath: /var/log/nginx/
          name: log-data
      restartPolicy: Always
      volumes:
      - name: log-data
        persistentVolumeClaim:
          claimName: demo-app-log

Ci-dessous, le fichier yaml utilisé pour créer le volume persistant (pvc) sur Kubernetes.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: demo-app-log
spec:
  storageClassName: rook-ceph-block
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

Ci-dessous, le fichier yaml utilisé pour créer le service sur Kubernetes.

apiVersion: v1
kind: Service
metadata:
  name: demo-app
  labels:
    app: demo-app
spec:
  selector:
    app: demo-app
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: NodePort

Pour générer un peu de logs Nginx, nous allons utiliser cette commande watch -n 30 curl IP_Master:NodePort et la laisser tourner dans un coin.
Notre volume stocke les logs de l’application Nginx. Avec la commande kubectl suivante : kubectl exec -it -n demo-app demo-app-5b955c984d-x7pqf -c nginx-app -- wc -l /var/log/nginx/access.log nous pouvons compter le nombre de lignes dans notre fichier access.log et ainsi voir les accès à notre page web. Nous vérifierons lors des restaurations leur bon fonctionnement avec cette commande.

Velero

Installation

	[default]
  aws_access_key_id = Your_Access_Key
  aws_secret_access_key = Your_Secret_Key
  • Installer Velero avec la ligne de commande (ajouter --dry-run -o yaml pour voir le yaml appliqué) :
  velero install --provider aws \
	--bucket velero-backup \
	--secret-file ./velero-creds \
	--use-volume-snapshots=true \
	--backup-location-config region=":default-placement",s3ForcePathStyle="true",s3Url=http://rook-ceph-rgw-my-store.rook-ceph.svc.cluster.local \
	--snapshot-location-config region=":default-placement" \
	--use-restic

Ou avec Helm :

  • Créer un secret avec les informations d’accès au stockage kubectl create secret generic s3-velero-creds -n velero --from-file cloud=velero-creds
  • Créer un fichier de paramètres values.yml
configuration:
  provider: aws
  backupStorageLocation:
    name: aws
    bucket: velero-backup
    config:
      region: ":default-placement"
      s3ForcePathStyle: true
      s3Url: http://rook-ceph-rgw-my-store.rook-ceph.svc.cluster.local

credentials:
  existingSecret: s3-velero-creds

deployRestic: true
  • Lancer l’installation avec helm helm-3 install -f values.yml velero --namespace velero --version 2.1.3 stable/velero

Lancement d’une sauvegarde

Pour sauvegarder nos volumes avec Restic, Velero demande d’annoter les pods utilisant ces volumes kubectl -n demo-app annotate pod/demo-app-57f87559b6-jhdfk backup.velero.io/backup-volumes=log-data. Cette annotation peut aussi être mise directement, dans le yaml de déploiement de l’application, dans la partie spec.template.metadata.annotations.

Les sauvegardes peuvent être créées avec la commande velero backup create demo-backup --include-namespaces demo-app ou en appliquant un fichier yaml. (https://velero.io/docs/v1.1.0/api-types/backup/).

apiVersion: velero.io/v1
kind: Backup
metadata:
  name: demo-app-backup
  namespace: velero # must be match velero server namespace
spec:
  includedNamespaces:
  - demo-app
  ttl: 24h0m0s # default 720h0m0s
  storageLocation: default # backup storage location
  volumeSnapshotLocations:
  - default

Avec ce fichier, une seule sauvegarde va être faite, mais il est possible de les programmer avec la commande velero schedule create demo-app-schedule-backup --schedule="@every 5m" --include-namespace demo-app --ttl 0h30m00s ou le yaml ci-dessous.

apiVersion: velero.io/v1
kind: Schedule
metadata:
  name: demo-app-schedule-backup
  namespace: velero
spec:
  schedule: '@every 5m'
  template:
    includedNamespaces:
    - demo-app
    ttl: 0h30m00s

Il est possible de créer plusieurs localisations pour le stockage des sauvegardes et ainsi de choisir où sauvegarder les données.

Pour supprimer une sauvegarde dans velero, il est préférable d’utiliser la ligne de commande velero que de faire une suppression avec kubectl de la resource backup.

Restauration de la sauvegarde

Avant de restaurer un des snapshots enregistrés précédement, nous allons supprimer le namespace de déploiement kubectl delete ns demo-app.

La liste des snapshots est disponible avec la commande velero backup get.

On peut créer une restauration avec la commande velero restore create --from-backup demo-app-backup ou avec un fichier yaml de ce type.

apiVersion: velero.io/v1
kind: Restore
metadata:
  name: demo-app-restore
  namespace: velero
spec:
  backupName: demo-app-backup
  excludeResources:
  - nodes
  - events
  - events.events.k8s.io
  - backups.velero.io
  - restores.velero.io
  - resticrepositories.velero.io

Comme pour les sauvegardes, il est possible de voir l’état de la restauration avec la commande velero restore get et d’avoir plus d’information avec les mots clés describe ou logs.
Une fois la restauration terminée, nous avons de nouveau toutes nos données disponibles. Pour le vérifier, on peut exécuter la commande kubectl get all,pvc -n demo-app.

Dans mon cas, le résultat est le suivant :

NAME                            READY   STATUS    RESTARTS   AGE
pod/demo-app-5b955c984d-x7pqf   2/2     Running   0          109s

NAME               TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/demo-app   NodePort   10.233.33.213           80:31010/TCP   105s

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/demo-app   1/1     1            1           108s

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/demo-app-5b955c984d   1         1         1       108s

NAME                                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
persistentvolumeclaim/demo-app-log   Bound    pvc-7f302738-15ad-4294-8e8e-6407e42f8bf3   1Gi        RWO            rook-ceph-block   110s

On observe que la restauration d’une sauvegarde avec Velero garde le nom exact du pod, que la restauration se fasse dans un namespace différent ou dans le même namespace que le précédent déploiement.
Après la restauration, en exécutant à nouveau la commande kubectl exec -it -n demo-app demo-app-5b955c984d-x7pqf -c nginx-app -- wc -l /var/log/nginx/access.log on peut voir que notre fichier comporte déjà plusieurs lignes alors qu’aucun accès n’a encore été fait sur le serveur web.

Stash

Installation

Stash peut s’installer en utilisant Helm en suivant les indications suivantes :

kubectl create ns stash
helm-3 repo add appscode https://charts.appscode.com/stable/
helm-3 repo update
helm-3 repo install --version 0.8.3 stash --namespace stash appscode/stash

Lancement d’une sauvegarde

L’intégralité de l’archivage de nos sauvegardes se fait sur du stockage s3. Stash a donc besoin d’un secret avec les informations d’authentification à ce stockage. En plus des identifiants, Stash demande un mot de passe pour Restic. Restic enregistre les données sur le stockage s3 en les chiffrant avec ce mot de passe. Ce secret est enregistré dans le namespace du projet à sauvegarder, ce qui permet de spécifier pour chaque projet/namespace un accès s3 et un secret Restic.

apiVersion: v1
kind: Secret
metadata:
  name: s3-secret
  namespace: demo-app
data:
  AWS_ACCESS_KEY_ID: czNfc3RvcmFnZV9rZXlfaWQ= # s3_storage_key_id
  AWS_SECRET_ACCESS_KEY: czNfc3RvcmFnZV9rZXlfc2VjcmV0 # s3_storage_key_secret
  RESTIC_PASSWORD: U3VwM3JSZXN0aWNQd2Q= # Sup3rResticPwd

Stash offre la mise en place de deux types de sauvegarde :

  • une sauvegarde à chaud (en ligne) : Stash ajoute un sidecar conteneur au pod actuel du déploiement. Ce sidecar monte le volume en lecture seule (RO) et effectue les sauvegardes pendant que l’application tourne.
  • une sauvegarde à froid (hors ligne) : Stash ajoute un init conteneur au pod et crée une cronjob Kubernetes. Cette cronjob lance les sauvegardes, le pod va être recréé à chaque sauvegarde pour lancer l’exécution de l’init conteneur.

Dans notre cas, nous allons effectuer une sauvegarde en ligne en appliquant le yaml ci-dessous.

apiVersion: stash.appscode.com/v1alpha1
kind: Restic
metadata:
  name: rook-restic
  namespace: demo-app
spec:
  type: online # default value
  selector:
    matchLabels:
      app: demo-app # Must match with the label of pod we want to backup.
  fileGroups:
  - path: /var/log/nginx
    retentionPolicyName: 'keep-last-5'
  backend:
    s3:
      endpoint: 'http://rook-ceph-rgw-my-store.rook-ceph.svc.cluster.local'
      bucket: stash-backup  # Give a name of the bucket where you want to backup.
      prefix: demo-app  # A prefix for the directory where repository will be created.(optional).
    storageSecretName: s3-secret
  schedule: '@every 5m'
  volumeMounts:
  - mountPath: /var/log/nginx
    name: log-data # name of volume set in deployment not claimName
  retentionPolicies:
  - name: 'keep-last-5'
    keepLast: 5
    prune: true

Quand ce yaml est appliqué, le/les pods sont recréés pour ajouter le sidecar permettant la sauvegarde. Une fois la configuration appliquée, le premier snapshot se lance lors du prochain passage programmé (ici 5 minutes après le déploiement). Les snapshots peuvent être mis sur pause à tout moment en patchant l’objet restic créé à l’aide de la commande kubectl patch restic -n demo-app rook-restic --type="merge" --patch='{"spec": {"paused": true}}'. Les snapshots créés par Stash sont visibles avec la commande kubectl get -n demo-app snapshots.repositories.stash.appscode.com.

Restauration de la sauvegarde

Avant de restaurer nos données, nous allons commencer par supprimer notre déploiement.

Stash a besoin de deux éléments pour restaurer une sauvegarde : le repository et le secret du stockage. Ces deux éléments se trouvent dans le namespace du projet à sauvegarder.

La suppression du namespace entraine la perte de ces deux données. Il est cependant possible de les recréer en ré-appliquant le secret et une configuration de repository pour pouvoir lancer la restauration d’une sauvegarde.

Dans notre cas, nous allons uniquement supprimer notre déploiement, le volume ainsi que notre ressource restic.

kubectl delete -n demo-app deployment demo-app
kubectl delete -n demo-app pvc demo-app-log
kubectl delete -n demo-app restics.stash.appscode.com rook-restic

Pour restaurer le système, nous allons d’abord créer un nouveau volume vide (de taille supérieure ou égale au précédent).

kubectl apply -f pvc-recovery.yml -n demo-app

kubectl get pvc -n demo-app
persistentvolumeclaim/demo-app-log-recovery   Bound    pvc-5a0ba64e-7cf8-49d8-a5a9-ac071160da11   2Gi        RWO            rook-ceph-block   4s

Ensuite, nous allons lancer une restauration. Ce fichier yaml va aller copier les données de la sauvegarde vers le volume précédemment créé. Stash crée un job Kubernetes qui va monter le volume et copier les données à l’intérieur.

apiVersion: stash.appscode.com/v1alpha1
kind: Recovery
metadata:
  name: s3-recovery
  namespace: demo-app
spec:
  repository:
    name: deployment.demo-app
    namespace: demo-app
  snapshot: deployment.demo-app-70b545c2 # snapshot name to restore
  paths: # path want to restore
  - /var/log/nginx
  recoveredVolumes: # where we want to restore
  - mountPath: /var/log/nginx
    persistentVolumeClaim:
      claimName: demo-app-log-recovery
kubectl get recoveries.stash.appscode.com -n demo-app -w
NAME          REPOSITORY-NAMESPACE   REPOSITORY-NAME       SNAPSHOT                       PHASE     AGE
s3-recovery   demo-app               deployment.demo-app   deployment.demo-app-70b545c2   Running   18s
s3-recovery   demo-app               deployment.demo-app   deployment.demo-app-70b545c2   Succeeded   39s

Une fois que la restauration est finie sans erreur, il ne reste plus qu’à appliquer le déploiement précédent en spécifiant le nom du volume restauré.

Alors, Velero ou Stash pour sauvegarder ses volumes persistants ?

Velero

  • Utilise un seul et unique mot de passe pour tous les volumes sauvegardés. Il faut donc être vigilant et restreindre l’accès aux emplacements des sauvegardes.
  • Tous les éléments servant aux sauvegardes ou à la restauration sont enregitrés dans le namespace de Velero. Si un namespace est supprimé, la restauration n’est pas compromise pour autant.
  • Fonctionne avec un système de plugins et hook pour customiser/améliorer les sauvegardes.
  • Offre des métriques pour le monitoring des sauvegardes.
  • La sauvegarde de volume est une extension au système total de sauvegarde qu’offre Velero.
  • Backends supportés : S3 ( AWS, Ceph, Minio…), ABS, GCS. Volume Snapshot providers: AWS EBS, AMD, GCED, Restic, Portworx, DigitalOcean, OpenEBS, AlibabaCloud.
  • Fonctionne très bien pour la migration ou la sauvegarde de projets, mais les droits sont difficiles à gérer : favoriser l’utilisation uniquement par des admins.

Stash

  • Le mot de passe de chiffrement pour Restic est définissable pour chaque namespace du cluster.
  • Les données pour restaurer les sauvegardes sont enregistrées dans le même namespace que le projet à sauvegarder. De ce fait, en cas de suppression du namespace, la restauration est un peu plus complexe, mais reste possible.
  • Offre des métriques pour le monitoring des sauvegardes.
  • Se concentre sur les sauvegardes de volumes (pvc).
  • Backends supportés : S3 (AWS, Ceph, Minio…), ABS, GCS, Openstack Swift, Backblaze B2, Local.
  • Peut être utilisé pour facilement redimensionner un volume persistant.