Backuper son Serveur Avec Restic et OpenStack

Tout ce qui est susceptible de mal tourner, tournera mal
Edward A. Murphy Jr

Introduction

Cet article vous conduira pas à pas sur la mise en place d’un système de backup régulier d’un serveur web sous Debian avec Restic et OpenStack. Il se focalisera dans un premier temps sur l’installation des dépendances et l’utilisation basique des différents logiciels dont il est sujet. À la fin, il va mettre à disposition un script qui va vous permettre de backuper votre serveur, toutefois, ce script en question suppose que vous avez suivi le reste de l’article, notamment, avez installé les dépendances requises pour son bon fonctionnement.

Restic

Restic est un gestionnaire de backups en ligne de commande qui propose des fonctionnalités essentielles comme les révisions (dans le jargon, des snapshots), le cryptage, entre autres.

Restic propose a peu près les mêmes fonctionnalités que Borg avec un “plus” determinant pour nos besoins : Le support de volumes de stockage OpenStack.

OpenStack

OpenStack est un service de gestion d’infrastructure cloud utilisé par l’hébergeur OVH. Ce qui nous interresse sur OpenStack est évidemment la possibilité d’avoir des volumes de stockage pour entreposer les backups gérés par Restic.

Requirements

  • ca-certificates
  • curl
  • mysql-client (Si vous avez besoin de backuper une base de donnée MySQL)

Visite des lieux

Installation de Restic

A l’heure où j’écris cet article, la version de Restic sur le dépot stable de debian est une vieille version. Le choix d’installer à partir du dépot testing s’impose donc. On commence donc par mettre en place buster (testing au moment où j’écris cet article) dans sources.list

1nano /etc/apt/sources.list.d/restic.list

On ajoute la ligne suivante :

1deb http://deb.debian.org/debian buster main

Ensuite, pour s’assurer que le dépot par défaut utilisé par Aptitude reste stable, on ajoute un fichier default release avec le contenu suivant :

1nano /etc/apt/apt.conf.d/default-release
1APT::Default-Release "stretch";

On lance ensuite la mise à jour suivi de l’installation de la version de Restic qu’il nous faut :

1apt-get update && apt-get -t testing install restic

Configurer les accès OpenStack

Il faut ensuite mettre en place les variables d’environnement nécessaires à restic qu’on placera dans un fichier texte dans le répertoire .config dans le dossier home de l’utilisateur qui executera les travaux de backup:

Créons le dossier si il n’existe pas encore :

1mkdir ~/.config
1nano ~/.config/backup.txt
1export OS_AUTH_URL=
2export OS_TENANT_ID=
3export OS_TENANT_NAME=
4export OS_USERNAME=
5export OS_PASSWORD=
6export OS_REGION_NAME=

Si vous êtes chez OVH Cloud, ces informations sont récupérables en fichier téléchargeable dans la section Cloud > Serveurs > Votre projet Cloud > OpenStack après avoir créé votre utilisateur OpenStack dans cette section même.

On ajoutera ensuite a ce fichier le mot de passe de cryptage Restic.

1export RESTIC_PASSWORD=

💥 Il ne faut jamais perdre ce mot de passe, autrement, vos backups seront illisibles.

Les commandes Restic disponibles

Avant les commandes suivantes, il faut charger le fichier contenant nos variables d’environnement :

1source ~/.config/backup.txt

Pour toutes les commandes suivantes, à définir par vos soins :

  • $CONTAINER est le nom de votre conteneur OpenStack. Si il n’existe pas encore, il sera créé à la volée.
  • $profileIdent est le nom de votre profil de stockage.

Créer un profile de stockage restic

1restic -r swift:$CONTAINER:/$profileIdent init 

Lancer le backup d’un dossier

$profileDir est le nom du dossier à backuper.

1restic -r swift:$CONTAINER:/$profileIdent backup $profileDir 

À chaque fois que cette commande est executée avec le conteneur et le profil déterminé, cela créera une révision de votre backup.

Autres commandes utiles

Restic dispose d’autres commandes qui permettent d’effectuer des actions sur vos backups, nous avons utilisé précédemment init et backup. Vous trouverez une liste complète en executant restic --help ou en ligne sur la page Manuel du logiciel.

Liste de révisions

Pour lister la liste des révisions disponibles pour votre profil de backup.

1restic -r swift:$CONTAINER:/$profileIdent snapshots 
Check

Permet de vérifier l’intégrité de votre profil de backup.

