# 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 \ --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: "" AWS_SECRET_ACCESS_KEY: "" AWS_S3_ENDPOINT: "https://fsn1.your-objectstorage.com" AWS_S3_BUCKET: "xpeditis-prod" # Credentials PostgreSQL PGPASSWORD: "" --- 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 \ -U xpeditis \ -d xpeditis_prod \ --clean \ --if-exists \ --no-privileges \ --no-owner # Étape 4 : Vérifier l'intégrité psql -h -U xpeditis -d xpeditis_prod \ -c "SELECT COUNT(*) as bookings FROM bookings;" psql -h -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= \ --docker-password= ``` ### É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 -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 -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 -U postgres -d xpeditis_restore_test # 4. Vérifier BOOKING_COUNT=$(psql -h -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 -U postgres -c "DROP DATABASE xpeditis_restore_test;" rm /tmp/test-restore.sql.gz echo "✅ Test de backup/restore réussi le $(date)" ```