Thomas Fransolet c9580fd0a8 Scripts de sauvegarde Postgres
Dump -Fc avec vérification de relecture et rotation, test de restauration qui
compare les comptages table par table, extraction par instance, contrôle de
fraîcheur, units systemd et procédure de restauration écrite.

Le dump Mongo est volontairement en lecture seule : aucun mongorestore, pour
qu'aucune erreur de manipulation ne puisse écrire dans la prod.

Ce qui manque pour que ce soit une sauvegarde : BACKUP_DEST n'est pas tranché.
Tant qu'il est vide, le dump reste sur la machine qui héberge la base et ne
protège de rien — le script le dit à chaque exécution. Et notify.sh ne fait
qu'un logger, donc les trois surveillances écrivent dans un journal que
personne ne lit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:11:33 +02:00

100 lines
4.0 KiB
Markdown

# Restauration — procédure
> Document à lire **sous stress**. Il ne cherche pas à expliquer, il dit quoi taper.
> La procédure est rejouée automatiquement chaque mois par `myim-restore-check.timer`.
**Règle unique, jamais enfreinte : la base de production n'est jamais la cible d'un `pg_restore`.**
On restaure toujours dans une base neuve, on regarde, puis on décide.
---
## Scénario A — sinistre complet (la machine est perdue)
1. Récupérer le dump depuis la destination hors site :
```sh
rclone lsl "$BACKUP_DEST" | sort -k2 | tail -5 # choisir, ne pas prendre le dernier à l'aveugle
rclone copy "$BACKUP_DEST/<fichier>.dump" .
```
⚠️ Vérifier la **date** avant de restaurer. Le dernier dump peut être postérieur à
l'incident et contenir déjà les dégâts.
2. Monter Postgres avec ses extensions (`postgis` **et** `vector`), puis appliquer le schéma :
```sh
docker compose -f docker-compose.preprod.yaml up -d postgres
dotnet ef database update
```
3. Restaurer :
```sh
docker exec -i myim_postgres pg_restore -U mym -d my_info_mate \
--no-owner --no-privileges < <fichier>.dump
```
4. **Relancer l'indexation des embeddings.** `ContentEmbeddings` est exclu des dumps
(régénérable, et ça divise leur taille par 7). Sans cette étape le guide IA répond
à côté sans le dire.
5. Vérifier avant d'ouvrir au public : login manager-app, une app visiteur avec sa clé API,
un parcours, une carte, un PDF, **un quiz avec ses questions**. C'est la liste du point 20
du §1quinquies, et les deux derniers sont ceux qui échouent en silence.
## Scénario B — un seul client à récupérer
Un client a supprimé son contenu, ou demande ses données (RGPD). **On ne restaure pas la
prod** : on extrait depuis le dump global.
```sh
./extract-instance.sh <dump-global>.dump <instanceId>
```
Le script restaure le dump dans une base jetable, la réduit à cette instance, la re-dumpe,
et affiche les comptages obtenus. Lire ces comptages : c'est là qu'on voit qu'un quiz est
arrivé sans ses questions.
Puis réinjecter en connaissance de cause — les ID sont conservés, donc une ligne encore
présente en prod provoquera un conflit de clé. Choisir explicitement entre écraser et
ignorer ; il n'y a pas de bon défaut.
### Ce que l'extraction ne contient pas
- **Les médias.** Les blobs vivent dans Firebase Storage, la base n'en garde que le chemin.
Une extraction seule rend un catalogue de liens morts.
- **Les lignes sans `InstanceId`** (logs d'audit système) : elles n'appartiennent à aucun
client, donc à aucun export. C'est volontaire — sans ça elles fuiteraient dans l'export
de *chaque* client.
- `SubscriptionPlans`, `spatial_ref_sys` et `__EFMigrationsHistory` repartent entiers :
données de référence, pas données client.
## Vérifier une sauvegarde sans rien restaurer
```sh
./restore-check.sh <fichier>.dump
```
Restaure dans une base jetable, compare les comptages table par table avec la base courante,
puis supprime la base jetable. Aucun effet sur la prod. Écart attendu : `ContentEmbeddings` à 0.
---
## Pièges rencontrés pour de vrai
**« collation version mismatch »** — bloque la création de toute base, donc toute
restauration. Arrivé deux fois en local (09/08 et 07/09). Dans cet ordre ; le second seul
masque le symptôme sans le corriger :
```sql
REINDEX DATABASE "<base>";
ALTER DATABASE "<base>" REFRESH COLLATION VERSION;
```
Peut concerner `template1` et `postgres`, pas seulement la base applicative — et c'est
`template1` qui bloque `createdb`.
**`pg_restore --list /dev/stdin`** échoue (« did not find magic string ») : un pipe n'est pas
seekable. Sans argument, `pg_restore` lit stdin correctement.
**`docker exec -i` dans une boucle** consomme le stdin de la boucle. Ne pas passer `-i` quand
la commande n'en a pas besoin.
**`"InstanceId" <> 'x'` ne supprime pas les `NULL`.** En SQL `NULL <> 'x'` vaut `NULL`, pas
vrai. Utiliser `is distinct from`. Repéré parce que la somme des extractions par client
dépassait le global de 37 lignes.