1restic -r swift:$CONTAINER:/$profileIdent check --with-cache 
Forget

Cette commande d’oublier des révisions de vos backups. Utilisez par exemple $profileKeepLast pour determiner combien de révisions vous voulez conserver. Les plus vieilles révisions seront supprimées par cette commande.

1restic -r swift:$CONTAINER:/$profileIdent forget --keep-last $profileKeepLast --prune
Restore

Pour restaurer un backup, vous aurez besoin de savoir quelle révision restituer parmi les révisions disponibles. Pour voir la liste des snapshot disponibles, voir précédemment. Ensuite, la commande suivante permettra de récupérer dans /destination/folder/ vos fichiers backupés sur la révision donnée $snapshotId.

1restic -r swift:$CONTAINER:/$profileIdent restore $snapshotId --target /destination/folder/

Backuper votre serveur de base de donnée MySQL

Mis à part les données stockés en fichiers, et étant donné qu’aujourd’hui, les codes sources sont versionnés, le plus interressant dans un système de backup est de pouvoir sauvegarder les bases de données. Notre exemple se porte ici sur une base de donnée MySQL. Ceci est applicable sans aucune différence sur les bases de données MariaDB et pour les autres, seule diffère la commande de dump.

Le script suivant va parcourir toutes les bases de données de votre serveur MySQL et créer un repertoire pour chacune d’entre elles, ensuite, il va parcourir chacunes des tables de cette base de donnée pour extraire les fichiers .sql, exports de ces tables et les stocker dans les répertoires représentant les bases de données.

  • $profileHost contiendra le nom de votre serveur de base de donnée, souvent localhost
  • $profilePort contiendra le nom de votre serveur de base de donnée, souvent 3306
  • $profileUser contiendra le nom d’utilisateur ayant accès à la lecture sur toutes les tables de toutes vos bases de données
  • $profilePass contiendra le mot de passe de l’utilisateur en question
  • $TMP_DIR indiquera le chemin du repertoire temporaire où seront stockés les exports
 1for database in $(mysql -h $profileHost -P $profilePort -u $profileUser -p$profilePass -e 'show databases;' | tail -n +2)
 2do
 3    mkdir $TMP_DIR/dump/$database
 4    for table in $(mysql -h $profileHost -P $profilePort -u $profileUser -p$profilePass $database -e 'show tables;' | tail -n +2)
 5    do
 6        mysqldump \
 7            --lock-tables=false \
 8            -h $profileHost \
 9            -P $profilePort \
10            -u $profileUser \
11            -p$profilePass \
12            $database "$table" \
13            > $TMP_DIR/dump/$database/"$table".sql
14    done
15done

Reste plus qu’à envoyer les exports sur votre conteneur avec Restic et nettoyer un peu derrière :

1restic -r swift:$CONTAINER:/$profileIdent backup $TMP_DIR/dump/ 
2rm -R $TMP_DIR/dump

🎁 Notifications Slack

En petit bonus, voici comment envoyer sous forme de notifications Slack les logs de vos serveurs. Pour réaliser ceci, nous allons utiliser l’excellent package disponible sur Github, Slack-Cli.

Commençons par installer une dépendance de Slack-Cli :

1apt-get install jq

Ensuite, Slack-Cli lui même, qu’on installera grâce au script bash téléchargé via curl, rendu executable et déplacé sur /usr/bin.

1curl -O https://raw.githubusercontent.com/rockymadden/slack-cli/master/src/slack
2chmod +x slack
3mv slack /usr/bin/

Vous devriez avoir la commande slack disponible. Nous allons avoir besoin des variables suivantes qu’on ajoutera dans ~/.config/backup.txt :

1export $SLACK_CLI_TOKEN=
2export $SLACK_CHANNEL=

Pour récupérer ces informations, connectez vous sur Slack et rendez vous sur la page Créer un Bot pour créer un nouveau bot Slack .

Créer un bot Slack

Sur la page suivante, vous pourrez récupérer le token API que vous devez renseignez pour $SLACK_CLI_TOKEN.

Récupérer le token du bot Slack

