390 lines
11 KiB
Markdown
390 lines
11 KiB
Markdown
# 13 — Backups et reprise après sinistre
|
|
|
|
---
|
|
|
|
## Stratégie de backup
|
|
|
|
| Composant | Méthode | Fréquence | Rétention | Destination |
|
|
|---|---|---|---|---|
|
|
| PostgreSQL | `pg_dump` via CronJob | Quotidien 3h00 | 30 jours | Hetzner Object Storage |
|
|
| PostgreSQL WAL | Streaming (si self-hosted) | Continue | 7 jours | Object Storage |
|
|
| Redis | RDB snapshot + AOF | Chaque 5 min | 24h | Volume local |
|
|
| Secrets Kubernetes | Export manuel chiffré | Avant chaque changement | Illimité | Hors-cluster (coffre) |
|
|
| Fichiers S3 | Versioning objet | Permanent | Voir lifecycle | Object Storage |
|
|
| Configs K8s | GitOps dans le repo | À chaque commit | Git history | GitHub |
|
|
|
|
**Objectifs :**
|
|
- **RPO (Recovery Point Objective) :** 24h max (vous pouvez perdre au plus 24h de données)
|
|
- **RTO (Recovery Time Objective) :** 4h max (vous pouvez reconstruire en moins de 4h)
|
|
|
|
---
|
|
|
|
## Backup PostgreSQL — Option A (Neon.tech)
|
|
|
|
Si vous utilisez Neon.tech, les backups sont **automatiques** :
|
|
- Point-in-time recovery (PITR) sur 7 jours (plan Free) ou 30 jours (plan Pro)
|
|
- Pas de CronJob à gérer
|
|
|
|
Pour créer un backup manuel :
|
|
```bash
|
|
# Installer la CLI Neon
|
|
npm install -g neonctl
|
|
neonctl auth
|
|
|
|
# Créer un point de restauration (branch)
|
|
neonctl branches create \
|
|
--project-id <project_id> \
|
|
--name "backup-$(date +%Y%m%d)" \
|
|
--parent main
|
|
```
|
|
|
|
---
|
|
|
|
## Backup PostgreSQL — Option B (self-hosted)
|
|
|
|
### CronJob Kubernetes de backup
|
|
|
|
```yaml
|
|
# k8s/backup-postgres-cronjob.yaml
|
|
---
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: backup-credentials
|
|
namespace: xpeditis-prod
|
|
type: Opaque
|
|
stringData:
|
|
# Même credentials que le backend pour Object Storage
|
|
AWS_ACCESS_KEY_ID: "<HETZNER_ACCESS_KEY>"
|
|
AWS_SECRET_ACCESS_KEY: "<HETZNER_SECRET_KEY>"
|
|
AWS_S3_ENDPOINT: "https://fsn1.your-objectstorage.com"
|
|
AWS_S3_BUCKET: "xpeditis-prod"
|
|
# Credentials PostgreSQL
|
|
PGPASSWORD: "<PG_PASSWORD>"
|
|
---
|
|
apiVersion: batch/v1
|
|
kind: CronJob
|
|
metadata:
|
|
name: postgres-backup
|
|
namespace: xpeditis-prod
|
|
spec:
|
|
schedule: "0 3 * * *" # 3h00 chaque nuit
|
|
concurrencyPolicy: Forbid
|
|
successfulJobsHistoryLimit: 3
|
|
failedJobsHistoryLimit: 5
|
|
jobTemplate:
|
|
spec:
|
|
template:
|
|
spec:
|
|
restartPolicy: OnFailure
|
|
containers:
|
|
- name: backup
|
|
image: postgres:15-alpine
|
|
command:
|
|
- /bin/sh
|
|
- -c
|
|
- |
|
|
set -e
|
|
echo "=== Démarrage backup PostgreSQL $(date) ==="
|
|
|
|
# Variables
|
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
BACKUP_FILE="/tmp/xpeditis_${TIMESTAMP}.sql.gz"
|
|
S3_KEY="backups/postgres/$(date +%Y/%m)/xpeditis_${TIMESTAMP}.sql.gz"
|
|
|
|
# Dump PostgreSQL compressé
|
|
pg_dump \
|
|
-h ${PGHOST} \
|
|
-p ${PGPORT:-5432} \
|
|
-U ${PGUSER} \
|
|
-d ${PGDATABASE} \
|
|
--no-password \
|
|
--clean \
|
|
--if-exists \
|
|
--format=custom \
|
|
| gzip > ${BACKUP_FILE}
|
|
|
|
BACKUP_SIZE=$(du -sh ${BACKUP_FILE} | cut -f1)
|
|
echo "Dump créé: ${BACKUP_FILE} (${BACKUP_SIZE})"
|
|
|
|
# Upload vers Hetzner Object Storage
|
|
apk add --no-cache aws-cli 2>/dev/null || pip install awscli 2>/dev/null
|
|
|
|
AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
|
|
AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
|
|
aws s3 cp ${BACKUP_FILE} s3://${AWS_S3_BUCKET}/${S3_KEY} \
|
|
--endpoint-url ${AWS_S3_ENDPOINT}
|
|
|
|
echo "✅ Backup uploadé: s3://${AWS_S3_BUCKET}/${S3_KEY}"
|
|
|
|
# Nettoyage local
|
|
rm ${BACKUP_FILE}
|
|
|
|
# Vérifier les anciens backups (garder 30 jours)
|
|
echo "Backups existants:"
|
|
AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
|
|
AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
|
|
aws s3 ls s3://${AWS_S3_BUCKET}/backups/postgres/ \
|
|
--endpoint-url ${AWS_S3_ENDPOINT} \
|
|
--recursive | tail -10
|
|
|
|
echo "=== Backup terminé $(date) ==="
|
|
env:
|
|
- name: PGHOST
|
|
value: "10.0.1.100" # IP privée serveur PostgreSQL
|
|
- name: PGPORT
|
|
value: "5432"
|
|
- name: PGUSER
|
|
value: "xpeditis"
|
|
- name: PGDATABASE
|
|
value: "xpeditis_prod"
|
|
envFrom:
|
|
- secretRef:
|
|
name: backup-credentials
|
|
resources:
|
|
requests:
|
|
cpu: 100m
|
|
memory: 256Mi
|
|
limits:
|
|
cpu: 500m
|
|
memory: 512Mi
|
|
```
|
|
|
|
```bash
|
|
# Appliquer
|
|
kubectl apply -f k8s/backup-postgres-cronjob.yaml
|
|
|
|
# Tester manuellement (créer un Job depuis le CronJob)
|
|
kubectl create job --from=cronjob/postgres-backup test-backup -n xpeditis-prod
|
|
kubectl logs -l job-name=test-backup -n xpeditis-prod -f
|
|
|
|
# Vérifier que le fichier est arrivé dans S3
|
|
aws s3 ls s3://xpeditis-prod/backups/postgres/ \
|
|
--profile hetzner \
|
|
--endpoint-url https://fsn1.your-objectstorage.com \
|
|
--recursive
|
|
```
|
|
|
|
---
|
|
|
|
## Procédure de restauration PostgreSQL
|
|
|
|
### Restauration complète (catastrophe totale)
|
|
|
|
```bash
|
|
# Étape 1 : Lister les backups disponibles
|
|
aws s3 ls s3://xpeditis-prod/backups/postgres/ \
|
|
--profile hetzner \
|
|
--endpoint-url https://fsn1.your-objectstorage.com \
|
|
--recursive | sort -r | head -10
|
|
|
|
# Étape 2 : Télécharger le backup le plus récent
|
|
aws s3 cp \
|
|
s3://xpeditis-prod/backups/postgres/2026/03/xpeditis_20260323_030001.sql.gz \
|
|
/tmp/restore.sql.gz \
|
|
--profile hetzner \
|
|
--endpoint-url https://fsn1.your-objectstorage.com
|
|
|
|
# Étape 3 : Décompresser et restaurer
|
|
# ⚠️ Cette commande EFFACE les données existantes
|
|
gunzip -c /tmp/restore.sql.gz | pg_restore \
|
|
-h <POSTGRES_HOST> \
|
|
-U xpeditis \
|
|
-d xpeditis_prod \
|
|
--clean \
|
|
--if-exists \
|
|
--no-privileges \
|
|
--no-owner
|
|
|
|
# Étape 4 : Vérifier l'intégrité
|
|
psql -h <POSTGRES_HOST> -U xpeditis -d xpeditis_prod \
|
|
-c "SELECT COUNT(*) as bookings FROM bookings;"
|
|
|
|
psql -h <POSTGRES_HOST> -U xpeditis -d xpeditis_prod \
|
|
-c "SELECT COUNT(*) as users FROM users;"
|
|
|
|
# Étape 5 : Redémarrer les pods pour reconnecter
|
|
kubectl rollout restart deployment/xpeditis-backend -n xpeditis-prod
|
|
```
|
|
|
|
---
|
|
|
|
## Backup des Secrets Kubernetes
|
|
|
|
Les secrets ne sont pas dans Git (intentionnel). Sauvegardez-les chiffrés.
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# scripts/backup-secrets.sh
|
|
|
|
set -e
|
|
BACKUP_DIR="$HOME/.xpeditis-secrets-backup"
|
|
mkdir -p "$BACKUP_DIR"
|
|
DATE=$(date +%Y%m%d_%H%M%S)
|
|
|
|
# Exporter les secrets (encodés base64)
|
|
kubectl get secret backend-secrets -n xpeditis-prod -o yaml > /tmp/backend-secrets-${DATE}.yaml
|
|
kubectl get secret frontend-secrets -n xpeditis-prod -o yaml > /tmp/frontend-secrets-${DATE}.yaml
|
|
kubectl get secret ghcr-credentials -n xpeditis-prod -o yaml > /tmp/ghcr-creds-${DATE}.yaml
|
|
|
|
# Chiffrer avec GPG (ou utiliser un password)
|
|
tar czf - /tmp/*-${DATE}.yaml | gpg --symmetric --cipher-algo AES256 \
|
|
> "${BACKUP_DIR}/k8s-secrets-${DATE}.tar.gz.gpg"
|
|
|
|
# Nettoyage des fichiers temporaires
|
|
rm /tmp/*-${DATE}.yaml
|
|
|
|
echo "✅ Secrets sauvegardés dans ${BACKUP_DIR}/k8s-secrets-${DATE}.tar.gz.gpg"
|
|
|
|
# Lister les backups existants
|
|
ls -la "$BACKUP_DIR"/
|
|
```
|
|
|
|
```bash
|
|
# Restaurer les secrets depuis un backup
|
|
gpg --decrypt "${BACKUP_DIR}/k8s-secrets-20260323_120000.tar.gz.gpg" | tar xzf -
|
|
kubectl apply -f /tmp/backend-secrets-20260323_120000.yaml
|
|
```
|
|
|
|
---
|
|
|
|
## Runbook — Reprise après sinistre complète
|
|
|
|
Procédure si vous perdez tout le cluster (serveurs détruits) :
|
|
|
|
### Étape 1 : Recréer l'infrastructure (30 min)
|
|
|
|
```bash
|
|
# 1. Recréer le réseau
|
|
hcloud network create --name xpeditis-network --ip-range 10.0.0.0/16
|
|
hcloud network add-subnet xpeditis-network --type cloud --network-zone eu-central --ip-range 10.0.1.0/24
|
|
|
|
# 2. Recréer le firewall
|
|
# (répéter les commandes du doc 03)
|
|
|
|
# 3. Recréer le cluster k3s
|
|
hetzner-k3s create --config ~/.xpeditis/cluster.yaml
|
|
|
|
# 4. Configurer kubectl
|
|
export KUBECONFIG=~/.kube/kubeconfig-xpeditis-prod
|
|
```
|
|
|
|
### Étape 2 : Restaurer les secrets (15 min)
|
|
|
|
```bash
|
|
# Créer le namespace
|
|
kubectl apply -f k8s/00-namespaces.yaml
|
|
|
|
# Restaurer les secrets depuis le backup chiffré
|
|
gpg --decrypt "$HOME/.xpeditis-secrets-backup/k8s-secrets-XXXXXXXX.tar.gz.gpg" | tar xzf -
|
|
kubectl apply -f /tmp/backend-secrets-*.yaml
|
|
kubectl apply -f /tmp/frontend-secrets-*.yaml
|
|
kubectl apply -f /tmp/ghcr-creds-*.yaml
|
|
|
|
# Recréer le secret GHCR
|
|
kubectl create secret docker-registry ghcr-credentials \
|
|
--namespace xpeditis-prod \
|
|
--docker-server=ghcr.io \
|
|
--docker-username=<GITHUB_USERNAME> \
|
|
--docker-password=<GITHUB_PAT>
|
|
```
|
|
|
|
### Étape 3 : Restaurer les services (15 min)
|
|
|
|
```bash
|
|
# Installer cert-manager
|
|
helm install cert-manager jetstack/cert-manager \
|
|
--namespace cert-manager --create-namespace \
|
|
--version v1.15.3 --set installCRDs=true
|
|
kubectl apply -f /tmp/cluster-issuers.yaml
|
|
|
|
# Déployer l'application
|
|
kubectl apply -f k8s/
|
|
|
|
# Attendre
|
|
kubectl rollout status deployment/xpeditis-backend -n xpeditis-prod --timeout=300s
|
|
```
|
|
|
|
### Étape 4 : Restaurer la base de données (30 min)
|
|
|
|
```bash
|
|
# Si PostgreSQL self-hosted :
|
|
# (Recréer le serveur PostgreSQL si nécessaire, doc 07)
|
|
# Puis restaurer depuis le backup S3
|
|
|
|
# Télécharger le backup le plus récent
|
|
LATEST=$(aws s3 ls s3://xpeditis-prod/backups/postgres/ \
|
|
--profile hetzner \
|
|
--endpoint-url https://fsn1.your-objectstorage.com \
|
|
--recursive | sort -r | head -1 | awk '{print $4}')
|
|
|
|
aws s3 cp s3://xpeditis-prod/$LATEST /tmp/restore.sql.gz \
|
|
--profile hetzner \
|
|
--endpoint-url https://fsn1.your-objectstorage.com
|
|
|
|
# Restaurer
|
|
gunzip -c /tmp/restore.sql.gz | pg_restore \
|
|
-h <POSTGRES_HOST> -U xpeditis -d xpeditis_prod \
|
|
--clean --if-exists --no-privileges --no-owner
|
|
```
|
|
|
|
### Étape 5 : Vérification finale (15 min)
|
|
|
|
```bash
|
|
# Health checks
|
|
curl https://api.xpeditis.com/api/v1/health
|
|
curl https://app.xpeditis.com/
|
|
|
|
# Test login
|
|
curl -X POST https://api.xpeditis.com/api/v1/auth/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"email":"admin@test.com","password":"test"}' | jq .
|
|
|
|
# Vérifier les données
|
|
kubectl exec -it deployment/xpeditis-backend -n xpeditis-prod -- \
|
|
node -e "console.log('Database OK')"
|
|
|
|
echo "✅ Système opérationnel. RTO: $(date)"
|
|
```
|
|
|
|
---
|
|
|
|
## Test régulier des backups (mensuel)
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# scripts/test-backup-restore.sh
|
|
# À exécuter en environnement de test, JAMAIS en production
|
|
|
|
echo "🧪 Test de restauration du backup PostgreSQL"
|
|
|
|
# 1. Créer une DB de test
|
|
psql -h <TEST_HOST> -U postgres -c "CREATE DATABASE xpeditis_restore_test;"
|
|
|
|
# 2. Télécharger le dernier backup
|
|
LATEST=$(aws s3 ls s3://xpeditis-prod/backups/postgres/ \
|
|
--profile hetzner \
|
|
--endpoint-url https://fsn1.your-objectstorage.com \
|
|
--recursive | sort -r | head -1 | awk '{print $4}')
|
|
|
|
aws s3 cp s3://xpeditis-prod/$LATEST /tmp/test-restore.sql.gz \
|
|
--profile hetzner \
|
|
--endpoint-url https://fsn1.your-objectstorage.com
|
|
|
|
# 3. Restaurer dans la DB de test
|
|
gunzip -c /tmp/test-restore.sql.gz | pg_restore \
|
|
-h <TEST_HOST> -U postgres -d xpeditis_restore_test
|
|
|
|
# 4. Vérifier
|
|
BOOKING_COUNT=$(psql -h <TEST_HOST> -U postgres -d xpeditis_restore_test \
|
|
-t -c "SELECT COUNT(*) FROM bookings;" | xargs)
|
|
|
|
echo "✅ Restauration réussie. Nombre de bookings: $BOOKING_COUNT"
|
|
|
|
# 5. Nettoyage
|
|
psql -h <TEST_HOST> -U postgres -c "DROP DATABASE xpeditis_restore_test;"
|
|
rm /tmp/test-restore.sql.gz
|
|
|
|
echo "✅ Test de backup/restore réussi le $(date)"
|
|
```
|