Man hofft Backups nie zu brauchen, aber wenn doch, müssen sie zu 100 % funktionieren. In diesem Beitrag geht es um ein Bash-Skript, das Backups von Remote-Servern verwaltet, differenzielle Sicherungen erstellt und per HTML-Mail über den Status auf dem Laufenden hält.
Das Konzept: Remote-Pull-Backup
Im Gegensatz zu Backups, die vom Quellserver „gepusht“ werden, nutzt dieses Skript das Pull-Prinzip. Ein zentraler Backup-Server steuert den Prozess:
- Zentraler Start: Das Skript läuft auf dem Backup-System (z. B. via Cronjob).
- Sicherer Login: Per SSH loggt sich das Skript auf dem Zielserver ein.
- Effizienter Abgleich: Nur geänderte Daten werden übertragen.
Die Architektur im Überblick
Die Logik ist zweistufig aufgebaut, um Speicherplatz zu sparen und dennoch eine schnelle Wiederherstellung zu ermöglichen.
| Phase | Tool | Beschreibung |
| Synchronisation | rsync | Spiegelt das Dateisystem des Remote-Servers 1:1 in ein lokales Verzeichnis. |
| Archivierung | tar | Erstellt komprimierte Abbilder (.tgz). Sonntags ein Full-Backup (Level 0), an Wochentagen nur die Änderungen (Differenziell). |
| Reporting | sendmail | Generiert eine HTML-E-Mail mit Statusfarben und Speicherplatz-Statistik. |
Kernfunktionen des Skripts
- Plausibilitäts-Check: Das Skript prüft nach der Archivierung, ob die Datei existiert und größer als 0 Byte ist. Ein abgebrochener Prozess wird sofort als FEHLER markiert.
- Dynamische Parameter: Servername und Host-Adresse werden als Argumente übergeben. Ein Skript bedient somit beliebig viele Server. Für ein zusätzliches manuelles Level 0 / Full-Backup kann man auch den Level als Parameter übergeben.
- Bandbreiten-Limitierung: Durch
--bwlimitwird verhindert, dass das Backup während der Geschäftszeiten die Internetverbindung blockiert. - Intelligentes Log-Parsing: Die E-Mail enthält die letzten Zeilen des Rsync-Logs, sodass man sofort sieht, welche Dateien übertragen wurden.
Das Skript (Anonymisierte Vorlage)
Hier ist die Struktur des Skripts zum Nachbauen. Pfade und Adressen wurden durch Platzhalter ersetzt.
#!/bin/bash
# ==============================================================================
# Remote-Backup-Skript mit Rsync, Tar-Differenz und HTML-Reporting
# ------------------------------------------------------------------------------
# Funktionsweise:
# 1. Sync per Rsync (Remote Pull)
# 2. Archivierung per Tar (Sonntag: Full, Wochentag: Differenziell)
# 3. Statusbericht per HTML-E-Mail
# ==============================================================================
# --- 1. KONFIGURATION (Bitte anpassen) ---
PATH=/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MNT="/mnt/backup-storage" # Lokaler Pfad zum Backup-Speicher
NAME="${1:-meinserver}" # Kurzname für Logs und Dateien
REMOTE_HOST="${2:-remote.example.com}" # Hostname oder IP des Zielservers
SSH_PORT="22" # SSH-Port des Zielservers
SENDER="backup@deine-domain.de" # Absender der Status-Mail
RECEIVER="admin@deine-domain.de" # Empfänger der Status-Mail
# Ausschlüsse für das Backup (Pfade auf dem Remote-Server)
EXCLUDES=(
--exclude /proc/ --exclude /sys/ --exclude /dev/
--exclude /tmp/ --exclude /run/ --exclude /mnt/
--exclude /media/ --exclude /backup/ --exclude /var/cache/
)
# Variablen für Logs und Temp-Dateien
DIR="$MNT/$NAME"
RSYNC_LOG="$MNT/$NAME-rsync.log"
TMP_MAIL=$(mktemp /tmp/backup_mail.XXXXXX.html)
DAY=$(date +%w) # 0=Sonntag (Full-Backup), 1-6=Diff-Backup
# --- 2. HILFSFUNKTIONEN ---
log_event() {
local msg="$1"
echo "$(date '+%d.%m.%Y %H:%M:%S') : $msg" >> "$MNT/backup_global.log"
logger -t "Backup-$NAME" "$msg"
}
get_size() {
[ -f "$1" ] && du -sh "$1" | cut -f1 || echo "0"
}
# --- 3. PRÜFUNG & START ---
[ -d "$DIR" ] || mkdir -p "$DIR"
log_event "Backup-Start für $NAME (Level $DAY) auf $REMOTE_HOST"
RSYNC_STATUS="Fehlgeschlagen"
TAR_STATUS="Wartet..."
COLOR_RSYNC="#d9534f" # Rot
COLOR_TAR="#777777" # Grau
# --- 4. RSYNC SYNCHRONISATION ---
# Wir holen die Daten vom Remote-Server in unser lokales Verzeichnis
rsync -avzHPWAX --delete-after --bwlimit=1000 "${EXCLUDES[@]}" \
-e "ssh -p $SSH_PORT" root@"$REMOTE_HOST":/ "$DIR/" > "$RSYNC_LOG" 2>&1
if [ $? -eq 0 ]; then
RSYNC_STATUS="OK"
COLOR_RSYNC="#5cb85c" # Grün
touch "$DIR/.stamp"
else
log_event "KRITISCH: Rsync-Fehler bei $NAME. Archivierung wird dennoch versucht."
fi
# --- 5. TAR ARCHIVIERUNG (Plausibilisierung) ---
cd "$DIR" || exit
ARCHIVE="$MNT/backup-$NAME-$DAY.tgz"
LAST_FULL="$MNT/backup-$NAME-0.tgz"
if [ "$DAY" = "0" ]; then
# Sonntag: Full Backup. Altes Archiv wird zur Vorwoche rotiert.
[ -f "$ARCHIVE" ] && mv "$ARCHIVE" "$MNT/backup-$NAME-lastweek.tgz"
tar cvfz "$ARCHIVE" ./ > /dev/null 2>&1
else
# Wochentag: Differenzielles Backup bezogen auf Level 0
if [ -f "$LAST_FULL" ]; then
find ./ -cnewer "$LAST_FULL" -type f > "$MNT/$NAME-tar-list.log"
tar cvfz "$ARCHIVE" -T "$MNT/$NAME-tar-list.log" > /dev/null 2>&1
else
log_event "Warnung: Kein Full-Backup gefunden. Erstelle Level 0..."
tar cvfz "$ARCHIVE" ./ > /dev/null 2>&1
fi
fi
# Prüfung: Existiert das Archiv und hat es Inhalt?
if [ -s "$ARCHIVE" ]; then
TAR_STATUS="OK"
COLOR_TAR="#5cb85c"
else
TAR_STATUS="FEHLER (Archiv leer oder fehlt)"
COLOR_TAR="#d9534f"
fi
# --- 6. HTML E-MAIL BERICHT ERSTELLEN ---
cat <<EOF > "$TMP_MAIL"
<html>
<head>
<style>
body { font-family: 'Segoe UI', Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; }
.card { background: #fff; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
h2 { color: #333; border-bottom: 2px solid #0056b3; padding-bottom: 10px; }
.status { padding: 8px 12px; color: white; border-radius: 3px; font-weight: bold; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { text-align: left; padding: 12px; border: 1px solid #eee; }
th { background: #f8f8f8; }
pre { background: #222; color: #adff2f; padding: 15px; border-radius: 3px; font-size: 12px; overflow-x: auto; }
</style>
</head>
<body>
<div class="card">
<h2>Backup-Status: $NAME</h2>
<p><strong>Zeitpunkt:</strong> $(date "+%d.%m.%Y %H:%M")<br>
<strong>Methode:</strong> Level $DAY (0=Full, 1-6=Diff)</p>
<table>
<tr><th>Prozess</th><th>Status</th></tr>
<tr><td>Rsync Abgleich</td><td><span class="status" style="background:$COLOR_RSYNC;">$RSYNC_STATUS</span></td></tr>
<tr><td>Tar Archivierung</td><td><span class="status" style="background:$COLOR_TAR;">$TAR_STATUS</span></td></tr>
</table>
<h3>Archiv-Details</h3>
<ul>
<li>Datei: <code>$(basename "$ARCHIVE")</code></li>
<li>Größe: $(get_size "$ARCHIVE")</li>
</ul>
<h3>Rsync Log (Auszug)</h3>
<pre>$(tail -n 8 "$RSYNC_LOG")</pre>
</div>
</body>
</html>
EOF
# --- 7. VERSAND ---
(
echo "From: $SENDER"
echo "To: $RECEIVER"
echo "Subject: [Backup] $NAME - Status: $TAR_STATUS"
echo "Content-Type: text/html; charset=UTF-8"
echo "MIME-Version: 1.0"
echo ""
cat "$TMP_MAIL"
) | /usr/sbin/sendmail -t
log_event "Backup $NAME beendet. Status: $TAR_STATUS"
rm "$TMP_MAIL"
Warum diese Lösung?
Viele Standard-Lösungen sind komplex oder bieten zu wenig Feedback. Dieses Skript schließt die Lücke:
- Transparenz: Man sieht sofort im Postfach, ob alles „Grün“ ist.
- Sicherheit: Durch das Pull-Verfahren muss der Zielserver keine Zugangsdaten zum Backup-Server kennen.
- Kontrolle: Man behält die volle Gewalt über Exclude-Listen und Kompressionsraten.
Am besten nutzt man SSH-Keys für den Login, damit das Skript ohne Passworteingabe durchlaufen kann. Man achtet dabei darauf, den SSH-Key auf dem Zielserver idealerweise auf die benötigten Befehle einzuschränken.
Die Automatisierung: Einbindung in Cron
Da das Skript generisch aufgebaut ist, kannst man für jeden Server eine eigene Zeile in die Crontab eintragen. Beispielsweise mit crontab -e . Ein klassisches Setup sieht vor, die Backups in die frühen Morgenstunden zu legen, wenn die Serverlast niedrig ist:
# Jeden Morgen um 01:00 Uhr Backup für Server 'nc5'
00 01 * * * /usr/local/bin/backup-server-generic.sh nc5 nc5.zlauf.de
# Jeden Morgen um 02:00 Uhr Backup für den Webserver
00 02 * * * /usr/local/bin/backup-server-generic.sh web01 web01.beispiel.de
Es kann sinnvoll sein, die Zeiten leicht versetzt zu organisieren, damit der Backup-Server (und die Internetanbindung) nicht alle Systeme gleichzeitig bearbeitet.
Restore einzelner Dateien
Wenn man versehentlich eine wichtige Datei gelöscht hat, musst man nicht das komplette Gigabyte-Archiv entpacken. Dank tar kann man gezielt einzelne Files oder Verzeichnisse „restorieren“.
Zuerst suchst man im Archiv nach der gewünschten Datei:
tar -ztf /pfad/zu/backups/backup-nc5-0.tgz | grep "dateiname"
Nachdem der Pfad bekannt ist, extrahiert man die Datei in ein temporäres Verzeichnis (z. B. /tmp/restore), um sie danach wieder an ihren Platz zu schieben:
# Erstellt die Ordnerstruktur und extrahiert nur die eine Datei
tar -zxvf /pfad/zu/backups/backup-nc5-0.tgz -C /tmp/restore var/www/html/wichtige_datei.php
Wichtig: Da mit relativen Pfaden im Archiv arbeitet wird, musst man beim Entpacken das Zielverzeichnis mit
-Cangeben, damittarnicht versehentlich Dateien im aktuellen Verzeichnis überschreibt. Erst prüfen, dann verschieben!
