Tutoriel : Sauvegardes régulières de machines distantes sur une machine de backup avec ssh, mysqldump et tar
C’est un problème qui revient de manière assez récurrente, mais trouver une méthode complète de A à Z pour effectuer une sauvegarde en pull d’un site web (ou de plusieurs en l’occurrence) se révèle étonnamment peu simple.
On trouve des bout de ci-de là, du rsync, du ssh, du sftp, du tar et du tar over ssh, mais rien qui compile toutes ces infos en une paire de scripts bien nets qui fasse ce qu’on lui demande, à savoir, depuis une machine sur laquelle on veut stocker des backups se connecter à un ou des hôtes distants, sauver une ou des bases de données pour cet hôte, et sauver ensuite un ensemble de dossiers.
Et quand on est un newbie en shell, ou comme dans mon cas qu’on en a plus fait de manière sérieuse depuis 12 ans (et c’est pas comme le vélo, ça s’oublie), on peut passer un sacré moment avant de trouver toutes les infos nécessaires (l’air de rien pour faire ça, j’ai consulté environ 40 sites différents sur lesquels j’en ai retenu a peu près 25 où j’ai trouvé des morceaux de ce qu’il me fallait mettre bout à bout)
J’ai mis ces scripts en place sur une Dedibox avec 1.2To d’espace disque (j’aime l’espace 🙂 ). Ils me sauvegardent tous les jours 13 gigas de données en tout juste une heure.
Le principe
pour différentes raisons de compatibilités (rsync bloqué ici, sftp sans récursion par là), je me suis rabattu sur une méthode qui fonctionnait pour tous les hôtes que je désirais sauver :
- Connexion automatique (sans login) en SSH a l’hôte avec l’utilisateur distant (on doit générer une paire de clés pour chaque hôte)
- mysqldump des bases de données de l’utilisateur gzippé en sortie sur ma machine de backup
- tar (depuis un dossier racine) des dossiers de l’utilisateur, gzippé lui aussi en sortie sur ma machine de backup
- rotation quotidienne, hebdomadaire et mensuelle de mes sauvegardes (via cron)
- mail d’un rapport de sauvegarde/espace libre à chaque rotation
dans ce tutoriel , j’utilise :
- backupserver : la machine qui va effectuer les sauvegarde
- distant_hostname : une machine distante à sauvegarder
- distant_user : le nom d’utilisateur sur la machine distante à sauvegarder
remplacez les pour vos besoins.
Générer les clés SSH pour les hôtes distants
Avant de commencer à sauvegarder proprement dit, il faut pouvoir se connecter aux hôtes distants sans avoir à remplir un mot de passe, si on veux que la sauvegarde se fasse de façon automatique.
On va donc, pour chaque hôte distant, générer une paire de clés privée/publique, et l’installer sur notre machine de backup pour créer pour chaque hôte une connexion automatique.
Pour commencer, depuis notre machine de backup on va se se connecter sur la machine qui effectuera les sauvegardes, avec l’utilisateur qui nous intéresse :
backupserver:~/$ ssh distant_user@distant_hostname.com
entrer le mot de passe de l’utilisateur :
distant_user@distant_hostname.com's password:
on est connecté :
Welcome to distant_user@distant_hostname.com Last login: Thu Mar 6 00:07:28 2014 from ... [distant_hostname]$
générer les clés (en changeant le nom par défaut « id_rsa » en « distant_hostname_id_rsa ») :
[distant_hostname]$ ssh-keygen -t rsa Generating public/private rsa key pair. Enter file in which to save the key (/home/distant_user/.ssh/id_rsa): /home/distant_user/.ssh/distant_hostname_id_rsa Created directory '/home/distant_user/.ssh'. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/distant_user/.ssh/distant_hostname_id_rsa. Your public key has been saved in /home/distant_user/.ssh/distant_hostname_id_rsa.pub. The key fingerprint is: 8d:49:6a:01:3b:70:e8:ea:66:4b:20:44:0d:bd:28:cf distant_user@distant_hostname The key's randomart image is: +--[ RSA 2048]----+ | o=.o | |. .= o | | o. + . . | |o... . + + | |++ o S . | |+ E . | |.. | |.+ | |o.. | +-----------------+ [distant_hostname]$
copier la clé publique dans le fichier de clés autorisées :
[distant_hostname]$ cd .ssh [distant_hostname]$ cat distant_hostname_id_rsa.pub >> authorized_keys
Changer les droits du fichier authorized_keys pour que seul le propriétaire puisse le lire :
[distant_hostname]$ chmod 0600 authorized_keys
effacer le fichier de clé publique, on n’en a plus besoin
[distant_hostname]$ rm distant_hostname_id_rsa.pub [distant_hostname]$ ls -al total 12 drwx------ 2 reitzel pg70174 61 Mar 11 06:46 . drwx--x--- 13 reitzel adm 4096 Mar 11 06:42 .. -rw------- 1 reitzel pg70174 396 Mar 11 06:44 authorized_keys <;- clés publiques autorisées a la connexion -rw------- 1 reitzel pg70174 1675 Mar 11 06:43 distant_hostname_id_rsa <- clés privée pour la connexion en cours
terminer la connexion et revenir sur la machine de sauvegarde
[distant_hostname]$ exit logout Connection to distant_host closed. backupserver:~/$
copier la clé privée depuis la machine a sauvegarder :
backupserver:~/$ cd .ssh (on suppose que le dossier.ssh existe, sinon, le créer...) backupserver:~/.ssh $ scp distant_user@distant_host:.ssh/distant_host_id_rsa . distant_user@distant_host's password: distant_hostname_id_rsa 100% 1675 1.6KB/s 00:00 backupserver:~/.ssh $
Éditer le fichier de configuration ssh pour y ajouter la connexion juste créée (on utilise un fichier de configuration pour permettre à notre machine de sauvegarde de se connecter a plusieurs hôtes :
(j'édite avec nano, remplacer par votre éditeur favori)
backupserver:~/.ssh $ nano ~/.ssh/config
Dans le fichier de config on ajoute :
Host mydistanthost Hostname distant_hostname IdentityFile ~/.ssh/distant_hostname_id_rsa Port 22 <- ou tout autre port défini pour la connexion à cet hôte User distant_user
on teste la connexion :
backupserver:~/.ssh$ ssh mydistanthost <-le Host défini dans le fichier de config _ _ Welcome to distant_user@distant_hostname Last login: Tue Mar 11 06:40:26 2014 from .... [distant_hostname]$
On est connecté, sans mot de passe, on est content, on revient sur notre machine de sauvegarde.
[distant_hostname]$ exit logout Connection to distant_hostname closed.
Il ne reste qu'à répéter le processus pour tous les hôtes distants auxquels on souhaite se connecter..
Créer le script de sauvegarde pour la machine distante
Pour ma part, mais c'est une question de préférence, je stocke mes sauvegardes dans
/opt/backups/nom_du_site_distant
mes scripts de sauvegarde dans :
~/backups_scripts/
pour chaque site a sauvegarder je fais :
Une sauvegarde quotidienne (je conserve les deux dernières)
Une sauvegarde hebdomadaire ( je conserve également les deux dernières)
Une sauvegarde mensuelle (je ne conserve que la dernières
Ça me donne une arborescence de ce type :
opt/backups/ distant_hostname/ daily/ AAAA-MM-JJ/ dbdumps/ sites/ AAAA-MM-JJ/ dbdumps/ sites/ weekly/ AAAA-MM-JJ/ dbdumps/ sites/ AAAA-MM-JJ/ dbdumps/ sites/ monthly/ AAAA-MM-JJ/ dbdumps/ sites/ distant_hostname2/ etc...
Pour chacun des hôtes distants on va créer un script contenant les infos de connexion, et les listes des bases et des dossiers à sauvegarder.
(on pourrait faire un seul script lisant un fichier de configuration pour chaque hôte, mais on peut selon les hôtes être amené à faire des modification à la marge, avoir deux utilisateurs de bases de données différents ou deux répertoires racines différents, ce genre de choses, on se laisse donc la possibilité de personnaliser chaque script par hôte)
On crée notre répertoire où l'on va stocker nos scripts de backups :
backupserver:~/ $ cd ~/ backupserver:~/ $ mkdir backups_scripts
on crée et on édite notre script de base
backupserver:~/ $ cd backups_scripts backupserver:~/backups_scripts/ $ > distant_hostname.sh backupserver:~/backups_scripts/ $ nano distant_hostname.sh
et dedans on met ça :
#!/bin/bash ########### CONFIGURATION DU SITE A SAUVEGARDER ############################### # Répertoire racine distant à partir duquel on fera un tar de chacun des dossiers listés dans directories_to_save REMOTE_ROOT=/home/distant_user #quoi sauver : # nom des bases de données et nom des dossiers (pas de '/' dans les nom de dossier) bases_to_dump=(base_de_donnnées_1 base_de_donnees_2 base_de_donnees3 etc...); directories_to_save=(repertoire_1 repertoire_2 repertoire_3 etc.. ) ; #Nom de l'hote dans notre ~/.ssh/config remote_host=distant_hostname # Informations mysql pour l'hote distant mysql_host=mysql_hostname (ou localhost) mysql_user=mysql_username mysql_pass=Mysql_password # Répertoire local pour la sauvegarde MYBACKUPDIR=/opt/backups/distant_hostname ########### FIN DE CONFIGURATION ############################### # Extensions pour les fichiers de sorties dump_extension=.sql.gz data_extension=.tar.gz # date_du jour au format AAAA-MM-JJ DATE=$(date +%F); #une fonction usage au cas où on n'ai pas tapé d'argument sur la ligne de commande usage () { cat <$DMPDIR/${bases_to_dump[$index]}_$DATE$dump_extension; done # parcours du tableau des répertoires à sauver et pour chacune : # connection ssh à l'hôte distant en passant un changement de dossier dans le répertoire racine à sauvegarder, puis # la commande tar zcf (z: gzippé, c: création d'archive, f: fichier de sortie en indiquant la sortie de tar sur stdout : - (le tiret) # à noter : le paramètre "f" doit etre les derniers de paramètres passé à tar, juste avant le '-' qui indique la sortie standard # on dirige la sortie de tar sur un fichier local for index in "${!directories_to_save[@]}"; do echo ${directories_to_save[$index]}_$DATE$data_extension; ssh $remote_host "(cd $REMOTE_ROOT; tar zcf - ${directories_to_save[$index]})" > $DATDIR/${directories_to_save[$index]}_$DATE$data_extension; done # l'argument tapé en ligne de commande n'est pas valide, on sort : else usage exit 2; fi fi
On sort et on teste notre script :
d'abord changer ses droits pour le rendre exécutable :
backupserver:~/backups_scripts/ $ chmod 0700 save_distant_hostname.sh
Lancer le script avec une sauvegarde quotidienne pour tester son exécution correcte :
backupserver:~/backups_scripts/ $ ./distant_hostname.sh daily
Copier et modifier le script pour chacun pour tous les hôtes distants qu'on désire sauver
backupserver:~/backups_scripts/ $ cp -a ./distant_hostname.sh ./distant_hotsname2.sh
backupserver:~/backups_scripts/ $ nano distant_hotsname2.sh
etc...
Rotation et lancement des sauvegardes
Une fois tous nos scripts de sauvegardes créés et testés, on écrit un script qui va se charger de :
- nettoyer les anciennes sauvegardes quotidiennes, hebdomadaires, mesuelles
- lancer le processus de sauvegarde pour tous nos hôtes distants
backupserver:~/backups_scripts/ $ > clean_and_backup.sh backupserver:~/backups_scripts/ $ nano clean_and_backup.sh
et dans ce script on met :
#!/bin/bash BASEDIR=/home/my_username/backups_scripts BACKUPS=/opt/backups host_to_save=(distant_hostname.sh distant_hostname2.sh) ; usage () { cat << EOF Usage : monthly .: réalise une sauvegarde mensuelle weekly ..: réalise une sauvegarde hebdomadaire daily ...: réalise une sauvegarde quotidienne EOF } if [ $# -lt 1 ]; then # pas d'argument on sort en erreur usage exit 2 else if [ $1 = "daily" ] || [ $1 = "weekly" ]; then # on boucle sur les 6 derniers jours pour trouver tous les répertoires qui ont le format AAAA-MM-JJ et on les efface avec leur contenu # (date -d "$i days ago" affiche n jours en arrière ici entre 2 et 6 jours) for ((i=2; i<7; i++)); do # on assigne la date a rechercher FINDDATE=$(date -d "$i days ago" +%F); # on liste les dossiers dans /opt/backups (ou là ou sont rangé les backups) for repertoire in $(ls $BACKUPS); do #on cherche dans $repertoire courant/daily (ou l'argument passé (weekly, monthly) les dossiers (type -d) contenant la date a trouver dans leur nom (-name "*$FINDDATE*") et on les efface avec leur contenu (-exec rm -r) find $BACKUPS/$repertoire/$1 -type d -name "*$FINDDATE*" -exec rm -r {} \; done; done; for index in "${!host_to_save[@]}"; do # pour chaque script listé dans le tableau host_to_save on exécute le script correspondant en lui passant l'argument. $BASEDIR/${host_to_save[$index]} $1 done; elif [ $1 = "weekly" ] ; then # on boucle sur les 3 derniers jours pour trouver tous les répertoires qui on le format AAAA-MM-JJ et on les efface avec leur contenu # (date -d "$i weeks ago" affiche n semaines en arrière ici entre 2 et 3 semaines) for ((i=2; i<4; i++)); do FINDDATE=$(date -d "$i weeks ago" +%F); for repertoire in $(ls $BACKUPS); do find $BACKUPS/$repertoire/$1 -type d -name "*$FINDDATE*" -exec rm -r {} \; done; done; for index in "${!host_to_save[@]}"; do $BASEDIR/${host_to_save[$index]} $1 done; elif [ $1 = "monthly" ] ; then # on cherche tous les répertoires qui on le format AAAA-MM-JJ et on les efface avec leur contenu (dans monthly on n'en garde qu'un pas de boucle nécessaire) # (date -d "1 month ago" affiche 1 mois en arrière ) FINDDATE=$(date -d "1 month ago" +%F); for repertoire in $(ls $BACKUPS); do find $BACKUPS/$repertoire/$1 -type d -name "*$FINDDATE*" -exec rm -r {} \; done; for index in "${!host_to_save[@]}"; do $BASEDIR/${host_to_save[$index]} $1 done; else # erreur dans la syntaxe de l'argument on affiche usa on sort en erreur usage exit 2; fi; fi;
on teste notre script au moins une fois 🙂
backupserver:~/backups_scripts/ $ chmod 0700 clean_and_backup.sh backupserver:~/backups_scripts/ $ chmod ./clean_and_backup.sh
Il ne nous reste qu'à lancer ce script à intervalles réguliers en créant une tâche planifiée :
backupserver:~/backups_scripts/ $ crontab -e
dans la crontab on ajoute :
MAILTO="myemail@example.com" # sauvegarde de serveurs distant # tous les jours à deux heures du matin : 0 2 * * * /home/my_user_name/backups_scripts/clean_and_backup.sh daily # le premier jour de la semaine (en europe le lundi, mais ça dépend de la locale) à 3 heures du matin 0 3 * * 1 /home/my_user_name/backups_scripts/clean_and_backup.sh weekly # à une heure du matin le 1er de chaque mois.. 0 1 1 * * /home/my_user_name/backups_scripts/clean_and_backup.sh monthly
On sauve, on vérifie que l'entrée est bien dan la crontab :
backupserver:~/backups_scripts/ $ crontab -l
Pour terminer on peut faire un dernier script qui nous informe de l'espace libre sur la machine de backup, juste au cas ou on approcherait saturation :
#!/bin/bash DATE=$(date +%F) df -h | mail -s "[backupservername] $DATE : Espace Libre sur backupservername" myemail@example.com
qu'on peut placer en crontab tous les jours après la sauvegarde (ou bien on peu placer ces commandes en fin du script "clean_and_backup.sh"
Il ne reste qu'à laisser tourner et a attendre que les sauvegardes se fassent, en vérifiant que tout se passe bien.
Pour info pour un hôte distant, entre la génération de clé, la configuration d'un nouveau script de sauvegarde, le test de sauvegarde, l'ajout de ce dernier dans le script d'appel des sauvegardes, il faut compter une vingtaine de minutes (le plus long étant le test de sauvegarde qui télécharge les données pour de bon).
Voilà, j'espère que ce sera utile à certains d'entre vous qui auraient des sauvegardes en séries à réaliser et qui n'ont pas les moyens, ou l'envie, d'acheter une solution de backup professionnelle (qui font ça très bien, avec une interface graphique et une clé usb à faire le café le matin mais bon ...)