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

338 lines
11 KiB
Markdown

# 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](./07-stockage-objet-s3.md). 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
```bash
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
```bash
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
```bash
# 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.
```bash
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) :
```bash
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 :
```bash
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
```bash
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](./07-stockage-objet-s3.md)), `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
```bash
# 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
```bash
# 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
```bash
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.
```bash
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
```bash
# 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
```bash
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
```bash
# 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 :**
```bash
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.
```bash
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 :
```bash
# 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
```bash
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](./05-cluster-k3s.md)**