# 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@:/tmp/ ssh -i ~/.ssh/xpeditis_prod deploy@ \ '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@ '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@:/tmp/data-node/ ssh -i ~/.ssh/xpeditis_prod deploy@ ' 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@ \ '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@:/tmp/ ssh -i ~/.ssh/xpeditis_prod deploy@ ' 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@:/tmp/ ssh -i ~/.ssh/xpeditis_prod deploy@ ' 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@:/tmp/sb.key ssh -i ~/.ssh/xpeditis_prod deploy@ ' 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@ 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@ sudo apt-get install -y postgresql-client # Doit RÉUSSIR PGPASSWORD='' 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='' 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 '' --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 ``` 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@ 'sudo systemctl start xpeditis-backup.service' ssh deploy@ '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@ \ '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@ "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)**