drill: monthly media restore probe, proven against production (#0030)
Counterpart to restore-drill.yaml: that one covers the databases, this the Synapse media store. Separate job on purpose - the database probe needs a throwaway Postgres, this one the production PVC read-only, and folding two different permission and failure pictures into one job makes an emergency harder to diagnose, not easier. The name is deliberate. BackupJobFailed already matches restore-drill.*, so a failure is covered without a new rule and reaches the maintenance room through the single alertmanager route. It re-proves its own comparison every run. After the check passes, one shared file is altered by a byte and the comparison must report it - otherwise the job fails with "this probe proves nothing". A comparison that has only ever said "equal" is a guess, and that stays true when it runs monthly rather than once. The intersection carries the proof, not the totals: the backup is a snapshot while production keeps running, and Synapse prunes its own preview caches. One-sided files are therefore tolerated in url_cache and url_cache_thumbnails and are an error anywhere else. The production PVC is mounted read-only and only emptyDir is written. On today's single node a second read-only mount of the ReadWriteOnce volume is unproblematic; the comment says what would have to change if a second node ever appeared, because that failure would alert without anything being wrong with the data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM
This commit is contained in:
co-authored by
Claude Opus 5
parent
73f56c3414
commit
59b7d288dc
@@ -42,6 +42,7 @@ resources:
|
||||
- synapse-backup.yaml
|
||||
# Monatliche Restore-Probe: spielt die Sicherungen isoliert zurueck (#0030)
|
||||
- restore-drill.yaml
|
||||
- restore-drill-media.yaml
|
||||
# Automatisierte TURN-Secret-Rotation (Issue #38)
|
||||
- turn-secret-rotation-secret.yaml
|
||||
- turn-secret-rotation.yaml
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# Monatliche Medien-Restore-Probe (#0030). Gegenstueck zu restore-drill.yaml: dort
|
||||
# Datenbanken, hier der Medienspeicher von Synapse.
|
||||
#
|
||||
# Warum eigener Job und nicht angehaengt: Die Datenbank-Probe braucht eine Wegwerf-Postgres,
|
||||
# diese hier das Produktions-PVC (lesend). Zwei verschiedene Rechte- und Fehlerbilder in
|
||||
# einem Job zu buendeln macht die Fehlersuche im Ernstfall schwerer, nicht leichter.
|
||||
#
|
||||
# Der NAME ist bewusst `restore-drill-media`: Die bestehende Regel BackupJobFailed matcht
|
||||
# `restore-drill.*`, also ist der Fehlschlag ohne neue Alarmregel abgedeckt und landet
|
||||
# ueber die einzige Alertmanager-Route im wartung-Raum.
|
||||
#
|
||||
# ⚠️ Das Produktions-PVC wird `readOnly: true` gemountet. Geschrieben wird ausschliesslich
|
||||
# nach emptyDir. Das PVC ist ReadWriteOnce; im heutigen Ein-Knoten-Cluster ist ein zweiter
|
||||
# lesender Mount unproblematisch. Kaeme je ein zweiter Knoten dazu, koennte der Pod dort
|
||||
# landen und der Mount scheitern - der Job schluege fehl und alarmierte, ohne dass an den
|
||||
# Daten etwas waere. Dann gehoert hier eine Node-Affinitaet zu Synapse hin.
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: restore-drill-media
|
||||
namespace: matrix
|
||||
spec:
|
||||
# 4. des Monats, 05:20 - eine Stunde nach der Datenbank-Probe, damit beide denselben
|
||||
# frischen Sicherungsstand pruefen, sich aber nicht um Storage Box und CPU streiten.
|
||||
schedule: "20 5 4 * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: synapse-backup # NetworkPolicy/Egress wie die Backups
|
||||
app.kubernetes.io/component: restore-drill-media
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: drill
|
||||
image: rohana.axion1337.de/sorb/axion-backup:v2
|
||||
env:
|
||||
- name: BORG_PASSPHRASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: synapse-backup-credentials
|
||||
key: borg-passphrase
|
||||
- name: SSH_PRIVATE_KEY_FILE
|
||||
value: /secrets/ssh/ssh-private-key
|
||||
- name: SSH_KNOWN_HOSTS_FILE
|
||||
value: /secrets/known-hosts/known_hosts
|
||||
command: ["sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
export BORG_RSH="ssh -i $SSH_PRIVATE_KEY_FILE -o UserKnownHostsFile=$SSH_KNOWN_HOSTS_FILE -o StrictHostKeyChecking=yes"
|
||||
export BORG_REPO='ssh://u641795@u641795.your-storagebox.de:23/./synapse-backup'
|
||||
cd /scratch
|
||||
latest=$(borg list --last 1 --format '{archive}' "$BORG_REPO")
|
||||
echo "[drill] Archiv: $latest"
|
||||
borg extract "$BORG_REPO::$latest" media/media_store
|
||||
anz=$(find media/media_store -type f | wc -l)
|
||||
echo "[drill] zurueckgespielt: ${anz} Dateien, $(du -sk media/media_store | cut -f1) KB"
|
||||
echo "[drill] Produktion jetzt: $(find /live/media_store -type f | wc -l) Dateien, $(du -sk /live/media_store | cut -f1) KB"
|
||||
if [ "$anz" -le 0 ]; then
|
||||
echo "[drill] FEHLER: nichts extrahiert - Sicherung unbrauchbar!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
( cd /scratch/media/media_store && find . -type f -print0 | xargs -0 sha256sum ) | sort -k2 > /scratch/restore.sha
|
||||
( cd /live/media_store && find . -type f -print0 | xargs -0 sha256sum ) | sort -k2 > /scratch/live.sha
|
||||
awk '{print $2}' /scratch/restore.sha | sort > /scratch/r.names
|
||||
awk '{print $2}' /scratch/live.sha | sort > /scratch/l.names
|
||||
comm -12 /scratch/r.names /scratch/l.names > /scratch/beide.names
|
||||
|
||||
# Die Sicherung ist eine Momentaufnahme, die Produktion laeuft weiter.
|
||||
# Beweiskraeftig ist deshalb die Schnittmenge - die Gesamtzahlen weichen
|
||||
# regulaer ab, weil Synapse seine Vorschau-Caches selbst aufraeumt.
|
||||
abw() { awk 'NR==FNR{h[$2]=$1;next} ($2 in h) && (h[$2]!=$1){print $2}' /scratch/live.sha "$1"; }
|
||||
n_abw=$(abw /scratch/restore.sha | wc -l)
|
||||
echo "[drill] gemeinsam: $(wc -l < /scratch/beide.names), davon abweichend: ${n_abw}"
|
||||
if [ "$n_abw" -ne 0 ]; then
|
||||
echo "[drill] FEHLER: gemeinsame Dateien sind NICHT byte-gleich:"
|
||||
abw /scratch/restore.sha | head -10
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Nur url_cache und url_cache_thumbnails duerfen einseitig sein - alles
|
||||
# andere waere ein echter Fehlbetrag und kein Cache-Effekt.
|
||||
fremd=$(comm -23 /scratch/r.names /scratch/l.names | sed 's|^\./||' | cut -d/ -f1 \
|
||||
| grep -vE '^url_cache(_thumbnails)?$' | sort -u || true)
|
||||
if [ -n "$fremd" ]; then
|
||||
echo "[drill] FEHLER: einseitige Dateien ausserhalb der Vorschau-Caches:"
|
||||
echo "$fremd"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Positivkontrolle bei JEDEM Lauf: eine gemeinsame Datei um ein Byte
|
||||
# veraendern und pruefen, dass der Vergleich es merkt. Ein Abgleich, der
|
||||
# nur je "gleich" gesagt hat, ist eine Vermutung - auch als CronJob.
|
||||
opfer=$(head -1 /scratch/beide.names)
|
||||
printf 'x' >> "/scratch/media/media_store/${opfer}"
|
||||
( cd /scratch/media/media_store && find . -type f -print0 | xargs -0 sha256sum ) | sort -k2 > /scratch/kaputt.sha
|
||||
if [ "$(abw /scratch/kaputt.sha | wc -l)" -eq 0 ]; then
|
||||
echo "[drill] FEHLER: der Vergleich erkennt eine absichtliche Aenderung NICHT - diese Probe beweist nichts!"
|
||||
exit 1
|
||||
fi
|
||||
echo "[drill] OK - ${anz} Dateien zurueckgespielt, alle gemeinsamen byte-gleich, Vergleich gegengeprueft"
|
||||
volumeMounts:
|
||||
- name: ssh-key
|
||||
mountPath: /secrets/ssh
|
||||
readOnly: true
|
||||
- name: known-hosts
|
||||
mountPath: /secrets/known-hosts
|
||||
readOnly: true
|
||||
- name: scratch
|
||||
mountPath: /scratch
|
||||
- name: live
|
||||
mountPath: /live
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: 100m
|
||||
limits:
|
||||
memory: 1Gi
|
||||
volumes:
|
||||
- name: ssh-key
|
||||
secret:
|
||||
secretName: synapse-backup-credentials
|
||||
defaultMode: 0400
|
||||
items:
|
||||
- key: ssh-private-key
|
||||
path: ssh-private-key
|
||||
- name: known-hosts
|
||||
configMap:
|
||||
name: synapse-backup-known-hosts
|
||||
- name: scratch
|
||||
emptyDir:
|
||||
sizeLimit: 4Gi
|
||||
- name: live
|
||||
persistentVolumeClaim:
|
||||
claimName: matrix-stack-synapse-media
|
||||
readOnly: true
|
||||
Reference in New Issue
Block a user