xpeditis2.0/docs/mise-en-prod/04-noeud-donnees.md
David b22f4e0b74 docs: procedure de mise en production pas a pas
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
2026-09-07 21:40:50 +02:00

11 KiB

04 — Nœud de données (PostgreSQL + Redis)

Durée : environ 2 h. C'est l'étape la plus délicate : c'est la seule dont les erreurs ne se rattrapent pas en redéployant.

Prérequis — WAL-G archive les journaux de transaction dès le premier démarrage de PostgreSQL. Créez d'abord le bucket de sauvegarde et ses clés : 07 — Stockage objet, sections 1 et 2. Revenez ensuite ici. Sans le bucket, PostgreSQL démarre quand même mais accumule ses WAL sur disque jusqu'à saturation.


1. Choix d'architecture, et pourquoi

PostgreSQL est hors de Kubernetes. Ce n'est pas un raccourci, c'est un choix :

  • Un StatefulSet PostgreSQL impose des volumes persistants, un ordre de démarrage, et transforme chaque montée de version majeure en opération à risque.
  • Hors cluster, pg_dump, WAL-G, la restauration à un instant T et les tests de restauration sont des commandes ordinaires.
  • Le nœud applicatif redevient entièrement jetable : on peut le détruire et le reconstruire sans jamais approcher les données.

Le trafic vers la base est chiffré. Le réseau privé Hetzner isole les projets mais ne chiffre pas les paquets. pg_hba.conf n'accepte que des lignes hostssl : une connexion en clair est refusée, y compris depuis app-01.


2. Installation

scp -i ~/.ssh/xpeditis_prod infra/prod/scripts/01-setup-data-node.sh \
    deploy@<db_public_ipv4>:/tmp/

ssh -i ~/.ssh/xpeditis_prod deploy@<db_public_ipv4> \
    'sudo bash /tmp/01-setup-data-node.sh'

Le script :

  1. formate (si vierge) et monte le volume Hetzner sur /var/lib/xpeditis/pgdata, avec nofail pour que le serveur démarre même si le volume manque ;
  2. installe Docker Engine depuis le dépôt officiel et le durcit (no-new-privileges, icc: false, rotation des journaux) ;
  3. génère le certificat TLS interne de PostgreSQL ;
  4. installe age ;
  5. installe les timers systemd de sauvegarde.

Il ne démarre pas la base : les secrets ne sont pas encore là.

Vérifier le volume

ssh deploy@<db_public_ipv4> 'df -h /var/lib/xpeditis/pgdata; lsblk'

Le point de montage doit apparaître avec la taille du volume (50 Go), pas celle du disque système. Si le volume n'a pas été détecté, /var/lib/xpeditis/pgdata est resté sur le disque système : corrigez avant d'écrire la moindre donnée.


3. Copier la configuration

# Depuis le dépôt, sur votre poste
rsync -az -e "ssh -i ~/.ssh/xpeditis_prod" \
  infra/prod/data-node/ \
  deploy@<db_public_ipv4>:/tmp/data-node/

