xpeditis2.0/docs/deployment/hetzner/13-backup-disaster-recovery.md
2026-03-26 18:08:28 +01:00

11 KiB

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 :

# 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

# 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
# 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)

# É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.

#!/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"/
# 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)

# 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)

# 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)

# 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)

# 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)

# 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)

#!/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)"