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, souventlocalhost
$profilePort
contiendra le nom de votre serveur de base de donnée, souvent3306
$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 .
Sur la page suivante, vous pourrez récupérer le token API que vous devez renseignez pour $SLACK_CLI_TOKEN
.
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
etSLACK_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 profil0
et s’arrête quandPROFILE_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