Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.9 KiB
Sauvegarde de la base utilisateurs sur S3 — conception
Date : 2026-06-24 Statut : conception validée, prête pour le plan d'implémentation
Contexte et objectif
users.sqlite contient les comptes utilisateurs (src/auth/db.py) et les tokens API
(src/api/tokens_db.py) — un seul fichier SQLite, ~36 Ko aujourd'hui, attendu jusqu'à
~500 utilisateurs × quelques centaines de lignes. C'est la seule donnée non reproductible
du projet (la base DECP en Parquet/DuckDB est régénérable, donc hors périmètre).
L'app tourne sur un VPS unique (systemd + gunicorn, /var/www/<APP_NAME>). On veut prévenir
toute perte de données par des sauvegardes régulières vers un stockage compatible S3
(non-Amazon), avec rotation multi-paliers et un mécanisme de restauration.
Décisions validées
- Déclencheur : timer systemd sur le VPS (indépendant de gunicorn, robuste aux redémarrages, pas de duplication entre workers).
- Chiffrement : les sauvegardes sont chiffrées côté application avant l'envoi (données personnelles : emails, hash de mots de passe, tokens).
- Périmètre : le seul fichier
users.sqlite(cheminUSERS_DB_PATH).
Vue d'ensemble
Un nouveau module src/backup/ expose une CLI à trois sous-commandes : backup, list,
restore. Le timer systemd lance backup toutes les heures. Chaque exécution :
- produit un snapshot cohérent de
users.sqlite(API de backup en ligne de SQLite, sûre même en cas d'écriture concurrente) ; - le compresse (gzip) puis le chiffre ;
- l'envoie sur le S3 sous une clé horodatée ;
- applique la rotation : recalcule l'ensemble à conserver et supprime les obsolètes.
Schéma de rétention
Union de quatre paliers — une sauvegarde est conservée si elle qualifie pour au moins un palier :
| Palier | Granularité | Horizon | ~ Nb gardés |
|---|---|---|---|
| Horaire | 1 par heure | 12 dernières heures | ~12 |
| Bi-quotidien | 1 par tranche de 12 h | 72 dernières heures | ~6 |
| Quotidien | 1 par jour | 21 derniers jours | ~21 |
| Mensuel | 1 par mois calendaire | 12 derniers mois | ~12 |
Soit ~51 fichiers au régime permanent, chacun de quelques dizaines de Ko → stockage négligeable.
Algorithme — fonction pure
select_retained(timestamps: list[datetime], now: datetime) -> set[datetime] :
- Pour chaque palier à période fixe (horaire, bi-quotidien, quotidien) : regrouper les
horodatages par tranche (
floor((now - t) / période)), et pour chaque tranche comprise dans l'horizon, garder le plus récent de la tranche. - Pour le palier mensuel : regrouper par mois calendaire (
(année, mois)) car les mois ont des durées variables ; garder le plus récent de chaque mois sur les 12 derniers mois. - Le résultat est l'union des ensembles retenus de tous les paliers.
- L'ensemble à supprimer = tous les horodatages présents − ensemble retenu.
Aucune I/O dans cette fonction : elle prend la liste des horodatages (extraits des clés S3)
et now, et renvoie quoi garder. Testée unitairement de façon exhaustive (chevauchements
de paliers, bascules d'horizon, mois variables, ensemble vide).
Modules (src/backup/)
Chaque module a une responsabilité unique et une interface explicite, testable isolément.
rotation.py—select_retained(fonction pure, pas d'I/O). Cœur logique, tests unitaires nombreux.snapshot.py— produit un snapshot SQLite cohérent via l'API de backup en ligne (sqlite3.connect(src).backup(dest)) vers un fichier temporaire, puis gzip. Renvoie le chemin du fichier compressé.crypto.py— chiffrement/déchiffrement symétrique authentifié (Fernet, libcryptography). Clé lue depuisBACKUP_ENCRYPTION_KEY. Round-trip testé.storage.py— wrapper S3 (upload,list,download,delete) viaboto3avecendpoint_url(compatible tout fournisseur S3 non-Amazon). Configuration lue depuis l'env.cli.py—argparse:backup,list,restore. Point d'entrée appelé par le timer systemd et par l'opérateur pour la restauration.
Nommage des objets S3
Clé : <S3_BACKUP_PREFIX>/users-<YYYYMMDDTHHMMSSZ>.sqlite.gz.enc, horodatage UTC ISO 8601
compact, triable lexicographiquement. La rotation extrait l'horodatage depuis la clé.
Procédure de sauvegarde (backup)
- Snapshot cohérent de
users.sqlite→ fichier temporaire. - gzip.
- Chiffrement Fernet.
- Upload sur S3 sous la clé horodatée.
- Rotation : lister les clés sous le préfixe, parser les horodatages, calculer l'ensemble
retenu via
select_retained, supprimer le reste. - Nettoyage des fichiers temporaires (y compris en cas d'erreur).
Journalisation de chaque étape (succès/échec, clés uploadées/supprimées) pour suivi dans les logs systemd.
Procédure de restauration (restore, manuelle, avec garde-fous)
list→ affiche les sauvegardes disponibles (clé, date, taille), triées.restore <clé>:- télécharge l'objet ;
- déchiffre puis décompresse vers un fichier temporaire ;
- vérifie l'intégrité SQLite (
PRAGMA integrity_check) — refuse de restaurer si KO ; - fait une copie de secours de la base courante (
users.sqlite.bak-<ts>) ; - remplace de façon atomique (
os.replace).
- Le script avertit d'arrêter le service (
systemctl stop <APP_NAME>) avant la restauration et de le redémarrer après.
La restauration est volontairement manuelle : un outil de reprise sur sinistre ne doit jamais restaurer automatiquement.
Déploiement et configuration
- Unités systemd dans
deploy/:decpinfo-backup.service(typeoneshot, exécutepython -m src.backup backup) ;decpinfo-backup.timer(OnCalendar=hourly,Persistent=truepour rattraper un créneau manqué après un redémarrage).
- Nouvelles variables dans
.template.env:S3_ENDPOINT_URL,S3_BUCKET,S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEY,S3_BACKUP_PREFIX,BACKUP_ENCRYPTION_KEY.
- Nouvelles dépendances :
boto3,cryptography. - Gestion de la clé de chiffrement :
BACKUP_ENCRYPTION_KEYdoit être sauvegardée hors du serveur (gestionnaire de secrets / coffre). Si elle est perdue, les sauvegardes sont irrécupérables.
Tests
- Unitaires :
select_retained(chevauchements, horizons, mois variables, vide) ; round-tripcrypto; nommage/parsing des clés. - Intégration : snapshot d'une base SQLite réelle → vérifie une base valide ;
cycle complet backup → list → restore sur un faux backend S3 (mock
boto3oumoto), vérifie l'égalité du contenu restauré et la copie de secours créée.
Hors périmètre
- Sauvegarde de la base DECP (régénérable).
- Restauration automatique / orchestration multi-serveurs.
- Réplication temps réel.