MySQL- und Website-Backup periodisch erstellen

Vor einigen Tagen konnte ich einen Punkt von meiner Aufgabenliste streichen, welchen ich schon längere Zeit offen hatte und eigentlich schon lange einmal hätte umsetzen wollen und sollen: das Einrichten eines Backups für meine Webseite.

Bisher machte ich nur sporadisch entweder manuelle Backups der Daten der Webverzeichnisse und der dazugehörigen MySQL-Datenbanken oder ich habe bei meinem VPS-Provider ein Backup für die ganze Maschine durchgeführt – beides nicht gerade sehr sexy, das muss besser und eleganter gelöst werden, dachte ich mir.

Als erstes schrieb ich ein Shell-Skript, welches mir alle Datenbanken (ausser den System-Datenbanken) in Archiv-Dateien exportiert (alle Pfade und Benutzerangaben wurden durch Beispieldaten ersetzt):

#!/bin/bash

BACKUP_DATE="$(date +%Y-%m-%d)"
DB_BACKUP_ROOT="/zielpfad/zum/backup/mysql"
DB_BACKUP="$DB_BACKUP_ROOT$BACKUP_DATE"
DB_USER="Datenbank-Benutzer"
DB_PASSWD="Datenbank-Kennwort"
HN=`hostname | awk -F. '{print $1}'`
# Create the backup directory
mkdir -p $DB_BACKUP

# Remove backups older than 30 days
find $DB_BACKUP_ROOT -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;

# Backup each database on the system using username and password
for db in $(mysql --user=$DB_USER --password=$DB_PASSWD -e 'show databases' -s --skip-column-names|grep -vi information_schema);
do
  BACKUP_FILE_PATH="$DB_BACKUP/mysqldump-$HN-$db-$BACKUP_DATE.gz"
  mysqldump --user=$DB_USER --password=$DB_PASSWD --opt $db | gzip > $BACKUP_FILE_PATH;
  # Set desired permissions
  chmod -R 600 $BACKUP_FILE_PATH
done

Das obenstehende Skript macht folgendes:

  • Zuerst setze ich einige Variablen, wie das Zielverzeichnis für die Backup-Dateien. Ich lese das aktuelle Datum aus und speichere es in einer Variable und benutze dies, um für die täglichen Backups jeweils ein neues Zielverzeichnis innerhalb des festgelegten „DB_BACKUP_ROOT“ zu erstellen.
  • Danach setze ich die MySQL-Zugangsdaten und ermittle den Host-Namen des Rechners, um diesen später auch zum Dateipfad hinzuzufügen.
  • Mittels des „find“-Befehls ermittle ich alle Backups, welche älter als 30 Tage sind und lösche diese.
  • Danach iteriere ich in einer Schleife über alle relevanten Datenbanken und speichere diese mittels „mysqldump“ an den gewünschten Zielort, dabei pipe ich das Resultat von „mysqldump“ zuvor noch an „gzip“ und sorge damit, dass ich eine Archivdatei erhalte.
  • Zum Schluss ändere ich mittels „chmod -R 600 …“ noch die Dateirechte der zuvor erstellten Backup-Datei, so dass nur noch der Besitzer der Datei diese lesen und ändern kann.

Nun habe ich die Datenbank gesichert. Mittels eines weiteren Shells-Skripts sichere ich nun noch die Dateien der einzelnen Webseiten:

#!/bin/bash

WWW_ROOT="/var/wwwdir/websites/"
WWW_BACKUP_ROOT="/zielpfad/zum/backup/www"
WWW_BACKUP="$WWW_BACKUP_ROOT`date +%Y-%m-%d`"
HN=`hostname | awk -F. '{print $1}'`
# Create the backup directory
mkdir -p $WWW_BACKUP

# Remove backups older than 30 days
find $WWW_BACKUP_ROOT -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;

# get arguments
while [ "$1" != "" ]; do
  case $1 in
    -s | --site )     shift
                      site=$1
                      ;;
    * )               echo "Unknown argument $1."
                      exit 1
  esac
  shift
done

# Backup website
if [ "$site" != "" ]; then
  BACKUP_PATH="$WWW_BACKUP/www-$HN-$site-$(date +%Y-%m-%d).tar.gz"
  WWW_PATH="$WWW_ROOT$site"
  echo "Backing up site $WWW_PATH to $BACKUP_PATH" 
  cd $WWW_ROOT
  tar -czf $BACKUP_PATH $site
  chmod -R 600 $BACKUP_PATH
else
  echo "You need to specifiy a site to backup with the -s argument."
  exit 1
fi

