Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
316 lines
12 KiB
Markdown
316 lines
12 KiB
Markdown
# 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. |
|
|
|
|
```bash
|
|
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 `rsync` vers 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)
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
# 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 à jour
|
|
> `DATABASE_HOST` et `REDIS_HOST` dans le ConfigMap, la NetworkPolicy
|
|
> `allow-egress-to-data-node`, `pg_hba.conf` et 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**.
|
|
|
|
```bash
|
|
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 :
|
|
|
|
```bash
|
|
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 :
|
|
>
|
|
> ```bash
|
|
> # 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é](./13-securite-durcissement.md)**
|