Ensuite, il suffit d’inviter votre bot dans la channel Slack où vous voulez retrouvez vos notifications. Le nom de cette channel sera après renseignée dans $SLACK_CHANNEL (Si votre channel s’appelle notifs, vous devez renseignez #notifs comme valeur).

C’est tout, vous devriez pouvoir envoyer le logs de vos backups une fois terminé en fichiers joints directement dans une channel Slack définie par vos soins :

1publicLog="$TMP_DIR/backup_${HOSTNAME}_`date "+%Y"``date "+%m"``date "+%d"`.log"
2touch $publicLog
3restic -r swift:$CONTAINER:/$profileIdent backup $profileDir 2>&1 >> $publicLog
4slack file upload --channels $SLACK_CHANNEL --file $publicLog --title "Backup logs from $HOSTNAME on `date "+%Y/%m/%d"`" 
5rm $publicLog

Scripter le tout et mettre en place la tâche plannifiée

Le fichier de configuration

L’objectif du programme fourni ci-après est de lancer les backups en fournissant les variables d’environnements nécessaires à son bon fonctionnement dans un fichier, dans l’ordre :

  • Les accès OpenStack
  • Le mot de passe Restic à ne pas oublier : RESTIC_PASSWORD
  • Les accès pour les notifications Slack (optionnels) SLACK_CLI_TOKEN et SLACK_CHANNEL
  • Le nom du container de stockage : CONTAINER
  • Une variable qui va mettre en pause le script de backup entre ses travaux SLEEP, reçoit un chiffre supposé en secondes. (ceci est nécessaire du fait que OpenStack a une certaine latence avant que les fichiers soient disponibles sur les conteneurs. Si les commandes Restic s’enchainent trop vite, cela provoque des erreurs intempestives)
  • La liste des dossiers et base de données à backuper (avec les profils)

Revenons sur notre fichier contenant des variables et complétons la :

1nano ~/.config/backup.txt
 1export OS_AUTH_URL=
 2export OS_TENANT_ID=
 3export OS_TENANT_NAME=
 4export OS_USERNAME=
 5export OS_PASSWORD=
 6export OS_REGION_NAME=
 7export RESTIC_PASSWORD=
 8
 9#Slack
10export SLACK_CLI_TOKEN=
11export SLACK_CHANNEL=
12
13# Storage container
14CONTAINER=
15SLEEP=10
16
17# Backup profiles
18PROFILE_IDENT_0=
19PROFILE_METHOD_0=db
20PROFILE_HOST_0=
21PROFILE_PORT_0=
22PROFILE_USERNAME_0=
23PROFILE_PASSWORD_0=
24PROFILE_KEEP_LAST_0=
25
26PROFILE_IDENT_1=
27PROFILE_METHOD_1=
28PROFILE_DIR_1=
29PROFILE_KEEP_LAST_1=

Les profils

Chaque profil de backup :

  • Représente un backup que réalisera le script avec Restic
  • Est composé de variables nécessaires à la réalisation de ce backup
  • Est numéroté de 0 à n. Les backups commencent par le profil 0 et s’arrête quand PROFILE_IDENT_n n’existe pas (Il faut donc que la numérotation suive)

PROFILE_IDENT_n défini l’identifiant du profil est sera utilisé pour $profileIdent sur les commandes Restic. PROFILE_KEEP_LAST_n défini la variable $profileKeepLast utilisé pour la commande forget de Restic.

Un profile type ‘Dossier’

Ce profil indique que c’est un dossier qui est sauvegardé. PROFILE_METHOD_n doit avoir la valeur dir. Il faut ensuite renseigner PROFILE_DIR_n pour définir le dossier à sauvegarder. Si dans votre premier profil, vous voulez sauvegarder /var/www, vous aurez probablement un profil de ce type :

1PROFILE_IDENT_0=var-www
2PROFILE_METHOD_0=dir
3PROFILE_DIR_0=/var/www
4PROFILE_KEEP_LAST_0=5
Un profile type ‘Base de donnée’

Pour les bases de données, la valeur de PROFILE_IDENT_n doit être db. Et il faut évidemment fournir les autres paramètres qui vont permettre d’exporter les tables de vos bases de données.

1PROFILE_IDENT_0=database-localhost
2PROFILE_METHOD_0=db
3PROFILE_HOST_0=localhost
4PROFILE_PORT_0=3306
5PROFILE_USERNAME_0=username
6PROFILE_PASSWORD_0=u53rn4m3
7PROFILE_KEEP_LAST_0=5

Le script complet

Et voici le script complet à installer :

  1#!/bin/bash
  2
  3exportFile=$1
  4if [ ! -f $1 ]; then
  5    echo "$1 is'nt a file."
  6    exit 1
  7fi
  8
  9source $1
 10
 11if [ -z $TMP_DIR ]; then
 12    TMP_DIR=/tmp/backups
 13fi
 14if [ ! -d $TMP_DIR ]; then
 15    mkdir $TMP_DIR
 16fi
 17
 18if [ -z $LOG_FILE ]; then
 19    LOG_FILE=/var/log/backup.log
 20fi
 21if [ ! -f $LOG_FILE ]; then
 22    touch $LOG_FILE
 23fi
 24
 25publicLog="$TMP_DIR/backup_${HOSTNAME}_`date "+%Y"``date "+%m"``date "+%d"`.log"
 26if [ ! -z $SLACK_CLI_TOKEN ]; then
 27    if [ -f $publicLog ]; then
 28        rm $publicLog
 29    fi
 30    touch $publicLog
 31fi
 32
 33export_var () {
 34    varName="${1}_${2}"
 35    echo "${!varName}"
 36}
 37
 38notif_collect () {
 39    if [ ! -z $SLACK_CLI_TOKEN ]; then
 40        echo $1 >> $publicLog
 41    fi
 42}
 43
 44log_file () {
 45    if [ -n "$1" ]; then
 46        echo "`date "+%Y/%m/%d %H:%M:%S"` $1" >> $LOG_FILE
 47        notif_collect "`date "+%Y/%m/%d %H:%M:%S"` $1"
 48    else
 49        while IFS= read -r line
 50        do
 51            echo "`date "+%Y/%m/%d %H:%M:%S"` $line" >> $LOG_FILE
 52            notif_collect "`date "+%Y/%m/%d %H:%M:%S"` $line"
 53        done
 54    fi
 55}
 56
 57slack_chat () {
 58    if [ ! -z $SLACK_CLI_TOKEN ]; then
 59        slack chat send "$1" "$SLACK_CHANNEL" > /dev/null
 60    fi
 61}
 62
 63go_sleep (){
 64    if [ -z $SLEEP ]; then
 65        SLEEP=10
 66    fi
 67    log_file "Sleeping $SLEEP s ..."
 68    sleep $SLEEP
 69}
 70
 71#https://unix.stackexchange.com/questions/27013/displaying-seconds-as-days-hours-mins-seconds
 72function displaytime {
 73  local T=$1
 74  local D=$((T/60/60/24))
 75  local H=$((T/60/60%24))
 76  local M=$((T/60%60))
 77  local S=$((T%60))
 78  (( $D > 0 )) && printf '%d days ' $D
 79  (( $H > 0 )) && printf '%d hours ' $H
 80  (( $M > 0 )) && printf '%d minutes ' $M
 81  (( $D > 0 || $H > 0 || $M > 0 )) && printf 'and '
 82  printf '%d seconds\n' $S
 83}
 84
 85count=0
 86next=1
 87log_file "Backup script executed."
 88slack_chat "Backup started for $HOSTNAME server !"
 89startExec=`date +%s`
 90
 91while [ $next == 1 ]; do
 92
 93    profileIdent=$(export_var "PROFILE_IDENT" $count)
 94    profileMethod=$(export_var "PROFILE_METHOD" $count)
 95
 96    log_file "Backuping profile ${count}."
 97    if [ ! -f "${TMP_DIR}/${CONTAINER}_${profileIdent}" ]; then
 98        log_file "Initialize container $CONTAINER:/$profileIdent."
 99        restic -r swift:$CONTAINER:/$profileIdent \
100            init \
101            --quiet \
102            2>&1 | log_file
103        log_file "Initialize container $CONTAINER:/$profileIdent done."
104        touch "${TMP_DIR}/${CONTAINER}_${profileIdent}"
105        go_sleep
106    else
107        log_file "$CONTAINER:/$profileIdent is already initialized."
108    fi
109
110    if [ "$profileMethod" == "dir" ]; then
111
112        profileDir=$(export_var "PROFILE_DIR" $count)
113
114        log_file "Backuping $profileDir."
115        restic -r swift:$CONTAINER:/$profileIdent \
116            backup $profileDir \
117           --tag auto \
118           --quiet \
119            2>&1 | log_file
120        log_file "Backuping $profileDir done."
121        go_sleep
122    fi
123
124    if [ "$profileMethod" == "db" ]; then
125        profileHost=$(export_var "PROFILE_HOST" $count)
126        profilePort=$(export_var "PROFILE_PORT" $count)
127        profileUser=$(export_var "PROFILE_USERNAME" $count)
128        profilePass=$(export_var "PROFILE_PASSWORD" $count)
129
130        if [ -d "${TMP_DIR}/dump" ]; then
131            rm -R ${TMP_DIR}/dump/*
132        else
133            mkdir $TMP_DIR/dump
134        fi
135
136        log_file "Dump databases on $profileUser@$profileHost:$profilePort."
137        for database in $(mysql -h $profileHost -P $profilePort -u $profileUser -p$profilePass -e 'show databases;' | tail -n +2)
138        do
139            log_file "Dumping database $database in $profileUser@$profileHost:$profilePort."
140            mkdir $TMP_DIR/dump/$database
141            for table in $(mysql -h $profileHost -P $profilePort -u $profileUser -p$profilePass $database -e 'show tables;' | tail -n +2)
142            do
143                #mysqldump: Got error: 1556: "You can't use locks with log tables." when doing LOCK TABLES
144                if [[ ! "$table" =~ "log" ]]; then
145                    mysqldump \
146                    -h $profileHost \
147                    -P $profilePort \
148                    -u $profileUser \
149                    -p$profilePass \
150                    $database "$table" \
151                    > $TMP_DIR/dump/$database/"$table".sql
152                else
153                    mysqldump \
154                    --lock-tables=false \
155                    -h $profileHost \
156                    -P $profilePort \
157                    -u $profileUser \
158                    -p$profilePass \
159                    $database "$table" \
160                    > $TMP_DIR/dump/$database/"$table".sql
161                fi
162                log_file "Dumping database table $table. Size : $(ls -lah $TMP_DIR/dump/$database/$table.sql | awk -F " " {'print $5'})."
163            done
164            log_file "End dumping database."
165        done
166        log_file "End dumping databases."
167
168        log_file "Sending all database to container."
169        restic -r swift:$CONTAINER:/$profileIdent \
170            backup $TMP_DIR/dump/ \
171           --tag auto \
172           --quiet \
173            2>&1 | log_file
174        log_file "End sending all database to container."
175
176        rm -R $TMP_DIR/dump
177        go_sleep
178    fi
179
180    profileKeepLast=$(export_var "PROFILE_KEEP_LAST" $count)
181    log_file "Forgeting backups except $profileKeepLast last."
182    restic -r swift:$CONTAINER:/$profileIdent \
183        forget \
184        --quiet \
185        --tag auto \
186        --keep-last $profileKeepLast \
187        --prune
188    log_file "Forgeting backups except $profileKeepLast last done."
189    go_sleep
190
191    log_file "Check backups."
192    restic -r swift:$CONTAINER:/$profileIdent \
193        check \
194        --quiet \
195        --with-cache \
196        2>&1 | log_file
197    log_file "Check backups done."
198    go_sleep
199
200    log_file "Snapshot list $CONTAINER:/$profileIdent."
201    restic -r swift:$CONTAINER:/$profileIdent \
202       snapshots \
203       2>&1 | log_file
204    go_sleep
205
206    let count=$count+1
207    nextVar="PROFILE_IDENT_${count}"
208
209    if [ ! -n "${!nextVar}" ]; then
210        let next=0
211    fi
212done
213
214endExec=`date +%s`
215runTime=$((endExec-startExec))
216
217log_file "End of Backup script after $(displaytime runTime)."
218
219if [ ! -z $SLACK_CLI_TOKEN ]; then
220    slack file upload \
221        --channels $SLACK_CHANNEL \
222        --file $publicLog \
223        --title "Backup logs from $HOSTNAME on `date "+%Y/%m/%d"`" \
224         > /dev/null
225    rm $publicLog
226fi
227
228slack_chat "End of backup for $HOSTNAME server. Started $(displaytime runTime) ago."

Placez son contenu dans $HOME/bin/backup

1mkdir $HOME/bin/
2nano $HOME/bin/backup

Et ajouter le script parmi vos commandes en ajoutant ceci dans votre fichier $HOME/.bashrc :

1export PATH="$HOME/bin:$PATH"

Vous devriez maintenant pouvoir lancer vos backups avec la commande :

1backup ~/.config/backup.txt

Tâche plannifiée CRON

Pour l’installer en tâche plannifiée (ici tous les jours à 3 heure du matin) :

1crontab -l > tmp_crontab
2echo "0 3 * * * /home/votre_utilisateur/bin/backup /home/votre_utilisateur/.config/backup.txt" >> tmp_crontab
3crontab tmp_crontab
4rm tmp_crontab