K8up est un Kubernetes Operator (c’est-à-dire qu’il ajoute des CRDs) conçu pour faire des backups de volumes dans Kubernetes vers une API S3, et leur restauration dans le sens inverse. L’outil est donc utile pour automatiser les backups, mais s’avère également très pratique pour certaines opérations telles que déplacer une application qu’on aurait déployé dans un namespace (comme celui par défaut « default » de Kubernetes), et qu’on voudrait mettre dans autre namespace (en fait on ne déplace pas, on crée des ressources, on copie des données, puis on supprime les anciennes ressources).
K8up est également capable de faire (une sorte de) backups de bases de données, via ce qu’il appelle des « PreBackupPod » et des « Application-Aware Backups ».
Contexte d’utilisation
Le sujet des backups est à la fois simple et compliqué car il est très dépendant des cas d’usage et des spécificités des applications à sauvegarder, ainsi que des critères (contraintes) que l’on vise.
Dans mon cas, il s’agit d’applications hébergées sur un cluster Kubernetes d’un seul noeud. Elles utilisent pour la plupart des volumes (via des PVCs), avec parfois des bases de données soit en mode fichier comme SQLite, soit en mode client-serveur comme PostgreSQL (qui est mon choix préféré quand il est supporté par l’application). Mon serveur PostgreSQL est hébergé hors Kubernetes.
Ce qui était un projet démarré, il y a quelques années, comme un hobby pour faire de la veille technologique s’est transformé en un ensemble de services et de données sur lesquels je m’appuie au quotidien sans trop y faire attention, mais qui me manquerait en cas de défaillance. J’y stocke mes photos (Immich), mes documents (Paperless), mes « spams » sur ma boîte Gmail (que je synchronise sur Dovecot pour ne plus passer par l’UI de gmail et ne pas atteindre sa limite de stockage), des bookmarks (Shiori), mes dépôts Git personnels et leurs artefacts (Forgejo) et d’autres encore.
Avant d’utiliser K8up, ma stratégie de backup était en grande partie manuelle et épisodique avec l’outil Borg au niveau du système de fichier de l’hôte de Kubernetes.
Les CRDs de K8up que j’utilise et que je décris sont:
Schedule: c’est le type d’objet principal qui permet de planifier un backup (c’est à dire un ensemble de créations de snapshots sur un dépôt Restic, à partir des différents PVCs au sein d’un namespace Kubernetes) et la gestion des snapshots (ne conserver que les N derniers). On peut le voir comme un CronJob spécialisé.Backup: effectue seulement la tâche de backup, immédiatement. Comparable à un Job spécialisé. Utile pour des tests ou pour opérations ponctuelles comme déplacer des volumes entre deux namespaces Kubernetes.PreBackupPod: c’est une forme de hook (point d’extension optionnel). Si présent, K8up lancera les containers spécifiés dans cet objet avant de lancer le job de backup proprement dit, puis les arrêtera à la fin du backup. On peut l’utiliser pour démarrer un container qui contient les outils nécessaires au dump d’une base de données, tel que pg_dump.Restore: on peut se douter que c’est l’inverse deBackup, pour restaurer un volume à partir d’un snapshot Restic.
K8up contient d’autres CRDs, tel que « Archive », qui permet de copier le dernier snapshot d’un dépôt Restic vers un autre dépôt Restic. L’idée est d’effectuer les backups courants en local (ou à proximité), et d’envoyer périodiquement la dernière copie à distance pour un stockage de long terme. Je n’utilise pas ce mécanisme pour le moment.
Alternatives à K8up
Il existe des alternatives. Il semble que celle qui revient le plus souvent dans la communauté open source est Velero, mais il y en a d’autres et je ne prétends pas que K8up se démarque d’une façon ou d’une autre. Ma préférence a été pour K8up car il m’a semblé assez simple d’utilisation, basique et efficace. K8up est essentiellement un wrapper écrit en Go autour de Restic et on a assez vite fait le tour de sa documentation.
Pré-requis
K8up ne fonctionne qu’avec un backend S3. Si le but est vraiment de faire des backups, il s’agira donc typiquement d’un service payant tel que Hetzner, Backblaze, Amazon Glacier, etc. En revanche, pour des opérations de copie interne au cluster, on pourra utiliser Garage, qui expose une API compatible S3 au dessus d’un stockage a priori local (devant un volume Kubernetes).
Les paramètres demandés par K8up pour le backend S3 sont:
- le endpoint de l’API S3
- un nom de bucket et optionnellement un sous-chemin si on souhaite organiser ses backups en différents dépôts Restic (repositories), ce qui me semble être à conseiller (à moins d’isoler au niveau des buckets)
- un couple d’Access Key ID et Access Key Secret (accès au S3)
- un mot de passe pour le dépôt chiffré Restic
Pour l’illustration, je propose d’installer Garage, mais cette étape peut être ignorée si vous disposez déjà d’un backend S3 prêt à l’emploi.
Il est également possible de publier des métriques vers Prometheus. Il est pour cela nécessaire de déployer également Prometheus Pushgateway qui agit comme un cache de métriques que Prometheus doit collecter. K8up pourra ainsi publier ses métriques vers Prometheus Pushgateway à chaque exécution de tâche. Ce composant est toutefois optionnel.
Installation de Garage
Garage se présente essentiellement comme un serveur exposant une API HTTP compatible S3. Son déploiement dans Kubernetes est donc très classique. La documentation est ici.
Dans mon cas, j’utilise Garage pour des copies temporaires, au sein d’un cluster composé d’un seul noeud, avec des manifestes semblables à ceux-ci:
apiVersion: v1
kind: Namespace
metadata:
name: garage
labels:
app.kubernetes.io/name: garage
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: garage-data
namespace: garage
labels:
app.kubernetes.io/name: garage
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 10Mi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: garage-meta
namespace: garage
labels:
app.kubernetes.io/name: garage
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: garage-config
namespace: garage
labels:
app.kubernetes.io/name: garage
data:
garage.toml: |
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
metadata_auto_snapshot_interval = "6h"
replication_factor = 1
data_fsync = true
compression_level = 2
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
# rpc_secret is not secret because there are no RPC in single node
rpc_secret = "42531fe1c82ac55a6c73b7f91dd3caefb0685a3e944c335ad3738a24353f69d0"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.garage"
index = "index.html"
[admin]
api_bind_addr = "[::]:3903"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: garage-deployment
namespace: garage
labels:
app.kubernetes.io/name: garage
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: garage
template:
metadata:
labels:
app.kubernetes.io/name: garage
spec:
volumes:
- name: garage-config
configMap:
name: garage-config
- name: data-volume
persistentVolumeClaim:
claimName: garage-data
- name: meta-volume
persistentVolumeClaim:
claimName: garage-meta
containers:
- name: garage
image: dxflrs/garage:v2.2.0
ports:
- containerPort: 3900
name: s3api
- containerPort: 3902
name: s3website
- containerPort: 3903
name: webadmin
volumeMounts:
- mountPath: /etc/garage.toml
name: garage-config
subPath: garage.toml
- name: data-volume
mountPath: /var/lib/garage/data
- name: meta-volume
mountPath: /var/lib/garage/meta
---
apiVersion: v1
kind: Service
metadata:
name: s3
namespace: garage
labels:
app.kubernetes.io/name: garage
spec:
selector:
app.kubernetes.io/name: garage
type: ClusterIP
ports:
- name: s3api
port: 3900
protocol: TCP
targetPort: 3900
---
apiVersion: v1
kind: Service
metadata:
name: admin
namespace: garage
labels:
app.kubernetes.io/name: garage
spec:
selector:
app.kubernetes.io/name: garage
type: ClusterIP
ports:
- name: webadmin
port: 3903
protocol: TCP
targetPort: 3903
Une fois déployé, il faut configurer Garage avec au moins un bucket S3 et un compte d’accès à ce bucket. Cela se fait en exécutant une ligne de commande à l’intérieur du pod déployé.
On commence par identifier le pod avec:
kubectl get pods -n garage
Puis on peut exécuter des commandes depuis le pod identifié ainsi:
kubectl exec --stdin --tty -n garage garage-deployment-8755d6dd7-v4l6k -- ./garage status
Ce qui donne quelque chose tel que:
INFO garage_net::netapp: Connected to 127.0.0.1:3901, negotiating handshake...
INFO garage_net::netapp: Connection established to <GARAGE_ID>
==== HEALTHY NODES ====
ID Hostname Address Tags Zone Capacity DataAvail Version
<GARAGE_ID> garage-deployment-8755d6dd7-v4l6k 127.0.0.1:3901 NO ROLE ASSIGNED v2.2.0
La première étape est de créer un « cluster layout », qui consiste à indiquer à Garage qu’il fonctionne sur un seul noeud:
kubectl get pods -n garage
kubectl exec --stdin --tty -n garage garage-deployment-8755d6dd7-v4l6k -- ./garage layout assign -z garage -c 1G <GARAGE_ID>
kubectl exec --stdin --tty -n garage garage-deployment-8755d6dd7-v4l6k -- ./garage layout apply --version 1
La commande est à adapter avec l’ID de votre instance Garage. L’option -c 1G définit une capacité de 1Go mais celle-ci est ignorée dans le cas d’un cluster d’un seul noeud.
On crée ensuite un bucket dédié à K8up:
kubectl get pods -n garage
kubectl exec --stdin --tty -n garage garage-deployment-8755d6dd7-v4l6k -- ./garage bucket create my-k8up-bucket
Puis sa clé d’accès destinée à notre client K8up:
kubectl get pods -n garage
kubectl exec --stdin --tty -n garage garage-deployment-8755d6dd7-v4l6k -- ./garage key create k8up-app-key
kubectl exec --stdin --tty -n garage garage-deployment-8755d6dd7-v4l6k -- ./garage bucket allow --read --write --owner my-k8up-bucket --key k8up-app-key
Garage est maintenant prêt à être utilisé par K8up.
Installation de K8up
L’installation est classique via Helm. La documentation est ici.
helm repo add k8up-io https://k8up-io.github.io/k8up
kubectl create namespace k8up
helm install k8up k8up-io/k8up --namespace k8up --set k8up.skipWithoutAnnotation=true
L’option --set k8up.skipWithoutAnnotation=true sert à personnaliser une valeur par défaut du Helm chart afin de désactiver le backup de tous les PVCs d’un namespace, sauf s’ils sont explicitement marqués avec une annotation k8up.io/backup: 'true'.
Noter que la documentation est parfois contradictoire sur la gestion des CRDs: c’est parce que celle-ci a bougé plusieurs fois entre une intégration au Helm chart et une installation manuelle. Au moment de rédiger cet article, il est dit qu’ils sont intégrés au Helm chart.
Plus tard, s’il faut mettre à jour le helm chart, il faudra faire (option --set à adapter si vous personnalisez d’autres valeurs du Helm chart):
helm repo update
helm upgrade k8up k8up-io/k8up --namespace k8up --set k8up.skipWithoutAnnotation=true
Une fois déployé, l’utilisation de K8up se fait en déployant des manifestes de type « Backup », « Schedule » ou « Restore ».
Installation de Prometheus Pushgateway
Comme indiqué avant, ce composant est optionnel mais pratique si on souhaite collecter des métriques de backups via Prometheus.
Il s’agit d’un serveur qui expose une API, donc très classique à déployer:
apiVersion: v1
kind: Namespace
metadata:
name: prom-pushgateway
labels:
app.kubernetes.io/name: prom-pushgateway
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: pushgateway-deployment
namespace: prom-pushgateway
labels:
app.kubernetes.io/name: pushgateway
spec:
replicas: 1
selector:
matchLabels:
app: pushgateway
template:
metadata:
labels:
app.kubernetes.io/name: pushgateway
spec:
containers:
# https://github.com/prometheus/pushgateway
- name: pushgateway
image: prom/pushgateway:v1.11.2
ports:
- containerPort: 9091
name: webapi
startupProbe:
httpGet:
path: /-/ready
port: webapi
initialDelaySeconds: 3
periodSeconds: 5
failureThreshold: 30
readinessProbe:
httpGet:
path: /-/ready
port: webapi
initialDelaySeconds: 3
periodSeconds: 60
failureThreshold: 1
livenessProbe:
httpGet:
path: /-/healthy
port: webapi
initialDelaySeconds: 3
periodSeconds: 60
failureThreshold: 2
---
apiVersion: v1
kind: Service
metadata:
name: pushgateway-service
namespace: prom-pushgateway
labels:
app.kubernetes.io/name: pushgateway
spec:
type: ClusterIP
ports:
- name: http
port: 9091
protocol: TCP
targetPort: 9091
selector:
app: pushgateway
Une fois déployé, Prometheus Pushgateway est accessible à l’intérieur du cluster K8s à l’adresse http://pushgateway-service.prom-pushgateway:9091. Cette valeur pourra être utilisée avec le paramètre promURL d’un manifeste de K8up (pour les types « Backup », « Schedule » ou « Check »).
Il reste à modifier le fichier de configuration de Prometheus pour ajouter cette « target ». L’exemple ci-dessous contient seulement la partie concernée de ce fichier. De plus, il faudra l’adapter à votre cas pour l’URL.
scrape_configs:
- job_name: "pushgateway"
scheme: http
honor_labels: true
# honor_labels: must be true for prometheus pushgateway (special)
static_configs:
- targets: ["<URL_pushgateway>"]
Important: si Prometheus est déployé dans K8s, l’URL est simplement celle du service de type « ClusterIP ». S’il est à l’extérieur, il faudra exposer un service de type « NodePort » ou bien configurer un ingress tel que Traefik que je ne documente pas ici.
Une fois configuré, Prometheus peut être redémarré et la nouvelle « target » doit être visible rapidement dans la page de statut (« http://<URL_prometheus>:9090/targets »).
Il existe un template de dashboard pour Grafana dans le dépôt GitHub de K8up, ainsi qu’une version publiée mais non maintenue sur le site de Grafana.
Backups planifiés
Pour planifier le backup des PVCs d’un namespace on commencera par ajouter une annotation k8up.io/backup: 'true' aux PVCs et on déploiera un manifeste de type « Schedule ». L’annotation n’est pas nécessaire si vous n’avez pas utilisé l’option --set k8up.skipWithoutAnnotation=true lors du déploiement de K8up. Pour ma part, je préfère que les backups de PVCs soient explicites. Pour certaines applications, je ne souhaite sauver que certains volumes et pas d’autres.
Par exemple, j’ai un namespace « dovecot » qui contient un PVC « dovecot-data ». Le template « Schedule » ci-dessous va planifier un backup vers Garage (en réalité, on voudra probablement faire un backup distant, mais c’est pour l’illustration).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dovecot-data
namespace: dovecot
labels:
app.kubernetes.io/name: dovecot
annotations:
k8up.io/backup: 'true'
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 15Gi
---
apiVersion: k8up.io/v1
kind: Schedule
metadata:
name: dovecot-backup-pv
namespace: dovecot
spec:
podSecurityContext:
# DO omit podSecurityContext section if app is writing data with root user account.
# Else, you need to adapt these values with the ones set in the app deployment.
runAsUser: 1000
runAsGroup: 1000
fsGroup: 65534
backend:
s3:
endpoint: http://s3.garage.svc:3900
bucket: my-k8up-bucket/dovecot
accessKeyIDSecretRef:
name: dovecot-secrets
key: S3_ID
secretAccessKeySecretRef:
name: dovecot-secrets
key: S3_SECRET
repoPasswordSecretRef:
name: dovecot-secrets
key: K8UP_REPO_PASSWORD
backup:
schedule: '@daily-random'
failedJobsHistoryLimit: 2
successfulJobsHistoryLimit: 2
promURL: http://pushgateway-service.prom-pushgateway:9091
check:
schedule: '@weekly-random'
promURL: http://pushgateway-service.prom-pushgateway:9091
prune:
schedule: '@weekly-random'
retention:
keepLast: 5
keepDaily: 14
Noter la présence de secrets que je ne documente pas ici. Quelle que soit la méthode que vous utilisiez pour déployer ces secrets (par exemple External Secrets Operator), du point de vue de ce manifeste, il s’agit de secrets standards Kubernetes et l’utilisation reste la même.
Un point d’attention particulier est la section podSecurityContext: dans la plupart des cas, cette section peut être omise, sauf si l’application est basée sur un container non root auquel cas il faut réutiliser les mêmes valeurs de manière cohérente entre le pod de l’application, le backup et le restore.
Le template d’exemple ci-dessus planifie un backup quotidien à une heure aléatoire gérée par K8up (mais fixe chaque jour). On peut connaître l’heure exacte via la commande suivante:
kubectl describe schedule dovecot-backup-pv -n dovecot
On peut voir les heures planifiées tout en bas (notation cron, https://crontab.guru/):
Effective Schedules:
Generated Schedule: 15 15 * * 3
Job Type: prune
Generated Schedule: 7 7 * * *
Job Type: backup
Generated Schedule: 48 12 * * 0
Job Type: check
Au moment de son exécution, K8up déploiera un manifeste de type « Backup » qui est essentiellement une ligne de commande (comparable à un Job) basée sur Restic.
Backup de base de données via une commande
K8up a le concept de PreBackupPod et de Application-Aware Backups, qui consiste à spécifier une ligne de commande à exécuter pour, par exemple, créer un dump de base de données. Cette partie de documentation de K8up n’est malheureusement pas toujours claire et va probablement évoluer. En attendant, ce qui a bien fonctionné pour moi est d’utiliser « PreBackupPod » afin de lancer un container avec l’outil pg_dump de PostgreSQL.
PreBackupPod avec pg_dump (PostgreSQL)
Voici un exemple pour l’application Forgejo, où je souhaite faire un backup des volumes et de la base PostgreSQL:
apiVersion: k8up.io/v1
kind: PreBackupPod
metadata:
name: forgejo-backup-db
namespace: forgejo
spec:
backupCommand: sh -c 'PGHOST="${PGHOST}" PGPORT="${PGPORT}" PGDATABASE="${FORGEJO__database__NAME}" PGUSER="${FORGEJO__database__USER}" PGPASSWORD="${FORGEJO__database__PASSWD}" pg_dump'
fileExtension: ".sql"
pod:
spec:
containers:
- env:
- name: PGHOST
valueFrom:
configMapKeyRef:
name: forgejo-configmap
key: PGHOST
- name: PGPORT
valueFrom:
configMapKeyRef:
name: forgejo-configmap
key: PGPORT
- name: FORGEJO__database__NAME
valueFrom:
configMapKeyRef:
name: forgejo-configmap
key: FORGEJO__database__NAME
- name: FORGEJO__database__USER
valueFrom:
configMapKeyRef:
name: forgejo-configmap
key: FORGEJO__database__USER
- name: FORGEJO__database__PASSWD
valueFrom:
secretKeyRef:
name: forgejo-secrets
key: DB_PASSWORD
imagePullPolicy: IfNotPresent
image: docker.io/postgres:17
name: postgres
command:
- 'sleep'
- 'infinity'
---
apiVersion: k8up.io/v1
kind: Schedule
metadata:
name: forgejo-backup-pv
namespace: forgejo
spec:
podSecurityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 65534
backend:
s3:
endpoint: http://s3.garage.svc:3900
bucket: my-k8up-bucket/forgejo
accessKeyIDSecretRef:
name: forgejo-secrets
key: S3_ID
secretAccessKeySecretRef:
name: forgejo-secrets
key: S3_SECRET
repoPasswordSecretRef:
name: forgejo-secrets
key: K8UP_REPO_PASSWORD
backup:
schedule: '@daily-random'
failedJobsHistoryLimit: 2
successfulJobsHistoryLimit: 2
promURL: http://pushgateway-service.prom-pushgateway:9091
check:
schedule: '@weekly-random'
promURL: http://pushgateway-service.prom-pushgateway:9091
prune:
schedule: '@weekly-random'
retention:
keepLast: 5
keepDaily: 14
Noter que le « PreBackupPod » lance un container basé sur l’image postgres. Cette image est plus lourde que nécessaire car elle contient tout le moteur de serveur de bases de données PostgreSQL alors que nous n’utilisons que l’outil pg_dump. Une alternative est d’utiliser une image non officielle avec uniquement les outils clients pour PostgreSQL.
On notera également la commande sleep infinity qui sert à remplacer le point d’entrée par défaut de l’image qui, dans le cas de postgres, tenterait de démarrer un serveur PostgreSQL et échouerait car la configuration est incomplète.
Le snapshot créé dans Restic portera le chemin « /forgejo-postgres.sql » qui correspond au namespace « forgejo » et au nom du container dans le « PreBackupPod » (le snapshot contient donc un seul fichier). Tandis que les backups de PVC sont stockés dans « /data/<PVC_NAME> » (leur contenu est donc vu comme un répertoire). Il est ainsi facile de retrouver ses fichiers dans les snapshots (décrit plus bas dans cet article). Il faudra cependant noter que K8up restaure très facilement les backups de PVC, tandis que pour les dumps de base de données, il semble plus prudent de le gérer soit même.
Backup et restauration manuelle de PVC pour déplacer une application dans un namespace
Si on souhaite déplacer une application dans un namespace, on doit en fait effectuer un nouveau déploiement. Cela pose problème avec les volumes car ils ne sont pas « déplaçables ». Il faut donc faire un backup, créer les nouvelles ressources dont les PVCs, et restaurer le backup. K8up et Garage sont donc bien adaptés à cette tâche.
Cela peut être un peu fastidieux mais c’est assez simple à réaliser (il faut évidemment faire très attention pour ne pas perdre de données personnelles).
Je reprendrai dovecot comme exemple car je l’avais initialement installé dans le namespace « default » et j’ai souhaité le déplacer dans un namespace dédié « dovecot ».
En premier, j’ai arrêté l’application avant de lancer le backup manuel, afin de réduire les risques d’écriture pendant le backup. J’ai effectué le backup du volume (après lui avoir appliqué un label utilisé par le backup pour le sélectionner), j’ai créé le nouveau PVC dans le nouveau namespace « dovecot », puis j’ai restauré les données dans ce nouveau volume. Enfin j’ai créé les autres ressources composant le déploiement de dovecot dans son nouveau namespace. Le serveur a pu redémarrer depuis son nouvel emplacement, avec toutes ses données.
Arrêter l’application avant de lancer le backup
Dans Kubernetes, on n’arrête pas une application, on la « scale » à 0:
kubectl scale --replicas=0 deployment/dovecot-deployment -n default
Label du volume et backup manuel
Pour effectuer la backup d’un groupe de volumes particulier, on peut utiliser les labels. Cela permet de sélectionner le ou les volumes au cas où il y en ai d’autres à ignorer dans le même namespace.
Pour le PVC, il suffit de redéployer son manifeste (pour l’exemple on suppose que seule la section « labels » a été ajoutée au manifeste ci-dessous):
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dovecot-data
namespace: default
labels:
app.kubernetes.io/name: dovecot
annotations:
k8up.io/backup: 'true'
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 15Gi
N’étant pas certain que cela suffise, j’ai voulu m’assurer que le label était aussi appliqué au volume existant, et comme il avait déjà été créé, je l’ai appliqué manuellement via cette commande:
kubectl get pv,pvc -n default
kubectl label pv pvc-46bfad0f-46ad-4bde-b8d7-f509ceb02e9c app.kubernetes.io/name=dovecot
Pour lancer le backup vers Garage, j’ai déployé ce manifeste:
apiVersion: k8up.io/v1
kind: Backup
metadata:
#https://docs.k8up.io/k8up/2.13/how-tos/backup.html#_target_specific_pvcs_or_prebackuppods
name: dovecot-backup-garage
namespace: default
spec:
podSecurityContext:
# DO omit podSecurityContext section if app is writing data with root user account.
# Else, you need to adapt these values with the ones set in the app deployment.
runAsUser: 1000
runAsGroup: 1000
fsGroup: 65534
labelSelectors:
- matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- dovecot
backend:
s3:
endpoint: http://s3.garage.svc:3900
bucket: my-k8up-bucket/dovecot
accessKeyIDSecretRef:
name: dovecot-secrets
key: S3_ID
secretAccessKeySecretRef:
name: dovecot-secrets
key: S3_SECRET
repoPasswordSecretRef:
name: dovecot-secrets
key: K8UP_REPO_PASSWORD
Une fois déployé, le backup devrait démarrer immédiatement (pour autant que le label mentionné ait bien été appliqué au PVC et au PV). Toutefois, si ce n’est pas le cas, il faut vérifier qu’il n’y a pas d’erreur ou d’oubli au niveau du filtre sur le label et de l’annotation k8up.io/backup: 'true'; puis il faut supprimer la tâche de backup avec la commande kubectl delete backup dovecot-backup-garage -n default avant de la recréer. On pourra aussi aller voir les logs du pod K8up qui peut contenir des pistes.
Restauration manuelle dans un autre volume
Une fois le backup terminé (à surveiller dans les logs du backup exécuté plus tôt), on peut créer le nouveau namespace et un PVC à l’intérieur de celui-ci:
apiVersion: v1
kind: Namespace
metadata:
name: dovecot
labels:
app.kubernetes.io/name: dovecot
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dovecot-data
namespace: dovecot
labels:
app.kubernetes.io/name: dovecot
annotations:
k8up.io/backup: 'true'
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 15Gi
Le manifeste de la commande de restauration est alors le suivant:
apiVersion: k8up.io/v1
kind: Restore
metadata:
name: dovecot-restore-garage
namespace: dovecot
spec:
podSecurityContext:
# DO omit podSecurityContext section if app is writing data with root user account.
# Else, you need to adapt these values with the ones set in the app deployment.
runAsUser: 1000
runAsGroup: 1000
fsGroup: 65534
restoreMethod:
folder:
claimName: dovecot-data
paths: ["/data/dovecot-data"]
backend:
repoPasswordSecretRef:
name: dovecot-secrets
key: K8UP_REPO_PASSWORD
s3:
endpoint: http://s3.garage.svc:3900
bucket: my-k8up-bucket/dovecot
accessKeyIDSecretRef:
name: dovecot-secrets
key: S3_ID
secretAccessKeySecretRef:
name: dovecot-secrets
key: S3_SECRET
La restauration sera lancée immédiatement après le déploiement de ce manifeste.
Les paramètres importants du manifeste sont folder.claimName et paths. Le premier sert à identifier le PVC vers lequel restaurer (dans le même namespace que le manifeste). Le second sert à filtrer le snapshot Restic à utiliser pour la restauration. Dans le cas où il n’y a qu’un seul volume dans le dépôt Restic, ce n’est pas indispensable. Mais s’il y en a plusieurs, K8up sélectionnera le plus récent sans faire de distinction s’il y a plusieurs volumes (ce problème est expliqué dans cette issue GitHub). Le chemin correspond à celui créé par le backup, à savoir le nom du PVC dans le dossier « /data ». Si le PVC initialement sauvé est nommé « dovecot-data », le chemin est « /data/dovecot-data ».
Une alternative plus précise est d’identifier le snapshot ID à restaurer et d’utiliser le paramètre snapshot. Un exemple basé sur un snapshot ID est dans la documentation de K8up.
Une fois déployé, on vérifiera dans les logs de la restauration que tout s’est bien déroulé (le nombre de fichiers et le poids total sont indiqués, comme lors du backup).
Déploiement de l’application dans le nouveau namespace
Je ne vais pas détailler le déploiement ici car l’exemple suppose qu’on disposait déjà d’une application fonctionnelle. Le nouveau déploiement consiste à créer une copie des anciens manifestes et d’adapter le nom du namespace cible. Si tout se passe bien, l’application redéployée retrouvera bien ses données dans le nouveau volume du namespace de destination.
On pourra supprimer l’ancien déploiement dans le namespace d’origine.
Explorer son dépôt Restic
Configuration d’accès
La documentation officielle est ici.
Il suffit de télécharger le CLI Restic, préparer les informations d’accès, et utiliser les bonnes commandes.
Pour rappel, il nous faut connaître ces informations d’accès:
- le endpoint de l’API S3
- un nom de bucket et optionnellement le chemin dans lequel on a organisé ses dépôts
- un couple d’Access Key ID et Access Key Secret (accès au S3)
- un mot de passe pour le dépôt chiffré Restic
Restic permet différentes méthodes pour définir ses paramètres, soit en ligne de commande, soit en variables d’environnement, soit en fichiers (non exclusif).
Dans mon cas j’ai préféré stocker tous les paramètres dans leur fichier respectif, par exemple:
Le fichier « repository » avec l’URL du dépôt au format de Restic, c’est-à-dire avec le préfixe « s3: ». Dans cet exemple, il s’agit d’un S3 publique (si on souhaite explorer le S3 de Garage, il faudra exposer un service à l’extérieur du cluster, avec un ingress tel que Traefik):
s3:https://...your-objectstorage.com/my-k8up-bucket/dovecot
Le fichier « s3_secret »:
# AWS_SHARED_CREDENTIALS_FILE
[default]
aws_access_key_id = <KEY_ID>
aws_secret_access_key = <KEY_SECRET>
Le fichier « password »:
<PASSWORD>
A partir de là, on peut charger ces paramètres dans des variables d’environnement utilisées par Restic, au sein d’une session Powershell:
$env:RESTIC_REPOSITORY_FILE = "repository"
$env:AWS_SHARED_CREDENTIALS_FILE = "s3_secret"
$env:RESTIC_PASSWORD_FILE = "password"
Dans la même session Powershell, il est possible d’explorer ses snapshots avec des commandes simples.
Lister le contenu
.\restic.exe snapshots
repository REDACTED opened (version 2, compression level auto)
created new cache in REDACTED
ID Time Host Tags Paths
-------------------------------------------------------------------------
cc0a692e 2026-04-04 23:33:22 default /data/dovecot-data
e1b10424 2026-04-05 00:06:35 default /data/dovecot-data
-------------------------------------------------------------------------
2 snapshots
Si on avait effectué un backup de plusieurs PVCs dans le même dépôt, on aurait un snapshot par PVC avec le nom du PVC en chemin. Dans l’exemple plus haut, « dovecot-data » est le nom du PVC. L’hôte correspondra au nom du namespace de K8s qui contenait le PVC, dans mon cas il s’agissait de « default ».
Pour lister le contenu d’un snapshot (attention, le résultat peut être énorme selon le contenu, tous les fichiers seront listés):
.\restic.exe ls --long e1b10424
.\restic.exe ls --long latest
.\restic.exe ls --long latest /data/dovecot-data/REDACTED/mail/
Contrôle d’intégrité
Pour vérifier l’intégrité du dépôt:
.\restic.exe check
Noter que cela se limite à quelques métadonnées. Pour vérifier le contenu des données, voir les différentes options --read-data et --read-data-subset dans la documentation de Restic (cela entraîne des téléchargements qui peuvent être facturés sur un dépôt S3 distant, selon le service utilisé).
Restauration locale
On peut extraire le contenu d’un snapshot dans un chemin local avec ce type de commande:
.\restic.exe restore latest --target $env:TEMP\test
Pour tester la restauration sans l’exécuter, on peut ajouter l’option --dry-run (peut être utile pour vérifier le volume de données que l’on va devoir télécharger.
Il est possible de filtrer les fichiers à restaurer, pour cela se référer à la documentation.
Supprimer un snapshot
Noter que K8up le gère dans le manifeste de type « Schedule ». Pour le faire manuellement, on peut supprimer physiquement tous les snapshots, au delà d’un certain nombre des plus récents:
.\restic.exe forget --keep-last 1 --prune