Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
338 lines
11 KiB
Markdown
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)**
|