Richard Dern

Sauvegarder et restaurer son serveur

Maintenant que nous disposons d’un serveur pratiquement complet pour avoir son propre cloud, que nous avons renforcé sa sécurité et que nous avons vu comment opérer un monitoring basique, nous allons voir comment sauvegarder et restaurer son serveur.

Nous allons faire appel au fameux rsync, pour ne pas encombrer notre serveur de solutions aussi lourdes à installer qu’à configurer, d’autant que je vous propose un script qui va simplifier tout ça, et même plus encore…

Vous pouvez placer ce script n’importe où : sur le serveur qui héberge vos services, sur votre propre machine, sur un serveur séparé, un NAS qui supporte rsync et l’authentification par clé, etc. Il suffit de configurer un peu le script pour que la sauvegarde se fasse presque toute seule. Elle est pas belle la vie Libre ?

Choisir son dispositif de stockage

Communément, on sauvegarde sur une partition séparée, de préférence sur un disque séparé, peut être dans une machine séparée, ultimement dans un local séparé. Il n’y a véritablement que deux prérogatives à l’utilisation du script que je vous propose : la machine doit pouvoir utiliser rsync, et l’authentification par clé. Toute distribution GNU/Linux en est capable, vous ne devriez donc pas avoir trop de mal à disposer d’une plateforme adéquate.

Évidemment, une autre prérogative s’impose d’elle-même : l’espace disque disponible.

À titre d’exemple, sachez que j’utilise ce script pour sauvegarder deux serveurs et les documents d’une dizaine de personnes. Le premier, celui qui héberge ingnu.fr, héberge aussi tout un tas d’autres sites, un serveur mail d’une dizaine d’utilisateurs, les bases de données, etc. Au total, un snapshot des deux machines occupe à l’heure actuelle 7.6Go.

Mon script vous permet de disposer de deux types de sauvegardes : un instantané (appelé snapshot) et un instantané de chaque heure de chaque jour. Le premier instantané est en réalité vieux au maximum d’une minute, tandis que les autres instantanés vous permettent de remonter dans le temps par tranche d’une heure, sur un nombre de jours que vous pouvez déterminer.

Au final, sur cinq jours (qui est le réglage par défaut), l’espace disque total occupé par les sauvegardes n’est que doublé : il est actuellement de 13Go. Seuls les fichiers modifiés occupent la différence entre l’espace total et l’espace initial.

Vous n’avez donc pas besoin d’un disque de plusieurs téraoctets, à moins que vous ne vouliez sauvegarder aussi des fichiers comme des vidéos ou des distributions GNU/Linux en quantité industrielle…

Créer la clé d’authentification

La communication entre le serveur et la machine qui exécute la sauvegarde (qui peut aussi être la même machine donc) se fait via rsync, donc ssh. La connexion est par conséquent chiffrée, et les transferts sont intelligents : la bande passante est minimisée.

Pour pouvoir tout sauvegarder d’un coup, on va donc sauvegarder en tant que root. Si vous sauvegardez depuis une machine distante, créez une clé avec la commande suivante :

ssh-keygen -t rsa -f ~/.ssh/id_rsa_backup

Que vous enverrez au serveur via la commande :

ssh-copy-id -i ~/.ssh/id_rsa_backup.pub root@exemple.fr

Vous devrez saisir le mot de passe root de la machine distante. Testez enfin l’accès :

ssh root@exemple.fr

Vous devriez être connecté sans avoir eu besoin de saisir votre mot de passe.

Créer le script de sauvegarde

Créez un répertoire dédié au script et à sa configuration :

mkdir -p /scripts/backup
cd /scripts/backup
mkdir conf.d include.d exclude.d

Pour respecter le Standard de Hiérarchie des Systèmes de Fichiers, nous devrions placer le script dans /usr/bin, et la configuration dans /etc/backup. Rien ne vous empêche de procéder de la sorte, à condition de modifier les chemins d’accès dans le script que nous allons voir tout de suite :

nano backup
#!/bin/bash

###############################################################################
# Variables                                                                   #
###############################################################################

my_pid=$$
my_dir=$(dirname $0)
pid_file="/var/run/backup.pid"

conf_dir="$my_dir/conf.d"
include_dir="$my_dir/include.d"
exclude_dir="$my_dir/exclude.d"

log_dir="/var/log/backup"
log_file="$log_dir/backup"

rsync_args="--archive --recursive --delete --delete-excluded"

backup_root="/mnt/backup"

rsync=$(which rsync)