ssh -i ~/.ssh/xpeditis_prod deploy@<db_public_ipv4> '
  sudo mkdir -p /opt/xpeditis/data-node
  sudo rsync -a /tmp/data-node/ /opt/xpeditis/data-node/
  sudo chown -R root:root /opt/xpeditis/data-node
  sudo chmod +x /opt/xpeditis/data-node/backup/*.sh
  rm -rf /tmp/data-node
'

Relancez ensuite le script d'installation. Il est idempotent : cette seconde passe ne refait rien de ce qui est déjà en place, mais elle trouve désormais les fichiers backup/ et installe les timers systemd.

ssh -i ~/.ssh/xpeditis_prod deploy@<db_public_ipv4> \
    'sudo bash /tmp/01-setup-data-node.sh'

4. Secrets

4.1 Générer les mots de passe

Sur votre poste, jamais sur le serveur (l'historique du shell garde tout) :

echo "POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d '\n/+=' | head -c 40)"
echo "REDIS_PASSWORD=$(openssl rand -base64 32 | tr -d '\n/+=' | head -c 40)"
# WAL-G attend 32 octets en HEXADÉCIMAL (64 caractères), pas en base64.
echo "WALG_LIBSODIUM_KEY=$(openssl rand -hex 32)"

Générez aussi la paire age dédiée au chiffrement des dumps :

age-keygen -o /tmp/backup-age.key
grep 'public key' /tmp/backup-age.key       # → BACKUP_AGE_RECIPIENT

WALG_LIBSODIUM_KEY et la clé privée backup-age.key sont les deux clés sans lesquelles aucune restauration n'est possible. Sauvegardez-les dans votre gestionnaire de mots de passe et hors ligne, avant d'aller plus loin. Une sauvegarde chiffrée dont on a perdu la clé est un fichier inutile.

4.2 Composer le fichier d'environnement

cp infra/prod/env/data-node.env.example /tmp/.env.data
$EDITOR /tmp/.env.data

Renseignez : POSTGRES_PASSWORD, REDIS_PASSWORD, les identifiants WAL-G (bucket créé en 07), WALG_LIBSODIUM_KEY, BACKUP_AGE_RECIPIENT, les coordonnées de la Storage Box, les webhooks.

4.3 Chiffrer pour Git, déposer en clair sur le serveur

# Version chiffrée, versionnée
cd infra/prod
sops -e --input-type dotenv --output-type dotenv /tmp/.env.data \
  > data-node/data-node.sops.env

# Version en clair, uniquement sur db-01
scp -i ~/.ssh/xpeditis_prod /tmp/.env.data deploy@<db_public_ipv4>:/tmp/
ssh -i ~/.ssh/xpeditis_prod deploy@<db_public_ipv4> '
  sudo install -m 600 -o root -g root /tmp/.env.data /opt/xpeditis/data-node/.env.data
  shred -u /tmp/.env.data
'

# Effacer la copie locale
shred -u /tmp/.env.data

4.4 Clés privées sur db-01

# Clé age de déchiffrement des dumps
scp -i ~/.ssh/xpeditis_prod /tmp/backup-age.key deploy@<db_public_ipv4>:/tmp/
ssh -i ~/.ssh/xpeditis_prod deploy@<db_public_ipv4> '
  sudo mkdir -p /root/.config/xpeditis && sudo chmod 700 /root/.config/xpeditis
  sudo install -m 600 -o root -g root /tmp/backup-age.key /root/.config/xpeditis/backup-age.key
  shred -u /tmp/backup-age.key
'
shred -u /tmp/backup-age.key

# Clé SSH vers la Storage Box
scp -i ~/.ssh/xpeditis_prod ~/.ssh/xpeditis_storagebox deploy@<db_public_ipv4>:/tmp/sb.key
ssh -i ~/.ssh/xpeditis_prod deploy@<db_public_ipv4> '
  sudo install -m 600 -o root -g root /tmp/sb.key /root/.ssh/storagebox
  shred -u /tmp/sb.key
  # Enregistre l empreinte de la Storage Box : StrictHostKeyChecking=yes
  # échouerait sinon, et le rsync de sauvegarde avec lui.
  sudo ssh-keyscan -p 23 uXXXXXX.your-storagebox.de | sudo tee -a /root/.ssh/known_hosts
'

5. Démarrage

ssh -i ~/.ssh/xpeditis_prod deploy@<db_public_ipv4>
cd /opt/xpeditis/data-node
sudo docker compose -f docker-compose.data.yml --env-file .env.data up -d --build

La construction de l'image PostgreSQL + WAL-G prend deux à trois minutes.

sudo docker compose -f docker-compose.data.yml --env-file .env.data ps
sudo docker compose -f docker-compose.data.yml --env-file .env.data logs postgres | tail -40

Attendu dans les journaux :

database system is ready to accept connections

Si PostgreSQL refuse de démarrer, la cause est presque toujours l'une de ces trois :

Message Cause Correction
could not load server certificate file droits du certificat chown 999:999 et chmod 600 sur /var/lib/xpeditis/certs/server.key
unrecognized configuration parameter faute de frappe dans postgresql.conf corriger, puis docker compose restart postgres
data directory has wrong ownership volume monté après le premier démarrage chown -R 999:999 /var/lib/xpeditis/pgdata

6. Vérifications

6.1 TLS obligatoire

# Depuis db-01 : connexion locale (socket) — doit fonctionner
sudo docker compose exec -u postgres postgres psql -c 'SELECT version();'

# Le chiffrement est-il actif ?
sudo docker compose exec -u postgres postgres \
  psql -c "SELECT name, setting FROM pg_settings WHERE name IN ('ssl','password_encryption');"

Attendu : ssl = on, password_encryption = scram-sha-256.

6.2 Depuis app-01, la connexion doit passer en TLS et uniquement en TLS

ssh deploy@<app_public_ipv4>
sudo apt-get install -y postgresql-client

# Doit RÉUSSIR
PGPASSWORD='<POSTGRES_PASSWORD>' psql "host=10.10.1.20 port=5432 dbname=xpeditis_prod user=xpeditis sslmode=require" -c '\conninfo'

# Doit ÉCHOUER — c'est le résultat attendu
PGPASSWORD='<POSTGRES_PASSWORD>' psql "host=10.10.1.20 port=5432 dbname=xpeditis_prod user=xpeditis sslmode=disable" -c 'SELECT 1;'

Le second doit renvoyer :

FATAL: no pg_hba.conf entry for host "10.10.1.10", ... SSL off

Si la connexion sans TLS réussit, pg_hba.conf n'a pas été pris en compte : vérifiez que le conteneur est bien lancé avec -c hba_file=/etc/postgresql/pg_hba.conf.

6.3 Redis

# Depuis app-01
redis-cli -h 10.10.1.20 -a '<REDIS_PASSWORD>' --no-auth-warning ping     # PONG
redis-cli -h 10.10.1.20 --no-auth-warning ping                            # NOAUTH

6.4 Rien n'est exposé publiquement

Depuis un autre réseau :

nmap -Pn -p 5432,6379,9187 <db_public_ipv4>

Les trois doivent être filtered.


7. Première sauvegarde

Elle doit être prise avant que la moindre donnée réelle n'existe : cela valide la chaîne complète pendant que l'enjeu est nul.

ssh deploy@<db_public_ipv4> 'sudo systemctl start xpeditis-backup.service'
ssh deploy@<db_public_ipv4> 'sudo journalctl -u xpeditis-backup -n 60 --no-pager'

Attendu :

WAL-G basebackup : termine
pg_dump : NNNNN octets
Envoi vers la Storage Box ...
Sauvegarde terminee.

Vérifiez les deux destinations :

# WAL-G sur Object Storage
ssh deploy@<db_public_ipv4> \
  'cd /opt/xpeditis/data-node && sudo docker compose exec -T -u postgres postgres wal-g backup-list --detail'

# Dumps sur la Storage Box
ssh -p 23 -i ~/.ssh/xpeditis_storagebox uXXXXXX@uXXXXXX.your-storagebox.de \
  'ls -lh xpeditis-prod/dumps/'

Timers actifs

ssh deploy@<db_public_ipv4> "systemctl list-timers 'xpeditis-*' --no-pager"

Attendu : xpeditis-backup.timer (quotidien 02h30) et xpeditis-backup-verify.timer (dimanche 04h00).


8. Réglages à connaître

Paramètre Valeur Conséquence
shared_buffers 2 Go Sur 8 Go de RAM, dont 1,5 Go plafonnés pour Redis.
max_connections 150 2 replicas backend + marge d'exploitation. Une alerte se déclenche à 80 %.
statement_timeout 5 min Borne les requêtes folles tout en laissant passer les imports de grilles CSV.
archive_timeout 5 min Plafonne la perte de données à 5 minutes en cas de perte totale du serveur.
log_min_duration_statement 500 ms Toute requête lente est tracée dans Loki.
log_statement ddl Chaque changement de schéma (migration) laisse une trace.
Redis maxmemory-policy volatile-lru Seules les clés à TTL sont évincées. Voir l'avertissement ci-dessous.

Redis et l'éviction. volatile-lru n'évince que les clés porteuses d'un TTL — les cotations tarifaires, qui sont sacrifiables. Mais les entrées de révocation de jetons ont elles aussi un TTL : sous saturation mémoire, l'une d'elles pourrait être évincée, redonnant validité à un jeton révoqué. maxmemory est fixé à 1 Go pour 0,5 Go estimé, et une alerte se déclenche à 75 % d'occupation. Si elle se déclenche, augmentez maxmemory — ne changez pas la politique d'éviction.


9. Contrôle

[ ] Volume Hetzner monté sur /var/lib/xpeditis/pgdata (bonne taille)
[ ] PostgreSQL démarré, ssl = on, password_encryption = scram-sha-256
[ ] Connexion TLS depuis app-01 : OK
[ ] Connexion NON TLS depuis app-01 : REFUSÉE
[ ] Redis répond avec mot de passe, refuse sans
[ ] 5432 / 6379 / 9187 filtered depuis Internet
[ ] Première sauvegarde réussie, visible sur Object Storage ET Storage Box
[ ] Timers xpeditis-backup et xpeditis-backup-verify actifs
[ ] WALG_LIBSODIUM_KEY et backup-age.key sauvegardées hors ligne
[ ] .env.data en clair effacé du poste et de /tmp du serveur

→ Suite : 05 — Cluster k3s