Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
12 KiB
12 — Sauvegardes et restauration
Durée : environ 2 h, dont un test de restauration réel.
Une sauvegarde qui n'a jamais été restaurée n'est pas une sauvegarde. C'est la seule phrase de ce dossier qui mérite d'être affichée au mur.
1. Objectifs
| Indicateur | Cible | Ce que cela signifie |
|---|---|---|
| RPO (perte maximale) | 5 minutes | archive_timeout = 300 force la clôture d'un segment WAL toutes les 5 min, même sans activité. |
| RTO (temps de remise en service) | < 2 h | Reconstruire db-01 et restaurer via WAL-G. |
| Rétention PITR | 7 jours | 7 sauvegardes complètes + les WAL associés. |
| Rétention des dumps | 7 j locaux, 30 j distants | Storage Box. |
Pour une plateforme de réservation, perdre 5 minutes signifie perdre au plus quelques réservations, identifiables dans les journaux Loki et rejouables manuellement. C'est un compromis raisonnable à ce stade.
2. Trois mécanismes, volontairement redondants
db-01
│
┌───────────────────────┼───────────────────────┐
│ │ │
WAL-G pg_dump -Fc Snapshots
continu quotidien Hetzner
│ │ │
│ chiffré libsodium │ chiffré age │ (image disque)
▼ ▼ ▼
Object Storage Storage Box Hetzner Cloud
xpeditis-prod-pgbackup uXXXXXX/dumps (rétention 7 j)
Pourquoi trois.
| Panne | Ce qui vous sauve |
|---|---|
| « J'ai supprimé les réservations de mars » | WAL-G / PITR — retour à l'instant précédant l'erreur. |
| Corruption logique découverte 3 jours après | WAL-G ou dump du jour concerné. |
| Compte Object Storage compromis ou supprimé | Dump sur Storage Box — autre service, autre identifiant, autre protocole. |
| Bug WAL-G, ou montée de version majeure de PostgreSQL | Dump logique — restaurable dans n'importe quelle version. |
| Perte totale du serveur | WAL-G + snapshot Hetzner. |
| Rançongiciel sur db-01 | Snapshots de la Storage Box — voir avertissement ci-dessous. |
Le point faible d'un rançongiciel. db-01 possède les identifiants d'écriture vers Object Storage et vers la Storage Box. Un attaquant ayant obtenu root peut donc chiffrer ou effacer les deux. La seule protection réelle est l'instantané côté fournisseur : activez les snapshots de la Storage Box (gratuits, dans son interface) et gardez les sauvegardes Hetzner Cloud activées. Ils ne sont pas accessibles depuis le serveur.
3. Ce qui tourne automatiquement
| Unité | Quand | Fait quoi |
|---|---|---|
xpeditis-backup.timer |
tous les jours 02h30 | WAL-G backup-push, purge (7 rétentions), pg_dump chiffré, envoi Storage Box, purge locale, battement de cœur. |
xpeditis-backup-verify.timer |
dimanche 04h00 | Restaure le dernier dump dans une base jetable et contrôle sa cohérence. |
archive_command |
en continu | Chaque segment WAL part sur Object Storage dès sa clôture. |
ssh deploy@<db_ip> "systemctl list-timers 'xpeditis-*' --no-pager"
ssh deploy@<db_ip> 'sudo journalctl -u xpeditis-backup -n 40 --no-pager'
Contrôles de sécurité intégrés
Le script pg-backup.sh refuse de considérer une sauvegarde comme réussie si :
- le conteneur PostgreSQL n'est pas démarré ;
wal-g backup-pushéchoue ;pg_dumpéchoue ;- le dump fait moins de 50 Ko (base vide ou erreur silencieuse) ;
- le chiffrement
ageéchoue ; - le
rsyncvers la Storage Box échoue.
Chaque échec part sur Discord et le battement de cœur n'est pas envoyé, ce qui déclenche l'alerte externe.
4. Le test de restauration — à faire maintenant
C'est l'étape que tout le monde saute et qui coûte le plus cher le jour venu.
4.1 Vérification automatique (sans impact)
ssh deploy@<db_public_ipv4>
sudo /opt/xpeditis/data-node/backup/pg-restore.sh verify
Ce que fait le script : déchiffre le dernier dump, crée
xpeditis_restore_check, restaure avec --exit-on-error, compte les tables et
les lignes de users, organizations, csv_bookings, migrations, échoue si
moins de 10 tables ont été restaurées, puis supprime la base jetable.
Attendu : SAUVEGARDE VALIDE.
4.2 Restauration à un instant T — le vrai test
Faites-le avant l'ouverture au public, pendant que l'enjeu est nul.
# 1. Marqueur horodaté
ssh deploy@<db_ip> 'cd /opt/xpeditis/data-node && sudo docker compose exec -T -u postgres postgres \
psql -d xpeditis_prod -c "CREATE TABLE test_pitr (t timestamptz DEFAULT now(), note text);
INSERT INTO test_pitr(note) VALUES (:'"'"'avant'"'"');
SELECT * FROM test_pitr;"'
# Notez l'horodatage renvoyé.
sleep 360 # au-delà de archive_timeout, pour garantir l'archivage du WAL
# 2. Destruction volontaire
ssh deploy@<db_ip> 'cd /opt/xpeditis/data-node && sudo docker compose exec -T -u postgres postgres \
psql -d xpeditis_prod -c "DROP TABLE test_pitr;"'
# 3. Arrêter l'application (sinon les écritures pendant la restauration sont perdues)
kubectl -n xpeditis-prod scale deploy/xpeditis-backend --replicas=0
# 4. Restaurer à l'instant précédant la suppression
ssh deploy@<db_ip>
sudo /opt/xpeditis/data-node/backup/pg-restore.sh pitr "2026-09-01 14:32:00"
# 5. Suivre le rejeu des WAL
cd /opt/xpeditis/data-node && sudo docker compose logs -f postgres
# Attendre : "database system is ready to accept connections"
# 6. Vérifier
sudo docker compose exec -T -u postgres postgres psql -d xpeditis_prod -c 'SELECT * FROM test_pitr;'
# La table doit être revenue.
# 7. Nettoyer et redémarrer
sudo docker compose exec -T -u postgres postgres psql -d xpeditis_prod -c 'DROP TABLE test_pitr;'
kubectl -n xpeditis-prod scale deploy/xpeditis-backend --replicas=2
Chronométrez. Le temps mesuré ici est votre RTO réel, pas celui du tableau.
Le script conserve l'ancien répertoire de données dans
/var/lib/xpeditis/pgdata-avant-pitr-<horodatage>. Supprimez-le seulement
après avoir validé la restauration — c'est votre filet.
5. Restauration
5.1 Suppression accidentelle de données — PITR
Le plus fréquent, et le plus stressant.
# 1. NE RIEN FAIRE D'AUTRE. Chaque écriture supplémentaire complique le retour.
kubectl -n xpeditis-prod scale deploy/xpeditis-backend --replicas=0
# 2. Déterminer l'instant précis, dans les journaux
# Grafana : {namespace="xpeditis-prod"} |= "DELETE" ou "DROP"
# 3. Restaurer à la seconde qui précède
ssh deploy@<db_ip>
sudo /opt/xpeditis/data-node/backup/pg-restore.sh pitr "AAAA-MM-JJ HH:MM:SS"
# 4. Vérifier AVANT de rouvrir
sudo docker compose exec -T -u postgres postgres psql -d xpeditis_prod \
-c 'SELECT count(*) FROM csv_bookings; SELECT max(created_at) FROM csv_bookings;'
# 5. Rouvrir
kubectl -n xpeditis-prod scale deploy/xpeditis-backend --replicas=2
La PITR ramène toute la base à l'instant T. Les données créées après cet instant sont perdues aussi. Si seules quelques lignes sont concernées, préférez une restauration logique dans une base séparée (§5.2), puis copiez uniquement ce qui manque.
5.2 Récupérer quelques lignes sans toucher à la production
ssh deploy@<db_ip>
sudo /opt/xpeditis/data-node/backup/pg-restore.sh logical \
/var/lib/xpeditis/dumps/xpeditis-20260901T023000Z.dump.age \
xpeditis_recuperation
# Copier ce qui manque, et rien d'autre
sudo docker compose exec -T -u postgres postgres psql -d xpeditis_prod -c "
INSERT INTO csv_bookings
SELECT * FROM dblink('dbname=xpeditis_recuperation',
'SELECT * FROM csv_bookings WHERE id = ''<uuid>''')
AS t(...);"
Plus simple sans dblink : exporter les lignes en CSV depuis la base de
récupération, puis les réimporter.
La production n'est jamais touchée. C'est presque toujours la bonne approche.
5.3 Perte totale de db-01
RTO visé : moins de 2 h.
# 1. Recréer le serveur
cd infra/prod/terraform
terraform apply -replace=hcloud_server.db
# Le volume pgdata a prevent_destroy : il survit et sera rattaché.
# 2. Réinstaller
scp infra/prod/scripts/00-bootstrap-common.sh deploy@<nouvelle_ip>:/tmp/
ssh deploy@<nouvelle_ip> 'sudo APP_PRIVATE_IP=10.10.1.10 bash /tmp/00-bootstrap-common.sh data'
scp infra/prod/scripts/01-setup-data-node.sh deploy@<nouvelle_ip>:/tmp/
ssh deploy@<nouvelle_ip> 'sudo bash /tmp/01-setup-data-node.sh'
# 3. Redéposer configuration, .env.data (SOPS), clés age et Storage Box
# → 04-noeud-donnees.md, sections 3 et 4
# 4a. Si le volume a survécu : démarrer, c'est tout
sudo docker compose -f docker-compose.data.yml --env-file .env.data up -d --build
# 4b. Si le volume est perdu : restaurer depuis WAL-G
sudo docker compose run --rm -u postgres postgres \
wal-g backup-fetch /var/lib/postgresql/data/pgdata LATEST
sudo docker compose run --rm -u postgres postgres bash -c \
"touch /var/lib/postgresql/data/pgdata/recovery.signal
echo \"restore_command = 'wal-g wal-fetch \\\"%f\\\" \\\"%p\\\"'\" \
>> /var/lib/postgresql/data/pgdata/postgresql.auto.conf"
sudo docker compose up -d postgres
# 5. Vérifier puis rouvrir
kubectl -n xpeditis-prod scale deploy/xpeditis-backend --replicas=2
Si l'IP privée du nouveau serveur diffère de
10.10.1.20, mettez à jourDATABASE_HOSTetREDIS_HOSTdans le ConfigMap, la NetworkPolicyallow-egress-to-data-node,pg_hba.confet la cible Prometheus. Terraform réutilise l'IP fixe déclarée : cela ne devrait pas arriver.
5.4 Perte totale de app-01
Beaucoup plus simple : aucune donnée n'y réside.
cd infra/prod/terraform && terraform apply -replace=hcloud_server.app
# Puis : 03-durcissement-serveurs.md → 05-cluster-k3s.md → 06 → 09
# Mettez à jour les enregistrements DNS Cloudflare avec la nouvelle IP.
Comptez une heure. C'est le bénéfice direct d'avoir gardé le nœud applicatif entièrement sans état.
6. Sauvegarde manuelle avant opération risquée
Avant une migration lourde ou une montée de version majeure :
ssh deploy@<db_ip> 'sudo systemctl start xpeditis-backup.service'
ssh deploy@<db_ip> 'sudo journalctl -u xpeditis-backup -f'
Attendez la fin avant de lancer l'opération. Notez l'horodatage : c'est votre point de retour.
7. Ce qui n'est pas sauvegardé, et pourquoi
| Élément | Sauvegardé ? | Justification |
|---|---|---|
| PostgreSQL | oui, 3 fois | données métier |
| Documents Object Storage | par Hetzner (réplication) | pas de version antérieure : une suppression est définitive. Voir ci-dessous. |
| Redis | AOF sur disque local | cache + sessions. Une perte déconnecte les utilisateurs, sans plus. |
| Journaux Loki / métriques Prometheus | non | 31 j / 15 j de rétention, sans valeur au-delà. |
| Configuration k3s | non | entièrement reconstructible depuis infra/prod/. |
| État Terraform | manuellement | à chiffrer SOPS ou à stocker sur un backend S3. |
Les documents ne sont pas versionnés. Hetzner Object Storage réplique mais ne conserve pas d'historique : un connaissement supprimé par erreur — ou par une faille applicative — est définitivement perdu. Si ces documents ont une valeur contractuelle (ils en ont), ajoutez une synchronisation hebdomadaire vers la Storage Box :
# Sur db-01, tâche hebdomadaire rclone sync hetzner:xpeditis-prod-documents \ storagebox:xpeditis-prod/documents --backup-dir storagebox:xpeditis-prod/documents-anciens/$(date +%F)À mettre en place dans le premier mois d'exploitation.
8. Contrôle
[ ] Timers xpeditis-backup et xpeditis-backup-verify actifs
[ ] Une sauvegarde complète réussie, visible sur Object Storage ET Storage Box
[ ] pg-restore.sh verify : SAUVEGARDE VALIDE
[ ] TEST PITR RÉEL EFFECTUÉ ET RÉUSSI
[ ] RTO réel chronométré et noté
[ ] Snapshots de la Storage Box activés
[ ] Sauvegardes Hetzner Cloud activées sur les deux serveurs
[ ] Battement de cœur externe vert
[ ] WALG_LIBSODIUM_KEY et backup-age.key sauvegardées HORS LIGNE
[ ] État Terraform sauvegardé
[ ] Synchronisation des documents planifiée (dans le premier mois)
→ Suite : 13 — Sécurité