Dieses Skript macht folgendes:

  • Als erstes setze ich wieder einige Variablen. Das Quellverzeichnis der Webseiten-Daten wird angegeben (WWW_ROOT) und das (Wurzel-)Zielverzeichnis für die Backup-Daten wird festgelegt (WWW_BACKUP_ROOT). Ausserdem lege ich den Ordernnamen für die täglichen Backups fest (WWW_BACKUP), dazu verwende ich wie schon beim MySQL-Backup das „date“-Befehl.
  • Mittels mkdir -p $WWW_BACKUP wird der tägliche Backup-Ziel-Ordner erstellt, es sei denn er ist schon vorhanden (dazu ist der „-p“-Parameter da).
  • Erneut suche ich mittels „find“ alle Backup-Dateien, welche älter sind als 30 Tage und lösche diese.
  • Danach kommt eine „while“-Schleife, mit der ich die Argumente, welche dem Skript übergeben wurden auslese, dies ist nötig, da ich das Skript später mittels Cron-Job aufrufen möchte und jeweils die Quell-Webseite mittels eines Parameters angeben möchte. Wenn keine Website als Argument übergeben oder falls ein unbekanntes Argument übergeben wurde, wird das Skript beendet (exit 1).
  • Danach wird die eigentliche Backup-Datei erstellt. Dazu wird der Zielpfad der Archivdatei zusammengesetzt (BACKUP_PATH) und dann mittels dem „tar“-Befehl das Backup erstellt und gepackt.
  • Am Ende passe ich wieder die Dateirechte mittels „chmod“ an, so dass nur der Besitzer die Datei lesen und ändern kann.

Nun haben wir zwei Skripts, welche die für mich relevanten Komponenten der Webseiten sichern können, jedoch müssten diese immernoch manuell ausgeführt werden. Damit automatisch täglich die Backups erstellt werden, richtete ich unter einem Account einige Cronjobs (Befehl: crontab -e) ein, welche die (ausführbaren) Skripte einmal am Tag laufen lassen:

# Daily website backup
15 2 * * * /usr/local/bin/mysql_backup.sh >/dev/null 2>&1
15 2 * * * /usr/local/bin/www_backup.sh -s website_dir_1 >/dev/null 2>&1
15 2 * * * /usr/local/bin/www_backup.sh -s website_dir_2 >/dev/null 2>&1
15 2 * * * /usr/local/bin/www_backup.sh -s website_dir_3 >/dev/null 2>&1
45 4 * * * /usr/local/bin/copy_backup_to_different_server.sh >/dev/null 2>&1

Die Backups werden mit dieser Konfiguration also immer um 2:15 Uhr durchgeführt. „/dev/null 2>&1“ bewirkt, dass die Ausgaben unterdrückt werden, sie landen im Nirvana.

In der letzten Zeile der obenstehenden Crontab seht ihr, dass noch ein weiteres Skript „copy_backup_to_different_server.sh“ zu einem späteren Zeitpunkt aufgerufen wird.
Da die Daten bisher nur lokal auf dem Server gesichert werden, nützen sie mir nichts, falls es zu einem generellen Datenverlust auf dem Server käme. Zur Sicherheit kopiere ich also die Backup-Archive noch an einen weiteren Ort, auf einen anderen Server.

Das  Skript ist wie folgt aufgebaut:

#!/bin/bash

rsync -rlthvz --delete --owner=1111 --group=333 -e '/usr/bin/ssh -i /home/userXy/.ssh/id_rsa' /zielpfad/zum/backup/ user@remote.server.com:/volumeXy/home/userMn/MyFiles/Backup/websites
  • Das Skript kopiert via RSYNC alle neuen Dateien von „/zielpfad/zum/backup“ auf den Server „remote.server.com“ in den Pfad „/volumeXy/home/userMn/MyFiles/Backup/websites“. Die Verbindung wird im Beispiel mit dem Benutzer „user“ durchgeführt.
  • Die Verbindung geschieht durch den SSH-Aufruf innerhalb des RSYNC-Befehls über eine verschlüsselte SSH-Verbindung.
  • „–delete“ bewirkt, dass im Ziel die Dateien gelöscht werden, welche nicht mehr in der Quelle vorhanden sind.
  • Mittels „–owner=…“ und „–group=…“ setze ich den Besitzer und die Gruppe der neuen Dateien und Ordner auf dem Zielsystem, da diese nicht mit denen auf dem Quellsystem identisch sind.

Damit RSYNC via SSH auch ohne Kennwort-Eingabe funktioniert, muss man natürlich zuerst ein Schlüssel-Paar (öffentlich/privat) erzeugen (sofern man noch keines hat) und den öffentlichen Schlüssel auf dem Zielsystem in den „Authorized Keys“ eintragen. Hier findet ihr eine Anleitung dazu: http://www.thegeekstuff.com/2008/11/3-steps-to-perform-ssh-login-without-password-using-ssh-keygen-ssh-copy-id/

Nun ist das Backup also eingerichtet. Es sichert täglich automatisch die Websites mitsamt der Datenbank und kopiert diese Dateien zur Sicherheit auch noch auf einen anderen Server.