current_date=$(date "+%Y-%m-%d")
current_minute=$(date +%M)
current_hour=$(date +%H)
yesterday_date=$(date --date "yesterday" "+%Y-%m-%d")

max_age=5
oldest_backup=$(date --date "$max_age days ago" "+%Y-%m-%d/%H")
oldest_log=$(date --date "$max_age days ago" "+%Y-%m-%d")

###############################################################################
# Fonctions                                                                   #
###############################################################################

function check_pid() {
    if [ -f "$pid_file" ]
    then
        old_pid=$(cat "$pid_file")
        old_pid_exists=$(ps -p $old_pid | grep -v TTY | awk -F " " '{print $1}')

        if [ "$old_pid_exists" != "" ]
        then
            exit
        fi
    fi
}

function log() {
    if [ "$2" == "true" -o "$2" == "TRUE" -o "$2" == "yes" -o "$2" == "YES" -o "$2" == "1" ]
    then
        log_date=$(date "+%d/%m/%Y %H:%M:%S")

        echo "[$log_date] $1" >> "$log_file"
    else
        echo "$1" >> "$log_file"
    fi
}

function check_dir() {
    if [ ! -d "$1" ]
    then
        mkdir -p "$1"
    fi
}

###############################################################################
# Lancement de la procédure                                                   #
###############################################################################

##### Vérification des journaux ###############################################

check_dir "$log_dir"

if [ ! -f "$log_file" ]
then
    touch "$log_file"
fi

##### Déplacement des anciens journaux ########################################

if [ "$current_hour" == "00" -a "$current_minute" == "00" ]
then
    mv "$log_file" "$log_file-$yesterday_date"
fi

##### Démarrage de la procédure ###############################################

check_pid

echo $my_pid > "$pid_file"

start_date=$(date "+%d/%m/%Y %H:%M:%S")

log " "
log "###############################################################################"
log "# Début de la procédure : $start_date                                 #"
log "###############################################################################"
log " "

list=$(ls "$conf_dir")

for host in $list; do
    conf_file="$conf_dir/$host"

    source "$conf_file"

    host_rsync_args="$rsync_args"
    host_snapshot="$backup_root/$host/snapshot"

    if [ "$ENABLED" != "yes" -a "$ENABLED" != "YES" -a "$ENABLED" != "true" -a "$ENABLED" != "TRUE" -a "$ENABLED" != "1" ]
    then
        log "**** L'hôte $host est désactivé ****"
        log " "
        continue
    fi

    log "**** Traitement de l'hôte $host ****"
    log " "

    source="$SOURCE"
    target="$host_snapshot"

    check_dir "$target"

    log "Source : $SOURCE" true
    log "Destination : $target" true

    if [ -f "$include_dir/$host" ]
    then
        log "Un fichier d'inclusion existe : $include_dir/$host" true
        host_rsync_args="$host_rsync_args --files-from=$include_dir/$host"
    fi

    if [ -f "$exclude_dir/$host" ]
    then
        log "Un fichier d'exclusion existe : $exclude_dir/$host" true
        host_rsync_args="$host_rsync_args --exclude-from=$exclude_dir/$host"
    fi

    log "Commande : $rsync $host_rsync_args $source $target" true
    log "Démarrage de la création ou mise à jour de l'instantané..." true

    $rsync $host_rsync_args $source $target

    log "Création ou mise à jour de l'instantané terminée" true

    ##### Sauvegarde périodique ###############################################

    if [ "$current_minute" == "00" ]
    then
        log " "
        log "Sauvegarde périodique" true

        source="$host_snapshot/"
        target="$backup_root/$host/$current_date/$current_hour/"

        check_dir "$target"

        log "Source : $source" true
        log "Destination : $target" true
        log "Commande : $rsync $host_rsync_args --links-dest=$source $SOURCE $target" true
        log "Démarrage de la sauvegarde périodique..." true

        $rsync $host_rsync_args --link-dest="$source" "$SOURCE" "$target"

        log "Sauvegarde périodique terminée" true
    fi

    ##### Suppression des anciennes sauvegardes ###############################

    log " "
    log "Recherche d'une ancienne sauvegarde ($backup_root/$host/$oldest_backup)..." true

    if [ -d "$backup_root/$host/$oldest_backup" ]
    then
        log "Une ancienne sauvegarde existe : $backup_root/$host/$oldest_backup" true
        log "Suppression de la sauvegarde la plus ancienne..." true

        rm -rf "$backup_root/$host/$oldest_backup"

        log "Suppression terminée" true
    else
        log "Il n'existe pas d'ancienne sauvegarde" true
    fi

    log " "
done

