Éclater l'application sur trois machines
Ce que vous allez construire
Ce que vous allez construire : l'application éclatée sur trois machines, chaque flux réduit à un seul port autorisé, et les données migrées depuis la VM du bloc 1.
PDF vectorielChaque VM garde sa carte NAT (Internet pour APT) ; tout le trafic applicatif passe par le réseau privé.
Étape 0 : le réseau host-only, côté hôte (15 min)
Dans VirtualBox : Fichier → Outils → Gestionnaire de réseau (Network Manager), onglet « Réseaux hôte uniquement » :
- Si
vboxnet0n'existe pas, cliquez Créer. - Vérifiez l'adressage : IPv4
192.168.56.1/24(l'hôte est donc.1, comme au plan du chapitre 7). - Onglet « Serveur DHCP » : décochez « Activer le serveur ». Des serveurs ont des adresses statiques (ch. 7, §3.2) ; un DHCP concurrent produirait des conflits.
Équivalent en ligne de commande, et vérification :
VBoxManage list hostonlyifs # vboxnet0, IPAddress 192.168.56.1
VBoxManage list dhcpservers # le serveur de vboxnet0 : Enabled: No
# Pour le désactiver si besoin :
VBoxManage dhcpserver modify --interface vboxnet0 --disable
Étape 1 : la VM de base (45 min)
Installer Ubuntu quatre fois serait de la friction sans valeur ; la friction intéressante du bloc est ailleurs. Nous créons donc une VM de base soignée, puis nous la clonerons. (Notez l'idée au passage : préparer une machine-modèle puis l'instancier, c'est déjà le concept d'image, central au S2.)
Créez listify-base comme au TP 1, avec ces différences :
| Paramètre | Valeur | Pourquoi |
|---|---|---|
| Nom | listify-base | |
| RAM / CPU | 1024 Mo / 1 vCPU | Chaque machine ne porte plus qu'un service |
| Disque | 15 Go dynamique | |
| Réseau, carte 1 | NAT | Internet (APT) |
| Réseau, carte 2 | Réseau privé hôte (host-only), vboxnet0 | Le réseau du chapitre 7 |
Installez Ubuntu Server 24.04 (procédure du TP 1, étape 2), hostname listify-base, utilisateur deploy, OpenSSH coché. Puis configurez la base par SSH : ajoutez temporairement la redirection NAT ssh (TCP, 127.0.0.1, 2222 → 22, comme au TP 1) et déroulez :
# Depuis le poste hôte : déposer votre clé (options anti multi-clés du TP 1)
ssh-copy-id -i ~/.ssh/id_ed25519.pub \
-o PubkeyAuthentication=no -o PreferredAuthentications=password \
# Sur la VM : le socle du TP 1, en accéléré (votre runbook du bloc 1 sert enfin !)
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowUsers deploy
EOF
sudo sshd -t && sudo systemctl reload ssh
sudo ufw default deny incoming && sudo ufw default allow outgoing
sudo ufw allow 22/tcp && sudo ufw enable
sudo apt update && sudo apt upgrade -y
sudo timedatectl set-timezone Europe/Paris
# La résolution interne (ch. 7, §5) : le même bloc sur TOUTES les machines
sudo tee -a /etc/hosts > /dev/null <<'EOF'
192.168.56.10 listify-lb
192.168.56.21 listify-app1
192.168.56.22 listify-app2
192.168.56.31 listify-db
EOF
Éteignez (sudo poweroff), puis :
- Supprimez la redirection de port 2222 de
listify-base. Impératif : les clones héritent des réglages de la VM, et trois clones revendiquant le port hôte 2222 entreraient en conflit au premier démarrage simultané. - Prenez un snapshot
base-prete.
Étape 2 : cloner et individualiser (45 min)
2.1 Les trois clones
Pour chaque nom (listify-lb, listify-app1, listify-db) : clic droit sur listify-base → Cloner : nom du clone, Clone intégral (full clone), et dans « Options » : « Générer de nouvelles adresses MAC pour toutes les cartes réseau ». Sans cette option, trois cartes porteraient la même MAC sur le même réseau : le genre de panne réseau qu'on met des heures à soupçonner.
2.2 Individualiser chaque clone
Un clone est une copie parfaite, y compris de tout ce qui devrait être unique. Démarrez chaque clone et, dans la console VirtualBox (pas de SSH possible : la carte host-only n'a pas encore d'adresse), déroulez, en remplaçant nom et adresse (listify-lb = .10, listify-app1 = .21, listify-db = .31) :
Le bloc est le même sur les trois machines, aux deux valeurs d'identité près (le nom et l'adresse) ; l'étape 3, elle, est rigoureusement identique partout. Chaque onglet ci-dessous est prêt à coller sur la machine correspondante :
- listify-lb (.10)
# 1. L'identité : hostname + l'entrée locale 127.0.1.1 d'Ubuntu
sudo hostnamectl set-hostname listify-lb
sudo sed -i 's/^127\.0\.1\.1.*/127.0.1.1 listify-lb/' /etc/hosts
# 2. L'adresse privée : netplan (ch. 7, §4.2)
sudo tee /etc/netplan/60-hostonly.yaml > /dev/null <<'EOF'
network:
version: 2
ethernets:
enp0s8:
addresses:
- 192.168.56.10/24
EOF
sudo chmod 600 /etc/netplan/60-hostonly.yaml
sudo netplan apply # en console, pas besoin de netplan try
ip -brief addr # enp0s8 doit porter l'adresse
# 3. Les clés d'hôte SSH : chaque serveur doit avoir les SIENNES
sudo rm /etc/ssh/ssh_host_*
sudo dpkg-reconfigure openssh-server
sudo systemctl restart ssh
- listify-app1 (.21)
# 1. L'identité : hostname + l'entrée locale 127.0.1.1 d'Ubuntu
sudo hostnamectl set-hostname listify-app1
sudo sed -i 's/^127\.0\.1\.1.*/127.0.1.1 listify-app1/' /etc/hosts
# 2. L'adresse privée : netplan (ch. 7, §4.2)
sudo tee /etc/netplan/60-hostonly.yaml > /dev/null <<'EOF'
network:
version: 2
ethernets:
enp0s8:
addresses:
- 192.168.56.21/24
EOF
sudo chmod 600 /etc/netplan/60-hostonly.yaml
sudo netplan apply # en console, pas besoin de netplan try
ip -brief addr # enp0s8 doit porter l'adresse
# 3. Les clés d'hôte SSH : chaque serveur doit avoir les SIENNES
sudo rm /etc/ssh/ssh_host_*
sudo dpkg-reconfigure openssh-server
sudo systemctl restart ssh
- listify-db (.31)
# 1. L'identité : hostname + l'entrée locale 127.0.1.1 d'Ubuntu
sudo hostnamectl set-hostname listify-db
sudo sed -i 's/^127\.0\.1\.1.*/127.0.1.1 listify-db/' /etc/hosts
# 2. L'adresse privée : netplan (ch. 7, §4.2)
sudo tee /etc/netplan/60-hostonly.yaml > /dev/null <<'EOF'
network:
version: 2
ethernets:
enp0s8:
addresses:
- 192.168.56.31/24
EOF
sudo chmod 600 /etc/netplan/60-hostonly.yaml
sudo netplan apply # en console, pas besoin de netplan try
ip -brief addr # enp0s8 doit porter l'adresse
# 3. Les clés d'hôte SSH : chaque serveur doit avoir les SIENNES
sudo rm /etc/ssh/ssh_host_*
sudo dpkg-reconfigure openssh-server
sudo systemctl restart ssh
Trois fois le même geste, avec deux valeurs qui changent : gardez cette sensation, c'est exactement ce qu'une boucle sur un inventaire Ansible supprimera au bloc 3.
2.3 Côté hôte : l'accès direct
Complétez ~/.ssh/config (fini les ports 2222 !) :
Host listify-lb
HostName 192.168.56.10
Host listify-app1
HostName 192.168.56.21
Host listify-app2
HostName 192.168.56.22
Host listify-db
HostName 192.168.56.31
Host listify-lb listify-app1 listify-app2 listify-db
User deploy
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
Point de contrôle n° 1 : le réseau est câblé
- Depuis l'hôte :
ssh listify-db 'hostname'répondlistify-db(idem pour les trois). Première connexion : l'empreinte présentée est différente pour chaque machine, preuve que la régénération des clés a fonctionné. - Entre VM :
ssh listify-app1 'ping -c2 listify-db'fonctionne par le nom (le /etc/hosts de la base fait effet). ssh listify-app1 'ip -brief addr': l'adresse NAT (10.0.2.15) et la privée (.21) : deux cartes, deux rôles.
Étape 3 : la base de données déménage (1 h)
3.1 Le dernier service de listify-s1
Démarrez listify-s1 (bloc 1) une dernière fois, produisez un dump frais (celui du TP 4 date d'avant vos dernières tâches), rapatriez-le, éteignez :
ssh -t listify-s1 'sudo -u postgres pg_dump -Fc -f /tmp/listify-final.dump listify'
scp listify-s1:/tmp/listify-final.dump ./backups/
ls -lh ./backups/listify-final.dump # VÉRIFIER que le dump est arrivé...
ssh -t listify-s1 'sudo poweroff' # ... AVANT d'éteindre définitivement
Le -t est indispensable partout où une commande distante appelle sudo : sans pseudo-terminal, sudo refuse de demander le mot de passe (« a terminal is required », TP 4, partie A). Il en va de même pour toutes les commandes ssh ... 'sudo ...' de ce TP et du suivant. Et vérifiez toujours l'arrivée du dump avant d'éteindre la source : une extinction ne se rattrape qu'en rallumant, mais l'ordre des gestes est ce qui distingue une migration d'un incident.
Une sauvegarde qui sert à déménager : les sauvegardes ne sont pas que pour les catastrophes, ce sont les valises des données.
3.2 Installer et ouvrir PostgreSQL au réseau privé
Sur listify-db (rituel d'inventaire du TP 2 compris, il va vite désormais) :
ssh listify-db
# Sur listify-db :
hostname # vérifier la machine AVANT toute chose
sudo apt update && sudo apt install -y postgresql
sudo ss -tlnp | grep 5432 # 127.0.0.1 uniquement : il faut ouvrir au privé
Deux fichiers à modifier, et seulement ceux-là :
# 1. Écouter AUSSI sur l'adresse privée (jamais 0.0.0.0 : ch. 3, le bind est une
# décision de sécurité ; la carte NAT n'a aucune raison de porter PostgreSQL)
sudo sed -i "s/^#listen_addresses = 'localhost'/listen_addresses = 'localhost, 192.168.56.31'/" \
/etc/postgresql/16/main/postgresql.conf
# 2. Autoriser le rôle listify DEPUIS app1 uniquement (pg_hba, vu au TP 2)
echo 'host listify listify 192.168.56.21/32 scram-sha-256' | \
sudo tee -a /etc/postgresql/16/main/pg_hba.conf
sudo systemctl restart postgresql
sudo ss -tlnp | grep 5432 # localhost ET .31 désormais
3.3 Recréer le rôle, restaurer les données
# Générer un NOUVEAU mot de passe (celui du bloc 1 a vécu ; il ira dans le
# listify.env de app1 à l'étape 4)
openssl rand -base64 24
sudo -u postgres psql -c "CREATE ROLE listify LOGIN PASSWORD 'LE_NOUVEAU_MDP';"
sudo -u postgres psql -c "CREATE DATABASE listify OWNER listify;"
# Depuis l'hôte : livrer le dump, puis restaurer EN TANT QUE listify
scp ./backups/listify-final.dump listify-db:/tmp/
--clean --if-exists /tmp/listify-final.dump'
-c "SELECT count(*) FROM tasks;"'
# Le compte doit être celui du bloc 1 : les données ont déménagé.
3.4 Le pare-feu de la zone données
ssh listify-db
# Sur listify-db :
sudo ufw allow from 192.168.56.21 to any port 5432 proto tcp
sudo ufw status verbose
Point de contrôle n° 2 : la segmentation est réelle
Trois tests, deux doivent échouer (à consigner : ce sont les preuves) :
ssh listify-app1 'nc -zvw 3 listify-db 5432' # OK : le flux autorisé
ssh listify-lb 'nc -zvw 3 listify-db 5432' # timeout : le LB n'a rien à faire là
nc -zvw 3 192.168.56.31 5432 # timeout : l'hôte non plus
Notez la forme de l'échec : timeout, pas refused : le pare-feu jette (DENY/DROP), il ne répond pas (ch. 3, §5.2). Et remarquez que la défense est double : même sans ufw, pg_hba n'accepterait que .21 ; c'est la défense en profondeur du ch. 5, en version réseau.
Étape 4 : le backend sur sa machine (45 min)
Sur listify-app1, rejouez l'installation du TP 2, avec trois différences qui sont toute la leçon :
ssh listify-app1
# Sur listify-app1 :
hostname # vérifier la machine AVANT toute chose
sudo adduser --system --group --home /opt/listify --shell /usr/sbin/nologin listify
sudo apt update && sudo apt install -y python3 python3-venv
# Depuis l'hôte : livrer le code (v1.1)
scp -r backend listify-app1:/tmp/backend
ssh -t listify-app1 'sudo mv /tmp/backend /opt/listify/backend &&
sudo chown -R listify:listify /opt/listify/backend'
# Sur app1 : venv, configuration, unité
sudo -u listify python3 -m venv /opt/listify/venv
sudo -u listify /opt/listify/venv/bin/pip install -r /opt/listify/backend/requirements.txt
sudo mkdir -p /etc/listify
sudo tee /etc/listify/listify.env > /dev/null <<'EOF'
DB_HOST=listify-db
DB_PORT=5432
DB_NAME=listify
DB_USER=listify
DB_PASSWORD=LE_NOUVEAU_MDP
EOF
sudo chown root:listify /etc/listify/listify.env
sudo chmod 640 /etc/listify/listify.env
sudo tee /etc/systemd/system/listify.service > /dev/null <<'EOF'
[Unit]
Description=Listify backend (Gunicorn)
After=network-online.target
Wants=network-online.target
[Service]
User=listify
Group=listify
WorkingDirectory=/opt/listify/backend
EnvironmentFile=/etc/listify/listify.env
ExecStart=/opt/listify/venv/bin/gunicorn --workers 3 --bind 192.168.56.21:8000 wsgi:app
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now listify
# Le pare-feu de la zone applicative : 8000 pour le LB seulement
sudo ufw allow from 192.168.56.10 to any port 8000 proto tcp
Les trois différences avec le TP 2, à expliquer dans le runbook :
DB_HOST=listify-db: un nom du réseau privé, plus127.0.0.1. Le code n'a pas changé d'une ligne : c'est le facteur III et le facteur IV (ch. 4) qui livrent exactement ce qu'ils promettaient.--bind 192.168.56.21:8000: le backend écoute sur le réseau privé (le LB doit le joindre), sur son adresse précise, pas0.0.0.0(la carte NAT n'a pas à porter l'API).- Plus de
After=postgresql.service: PostgreSQL n'est plus une unité locale, c'est un service distant. systemd ne peut plus rien ordonnancer : la dépendance est devenue réseau, invisible pour lui. C'est un petit deuil et une grande leçon (ch. 6, §5) ; notre API sait attendre (503 propre), c'est elle qui absorbe.
Point de contrôle n° 3
ssh listify-app1 'curl -s http://192.168.56.21:8000/api/health'
# {"api":"ok","database":"ok"} ← ok/ok À TRAVERS le réseau privé
curl -m 3 http://192.168.56.21:8000/api/health # depuis l'hôte : timeout (ufw)
Étape 5 : le load balancer, porte d'entrée unique (45 min)
Sur listify-lb : Nginx, les statiques, le TLS, et le proxy qui pointe désormais vers une autre machine :
Connectez-vous d'abord (commande seule), puis collez le bloc suivant une fois l'invite affichée :
ssh listify-lb
# Sur listify-lb :
hostname # vérifier la machine AVANT toute chose
sudo apt update && sudo apt install -y nginx
nginx -v # confirme l'installation : /etc/nginx existe désormais
sudo mkdir -p /opt/listify
# Depuis l'hôte : les statiques (v1.1)
scp -r frontend listify-lb:/tmp/frontend
ssh -t listify-lb 'sudo mv /tmp/frontend /opt/listify/frontend &&
sudo chown -R root:root /opt/listify/frontend &&
sudo chmod -R a+rX /opt/listify/frontend'
# (pas de chmod o+x du parent ici : /opt/listify vient d'un mkdir root en 755,
# ce n'est pas un home 750 ; comparez avec le piège du TP 3 et expliquez)
# Sur lb : certificat (TP 3, étape 3, à l'identique) puis le site
sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout /etc/nginx/ssl/listify.key -out /etc/nginx/ssl/listify.crt \
-days 365 -nodes -subj "/CN=listify.local" \
-addext "subjectAltName=DNS:listify.local"
sudo chmod 600 /etc/nginx/ssl/listify.key
sudo tee /etc/nginx/sites-available/listify > /dev/null <<'EOF'
server {
listen 80;
server_name listify.local;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name listify.local;
ssl_certificate /etc/nginx/ssl/listify.crt;
ssl_certificate_key /etc/nginx/ssl/listify.key;
ssl_protocols TLSv1.2 TLSv1.3;
root /opt/listify/frontend;
index index.html;
location / {
try_files $uri $uri/ =404;
}
location /api/ {
proxy_pass http://listify-app1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/listify /etc/nginx/sites-enabled/listify
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp
Deux détails qui closent des dossiers ouverts au TP 3 :
- La redirection est redevenue
https://$host$request_uri, la forme « production » : plus de NAT entre le navigateur et Nginx, donc plus de:8443. La verrue du TP 3 disparaît avec sa cause : notez la boucle. proxy_pass http://listify-app1:8000;: un nom, résolu via/etc/hostsau chargement de la configuration. Si l'adresse de app1 change un jour : un fichier hosts à corriger, zéro configuration Nginx.
Côté hôte, mettez à jour la résolution : remplacez la ligne du TP 3 (127.0.0.1 listify.local) par :
192.168.56.10 listify.local
Étape 6 : la traversée complète (30 min)
# Depuis l'hôte, la chaîne entière :
curl -sk https://listify.local/api/health # {"api":"ok","database":"ok"}
curl -sk https://listify.local/ | head -5 # le HTML
curl -s -o /dev/null -w '%{redirect_url}\n' http://listify.local/
Puis au navigateur : https://listify.local (acceptez le nouveau certificat). Vos tâches du bloc 1 s'affichent : elles ont traversé un pg_dump, un scp, un pg_restore et trois machines. Créez-en une nouvelle, cochez-la : la requête parcourt hôte → lb → app1 → db et revient.
Terminez par la preuve de segmentation complète, tableau à remplir au runbook (chaque case = une commande nc -zvw 3 et son résultat) :
| Depuis ↓ vers → | lb :443 | app1 :8000 | db :5432 |
|---|---|---|---|
| hôte | OK | timeout | timeout |
| lb | : | OK | timeout |
| app1 | : | : | OK |
Point de contrôle final
-
https://listify.localfonctionne au navigateur, avec les données du bloc 1 (migration prouvée par le comptage) - Le tableau de segmentation est rempli, avec les échecs en timeout expliqués
- Chaque VM :
sudo ss -tlnpne montre que les ports de son rôle, sur les bonnes adresses (récitez la justification) - Les trois clones ont des hostnames, adresses, et empreintes SSH distincts
- Plan d'adressage documenté dans le README ;
~/.ssh/configet/etc/hostsde l'hôte à jour -
listify-s1éteinte, conservée ; snapshottp5-eclatepris sur les trois VM ; runbook committé
Pour aller plus loin (bonus)
- DNS interne : installez dnsmasq sur
listify-lb, faites-en le résolveur des trois VM (netplannameservers:), et supprimez les blocs/etc/hosts. Qu'a-t-on centralisé, et qu'a-t-on fragilisé ? (Indice : que se passe-t-il si lb tombe ?) - Zone données étanche : ajoutez une carte « réseau interne » VirtualBox entre app1 et db et faites passer PostgreSQL dessus : la base disparaît même du réseau host-only. Quel prix pour l'administration ?
- Chronométrage prospectif : notez précisément le temps passé sur ce TP. Au TP 9, Vagrant + Ansible referont tout ceci en une commande ; l'écart chiffré ira dans votre compte rendu.
Questions de compréhension (à préparer pour le TD)
- Reconstituez le trajet complet d'un
POST /api/tasksdepuis le navigateur, en nommant chaque machine, chaque adresse, chaque port traversé, et l'endroit exact où chaque règle ufw et pg_hba intervient. - Pourquoi avoir mis
listen_addresses = 'localhost, 192.168.56.31'et non'*'? Donnez les deux lignes de défense que cette décision complète. - La panne « un seul /etc/hosts oublié » : décrivez précisément les symptômes si le fichier de
listify-lbn'avait pas l'entréelistify-app1(que ditnginx -t? et au chargement ? et les autres machines ?). - Le clonage a copié les clés d'hôte SSH et aurait copié une adresse netplan si la base en avait eu une. Généralisez : qu'est-ce qui, dans l'état d'une machine, est « rôle » (clonable) et qu'est-ce qui est « identité » (à attribuer) ? Cette distinction resservira mot pour mot au S2 (images vs configuration d'instance).