Sauvegardes Incrémentielles avec zfs send/receive avec des datasets encryptés (script)
Introduction
ZFS est formidable, il offre une possibilité d'effectuer des sauvegardes complètes et incrémentielles avec une étonnante efficience.
Si le grand-père qu'est rsync est formidable, face aux outils send/receive de ZFS, il ne fait pas le poids.
La différence est d'ailleurs absolument phénoménale.
Sur ZFS il n'est donc plus vraiment intéressant de se servir d'rsync (sauf dans de très rares cas).
Lorsqu'rsync est mis en oeuvre pour la première fois, une synchro complète est effectuée, et la même commande va servir ensuite à transférer les différences.
Ce n'est pas le cas avec les outils de ZFS... et ça se complique même carrément avec les datasets encryptés (!).
Bah oui ! Tant qu'à faire, autant sauvegarder des datasets encryptés dans des pools encryptés !
Pour l'exemple
2 serveurs :
- SERVEUR-A
- SERVEUR-B
Le SERVEUR-A est le serveur principal.
Il possède un pool de stockage ZFS (appelons-le MONSUPERPOOL) fonctionnel et à l'intérieur de ce pool, on trouve plusieurs datasets encryptés et montés (dataset1, dataset2, dataset3).
Les clients peuvent s'y connecter de n'importe où pour récupérer ou stocker des données (par exemple).
Le SERVEUR-B quant a lui possède également un pool de stockage prêt à l'emploi, de capacité équivalente ou supérieure (pour pouvoir accueillir toutes les futures données des datasets de A), de même nom (histoire de faire simple) mais sans aucun dataset à l'intérieur (du moins pour l'instant).
Afin de faciliter la compréhension et mettre en oeuvre une solution qui se rapproche de la tolérance aux pannes, nous allons également nommer le pool du SERVEUR-B MONSUPERPOOL.
Pourquoi ? Tout simplement parce qu'il ne sera pas difficile, grâce à cela, de mettre en place une solution de redondance en cas de défaillance d'un des 2 serveurs.
Si les 2 serveurs possèdent exactement les mêmes datasets et le même pool, la bascule en cas de problème sera beaucoup plus rapide et facile à mettre en oeuvre.
Nous allons ici nous cantonner à la réplication des données (pour ce qui est de la bascule entre les serveurs en cas de panne, il existe plusieurs solutions.. je ne vais pas les détailler ici, ce serait une première approche trop complexe d'un seul coup).
Donc les données sont sur A et on doit les transférer sur B et mettre en place une tâche cron pour automatiser le processus quotidiennement (par exemple).
2 façons de faire
- exécuter un script/commande depuis A (récupérant les datasets sur A pour les copier sur B)
- exécuter un script/commande depuis B (récupérant les datasets sur A pour les copier sur B).
Nous allons choisir la deuxième option (exécuter depuis B, pour aller chercher sur A), légèrement plus complexe..
Cette solution permet de ne pas avoir à se soucier du réseau de B. B peut ainsi être laissé pour se configurer automatiquement via DHCP et faire le travail de backup de façon autonome (entendez par-là que vous n'aurez pas à configurer une box internet de merde avec un DNAT pour permettre à B de faire ses sauvegardes).
Un simple accès à internet classique suffira. C'est lui qui ira chercher les données sur A de lui-même.
Bien entendu, libre à vous de modifier le script pour travailler dans le sens inverse !
Nous partons donc du fait que le pool est déjà créé sur B, mais pas les datasets.
Le problème des datasets encryptés, c'est qu'on ne peut pas les copier dans des datasets encryptés existants. Raison pour laquelle je précise qu'il n'y a pas (encore) de dataset sur B.
L'option -F utilisée habituellement pour écraser des datasets non encryptés déjà existants ne fonctionne pas.
Il faut utiliser l'option --raw lors de l'envoi du dataset (vers B) afin de le copier tel quel.
Cela va créer de facto le dataset encrypté automatiquement sur B lors de la première sauvegarde.
Ce dernier pourra alors être monté en chargeant d'abord la clef (la même que sur le dataset sur A) puis en le montant :
(Sur B : )
zfs load-key MONSUPERPOOL/dataset1
zfs mount MONSUPERPOOL/dataset1
(Je me cantonne ici uniquement aux synchro... il vous faudra rajouter les montages au script si vous souhaitez que les datasets de B soient automatiquement montés et exploitables).
Une fois cette première sauvegarde effectuée, on ne pourra plus utiliser la même commande que lors de la première sauvegarde, car il faudra alors réaliser uniquement des incrémentielles.
Un exemple concret (et un rappel entre incrémentiel et différentiel)
- Lundi : Vous faites une première sauvegarde initiale (complète) : snapshot@lundi_initial
- Mercredi : Deux jours plus tard, des données ont été modifiées dans le dataset. Vous réalisez une sauvegarde incrémentielle : snapshot@mercredi
- Vendredi : Deux jours après, des données ont à nouveau été modifiées. Vous réalisez une nouvelle sauvegarde incrémentielle : snapshot@vendredi
Que se passe-t-il cette fois ?
Lors d'une sauvegarde incrémentielle, les différences sont calculées par rapport à la dernière sauvegarde (qu'elle soit initiale ou incrémentielle). Dans ce cas :
- Mercredi : La sauvegarde incrémentielle snapshot@mercredi capture les modifications depuis snapshot@lundi_initial.
- Vendredi : La sauvegarde incrémentielle snapshot@vendredi capture les modifications depuis snapshot@mercredi.
Restauration avec des sauvegardes incrémentielles
- Lundi suivant (J+8) : Vous faites une nouvelle sauvegarde incrémentielle basée sur l'état actuel et les modifications depuis snapshot@vendredi.
- Mercredi suivant (J+10) : Vous faites une nouvelle sauvegarde incrémentielle basée sur l'état actuel et les modifications depuis snapshot@lundi_J+8)
- Vendredi suivant (J+12) : Vous faites une nouvelle sauvegarde incrémentielle basée sur l'état actuel et les modifications depuis snapshot@mercredi_J+12
- Lundi suivant (J+16) : Un collaborateur vient vous voir pour restaurer un fichier supprimé le mardi précédent (J+9). Ce fichier était présent lors de la sauvegarde de lundi (snapshot@lundi_J+8).
Pour retrouver ce fichier, il vous faudra restaurer à partir de snapshot@lundi_initial, puis appliquer les sauvegardes incrémentielles de mercredi (snapshot@mercredi), vendredi (snapshot@vendredi), et lundi suivant (snapshot@lundi_J+8).
La restauration avec des sauvegardes incrémentielles nécessite de restaurer l'initiale (snapshot@lundi_initial) puis d'appliquer chaque sauvegarde incrémentielle jusqu'au point de restauration souhaité.
Différence avec une sauvegarde différentielle
Avec une sauvegarde différentielle, chaque sauvegarde capture toutes les modifications depuis la dernière sauvegarde complète.
Dans notre cas présent, si vous aviez des sauvegardes différentielles à la place des sauvegardes incrémentielles, pour restaurer le fichier du collaborateur, vous n'auriez besoin QUE de la sauvegarde initiale (snapshot@lundi_initial) de celle du lundi_J+8 (snapshot@lundi_J+8)
Après cet interlude, reprenons l'installation de nos serveurs.
Il nous faut donc
- 2 serveurs : SERVEUR-A et SERVEUR-B
- 2 supports stockages pour chaque serveur a minima, l'un pour le système, l'autre pour le pool (a minima)
1) Installation
Une fois installés, sur les 2 serveurs :
- On met l'image à jour :
pkg update && reboot
- On configure ntp pour l'heure Française (ou autre)
pkg install ntpsec && /usr/bin/ntpdate 0.fr.pool.ntp.org && date && svcadm enable svc:/network/ntp:default
- Pour l'exemple nous choisissons des adresses IP pour nos serveurs respectifs :
SERVEUR-A : 10.10.10.1
SERVEUR-B : 10.10.10.2
2) Pool et datasets
- Repérer le support de stockage dévolu au futur zpool sur SERVEUR-A et SERVEUR-B
echo | format -e
(pour l'exemple, c1t2d0. Le disque a le même nom sur les 2 serveurs)
- Créer le zpool encrypté 'MONSUPERPOOL' sur SERVEUR-A et sur SERVEUR-B
zpool create -O encryption=on -O keyformat=passphrase MONSUPERPOOL c1t2d0
Il faut bien noter la passphrase que vous allez entrer !
- Créer un premier dataset encrypté (MONSUPERPOOL/dataset1) à l'intérieur : uniquement sur SERVEUR-A
zfs create -o encryption=on -o keyformat=passphrase -o compression=lz4 MONSUPERPOOL/dataset1
Là aussi, il faut bien noter la passphrase choisie !
- Créer un second dataset encrypté (MONSUPERPOOL/dataset2) à l'intérieur : uniquement sur SERVEUR-A
zfs create -o encryption=on -o keyformat=passphrase -o compression=lz4 MONSUPERPOOL/dataset2
Là aussi, il faut bien noter la passphrase choisie !
- Créer un troisième dataset encrypté (MONSUPERPOOL/dataset3) à l'intérieur : uniquement sur SERVEUR-A
zfs create -o encryption=on -o keyformat=passphrase -o compression=lz4 MONSUPERPOOL/dataset3
Là aussi, il faut bien noter la passphrase choisie !
On s'arrête là pour les datasets... vous comprenez la logique.
Vos datasets sont automatiquement montés sur /MONSUPERPOOL/datasetX
3) Configuration de SSH : connexion par clef
SERVEUR-B doit pouvoir envoyer les snapshots ZFS automatiquement.
Il faut donc mettre en place une connexion SSH de B vers A avec des clefs, car c'est root qui va faire les transactions ! (on peut procéder autrement pour éviter que ce soit root qui fasse le job d'ailleurs.. je vous laisse chercher...)
- Donc d'abord, on autorise la connexion à root sur SERVEUR-A :
sed -i 's/PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config
- On relance ssh :
svcadm restart ssh
Sur SERVEUR-B :
On génère les clefs SSH et on envoie la publique à SERVEUR-A
ssh-keygen -t ed25519 # on ne génère pas de passphrase !!
ssh-copy-id -i /root/.ssh/id_ed25519.pub root@10.10.10.1
On tente une première connexion pour vérifier :
ssh root@10.10.10.1
On peut à présent interdire la connexion par mot de passe à SERVEUR-B et ne plus utiliser que la clef publique.
(assurez-vous, si vous travaillez via SSH depuis votre client, d'envoyer vous aussi votre clef publique à SERVEUR-B avant de couper la connexion par mot de passe !!)
sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/#KbdInteractiveAuthentication yes/#KbdInteractiveAuthentication no/' /etc/ssh/sshd_config
- On relance ssh :
svcadm restart ssh
4) Le script de sauvegarde automatique !
Voyons comment écrire un script unique qui fait tout ! (s'il n'y a jamais eu de sauvegarde sur B, le script doit le détecter et faire cette première sauvegarde intégrale initiale, sinon il devra réaliser des incrémentielles entre l'initiale et l'état actuel.
Allez, on s'accroche ! ça n'est pas très difficile..
On écrit ce script sur B..
Par exemple :
mkdir /SCRIPTS
Mettez ce script : /SCRIPTS/bakfromA.sh
#!/bin/bash
# Liste des datasets à synchroniser (séparés par des virgules)
datasets="dataset1,dataset2,dataset3"
# Configuration des serveurs et chemins
SOURCE_USER="root"
SOURCE_HOST="10.10.10.1"
# Fonction pour synchroniser un dataset
sync_dataset() {
local dataset=$1
SOURCE_DATASET="MONSUPERPOOL/$dataset"
TARGET_DATASET="MONSUPERPOOL/$dataset"
# Vérifier si une sauvegarde (initiale ou incrémentielle) existe déjà sur le serveur B
if ! zfs list -t snapshot -o name | grep -q "^${TARGET_DATASET}@"; then
# Aucune sauvegarde trouvée, effectuer une sauvegarde complète initiale
echo "Aucune sauvegarde trouvée pour ${TARGET_DATASET}. Effectuer une sauvegarde complète initiale."
# Créer un instantané initial sur le serveur A
if [ -z "$(ssh ${SOURCE_USER}@${SOURCE_HOST} zfs list -t snapshot -o name | grep "${SOURCE_DATASET}@initial")" ]; then
ssh ${SOURCE_USER}@${SOURCE_HOST} zfs snapshot ${SOURCE_DATASET}@initial
fi
# Envoyer l'instantané initial au serveur B
ssh ${SOURCE_USER}@${SOURCE_HOST} zfs send --raw ${SOURCE_DATASET}@initial | zfs receive $TARGET_DATASET
else
# Sauvegarde trouvée, effectuer une sauvegarde incrémentielle
echo "Sauvegarde précédente trouvée pour ${TARGET_DATASET}. Effectuer une sauvegarde incrémentielle."
# Créer un nouvel instantané horodaté sur le serveur A
TIMESTAMP=$(ssh ${SOURCE_USER}@${SOURCE_HOST} date +%Y%m%d%H%M%S)
NEW_SNAPSHOT="${SOURCE_DATASET}@${TIMESTAMP}"
ssh ${SOURCE_USER}@${SOURCE_HOST} zfs snapshot $NEW_SNAPSHOT
# Trouver le dernier instantané transféré sur le serveur B
LAST_SNAPSHOT=$(zfs list -t snapshot -o name -S creation | grep "^${TARGET_DATASET}@" | head -n 1)
if [ -z "$LAST_SNAPSHOT" ]; then
# Aucun instantané précédent trouvé, envoyer l'instantané complet
echo "Aucun instantané précédent trouvé pour ${TARGET_DATASET}. Transfert de l'instantané complet."
ssh ${SOURCE_USER}@${SOURCE_HOST} zfs send --raw ${NEW_SNAPSHOT} | zfs receive -F $TARGET_DATASET
else
# Envoyer les différences depuis le dernier instantané
echo "Transfert des différences depuis l'instantané $LAST_SNAPSHOT."
ssh ${SOURCE_USER}@${SOURCE_HOST} zfs send --raw -I ${LAST_SNAPSHOT} ${NEW_SNAPSHOT} | zfs receive -F $TARGET_DATASET
fi
fi
}
# Convertir la liste des datasets en un tableau
IFS=',' read -r -a datasets_array <<< "$datasets"
# Synchroniser tous les datasets indiqués l'un après l'autre
for dataset in "${datasets_array[@]}"; do
sync_dataset "$dataset"
done
On autorise l'exécution du script :
chmod +x /SCRIPTS/bakfromA.sh
A présent, on exécute le script depuis B :
/SCRIPTS/bakfromA.sh
Voici ce qui va logiquement s'afficher :
root@SERVEUR-B:~# /SCRIPTS/bakfromA.sh
Aucune sauvegarde trouvée pour MONSUPERPOOL/dataset1. Effectuer une sauvegarde complète initiale.
Aucune sauvegarde trouvée pour MONSUPERPOOL/dataset2. Effectuer une sauvegarde complète initiale.
Aucune sauvegarde trouvée pour MONSUPERPOOL/dataset3. Effectuer une sauvegarde complète initiale.
root@SERVEUR-B:~#
Etant donné qu'il n'y a jamais eu de sauvegarde, il n'y a pas de dataset1,2,3 sur SERVEUR-B.
Le script les copie donc intégralement depuis SERVEUR-A.
On peut vérifier que tout s'est bien passé sur SERVEUR-B et que les datasets ont bien été transféré :
root@SERVEUR-B:~# zfs list
NAME USED AVAIL REFER MOUNTPOINT
MONSUPERPOOL 621K 30.5G 99K /MONSUPERPOOL
MONSUPERPOOL/dataset1 96K 30.5G 96K /MONSUPERPOOL/dataset1
MONSUPERPOOL/dataset2 96K 30.5G 96K /MONSUPERPOOL/dataset2
MONSUPERPOOL/dataset3 96K 30.5G 96K /MONSUPERPOOL/dataset3
Les datasets transférés ne sont évidemment pas montés, ils ne sont donc pas accessibles.
Pour les monter, il faut disposer de la clef ayant servi à les encrypter lors de leur création, la charger, puis monter le dataset :
zfs load-key MONSUPERPOOL/dataset1
zfs mount MONSUPERPOOL/dataset1
Ajoutons un fichier test sur SERVEUR-A :
- Placez un fichier texte dans l'un des datasets pour l'exemple : uniquement sur SERVEUR-A
echo 'coucou' > /MONSUPERPOOL/dataset1/test.txt
Effectuons une nouvelle sauvegarde depuis SERVEUR-B regardons ce qui se produit :
root@SERVEUR-B:~# /SCRIPTS/bakfromA.sh
Sauvegarde précédente trouvée pour MONSUPERPOOL/dataset1. Effectuer une sauvegarde incrémentielle.
Transfert des différences depuis l'instantané MONSUPERPOOL/dataset1@initial.
Sauvegarde précédente trouvée pour MONSUPERPOOL/dataset2. Effectuer une sauvegarde incrémentielle.
Transfert des différences depuis l'instantané MONSUPERPOOL/dataset2@initial.
Sauvegarde précédente trouvée pour MONSUPERPOOL/dataset3. Effectuer une sauvegarde incrémentielle.
Transfert des différences depuis l'instantané MONSUPERPOOL/dataset3@initial.
root@SERVEUR-B:~#
root@SERVEUR-B:~# ls /MONSUPERPOOL/dataset1
test.txt
root@SERVEUR-B:~#
Le fichier text.txt est bien arrivé !
5) La tâche cron : pour exécuter le script automatiquement
Pour que le script de sauvegarde s'exécute de lui-même, nous pouvons créer une tâche cron sur SERVEUR-B :
Par exemple, tous les jours à 1h du matin :
echo '* 1 * * * /SCRIPTS/bakfromA.sh' >> /var/spool/cron/crontabs/root && svcadm refresh cron
6) La gestion des snapshots ZFS
Au bout d'un moment, il va falloir gérer le nombre de snapshots accumulés.
Pour voir la liste des snapshots :
zfs list -t snapshot
Souvenez-vous qu'il s'agît de sauvegardes incrémentielles !!!
Chaque snapshot depuis l'initial est nécessaire pour revenir à un état antérieur à celui actuel !
Il faudra donc écrire un script de refusion des snapshots incrémentiels afin d'obtenir un état initial (refusionné) plus récent et minimiser ainsi le nombre de snapshots incrémentiels !
ChatGPT ne devrait pas avoir trop de mal à vous écrire cela, maintenant que vous avez un bon script d'exemple ;-). (qu'il a été, du reste, absolument incapable d'écrire lui-même, ne connaissant pas l'option --raw).
Dernier point : la haute-disponibilité.
Vos datasets sont montés sur SERVEUR-B.. ce qui signifie que vous n'êtes plus très loin de la haute-disponibilité... la seule chose qui vous manque : une VIP !!
Utilisez ucarp ou vrrp (si vous y arrivez avec vrrp, écrivez-moi la solution par pitié, je ne l'ai jamais trouvée sur illumos... sur solaris oui en revanche), ou encore un banal script de ping comme celui que j'ai montré dans l'article consacré à ce sujet.
Mise en place d'une Haute Disponibilité (VIP) de base
↑ Haut de page