end_date=$(date "+%d/%m/%Y %H:%M:%S")

log "###############################################################################"
log "# Fin de la procédure : $end_date                                   #"
log "###############################################################################"
log " "

rm -f "$pid_file"

Important : Veuillez noter que ce script me convient bien : rien ne vous empêche de faire des ajustements pour qu’il vous corresponde à VOUS !

Notice : Modifiez impérativement la variable backup_root !

Quelques explications. Si vous voulez respecter les FHS, modifiez les variables conf_dir, include_dir et exclude_dir. Vous pouvez également modifier la variable max_age, dont la valeur (un entier) représente le nombre de jours à conserver.

Ensuite, quelques fonctions classiques : recherche d’un pid existant (pour ne pas lancer deux sauvegardes simultanées), journalisation, et recherche et création de répertoire.

La procédure commence alors par la vérification de l’existence des journaux et leur archivage. Puis, le script récupère la liste des fichiers contenus dans conf.d, lit chacun d’entre eux pour obtenir deux directives de configuration ($ENABLED true/false et $SOURCE représentant la source à sauvegarder), recherche s’il existe un fichier d’inclusion (il vaut mieux sinon vous sauvegardez toute la partition, se trouve dans le répertoire include.d) et un fichier d’exclusion (facultatif, se trouve dans exclude.d).

Chaque minute (via une tâche cron), le script se lance, et créé ou met à jour l’instantané (le contenu du répertoire $backup_root/$host/snapshot). Chaque heure, ce snapshot est créé dans un répertoire distinct, en faisant appel aux hard links : le fichier n’est pas copié, mais un hard link est créé. Autrement dit, le fichier est accessible depuis deux adresses différentes. Même si la source (dans le snapshot) est supprimée, le fichier existe toujours grâce au hard link. Dans notre cas, cela nous permet d’économiser de l’espace disque.

La conséquence de cette façon de procéder est de disposer à tout instant d’une image complète du système de fichiers sauvegardé. Et le but, c’est de le restaurer en une ligne de commande (une simple copie suffira).

Enfin, on supprime les instantanés les plus anciens.

Attribuez le droit d’exécution au script :

chmod +x backup

Configuration

Vous disposez donc d’un répertoire conf.d, include.d et exclude.d. Le premier contiendra un fichier par machine à sauvegarder. Vous pouvez nommer ce fichier comme bon vous semble. Une seule règle à respecter : ce nom devra être le même que celui du fichier d’inclusions et du fichier d’exclusions.

Créons la configuration de notre cloud :

nano conf.d/my_cloud
ENABLED=true
SOURCE=/

Si le serveur est une machine différente, on utilisera la notation suivante :

SOURCE="root@exemple.fr:/"

Avec cette configuration, on va sauvegarder l’ensemble de la machine concernée (ou plus exactement, l’intégralité de la partition /). Ce n’est probablement pas ce que vous voulez, alors nous devons créer un fichier d’inclusions :

nano include.d/my_cloud

À peu de chose près, vous devriez mettre ceci :

/etc/amavis
/etc/apache2
/etc/bind
/etc/clamav
/etc/dovecot
/etc/php5
/etc/postfix
/etc/prosody
/etc/rc.local
/etc/spamassassin
/etc/ssl
/opt
/scripts
/var/vmail
/var/www
/var/lib/mysql

Avec ce contenu, vous devriez sauvegarder l’intégralité du serveur que nous avons configuré jusqu’à maintenant.

Pour éviter de sauvegarder des choses inutiles, créons un fichier d’exclusions :

nano exclude.d/my_cloud
log

Lancement

On édite la crontab de l’utilisateur qui procède à la sauvegarde :

crontab -e
* * * * * /scripts/backup/backup

Et on vérifie le contenu du journal :

tail -f /var/log/backup/backup

Restauration

Pour restaurer votre serveur, il suffit de copier vers lui tout ou partie de la dernière sauvegarde valide, via scp, tout simplement. Inutile de tergiverser : grâce à ce système de sauvegarde, il suffit d’une simple copie sécurisée pour restaurer le dernier état valide du serveur; contrairement à d’autres solutions de sauvegarde, pas besoin d’installer un agent sur la machine distante, une simple connexion ssh suffit !

Conclusion

Nous voici arrivés à la fin de la série d’articles sur la création de son cloud personnel. Plus qu’un article à publier d’ici quelques minutes, alors à tout de suite !

Échanger autour de ce texte

Si vous souhaitez réagir publiquement, un fil dédié vous attend.

Ouvrir le fil de discussion

Taxonomies

Tags