Tutoriel : Sauvegardes régulières de machines distantes sur une machine de backup avec ssh, mysqldump et tar

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 ...)