Lab 7.2 : Application trois niveaux avec Compose¶
| Durée | 75 min |
| Niveau | Semi-guidé |
| Prérequis | Lab 7.1 : Première pile Compose, Lab 6.2 : Isoler une application par réseaux, cours du chapitre 7 |
| Acquis visés | AA5 |
Objectifs
- Décrire dans un seul fichier Compose une application en trois niveaux (proxy, API, base de données)
- Cloisonner l'application avec deux réseaux et ne publier que le point d'entrée
- Définir des healthchecks et ordonner le démarrage avec
depends_on - Observer le comportement de la pile lorsque la base s'arrête
- Vérifier la persistance des données après
downetup
Contexte et schéma¶
Vous reprenez l'architecture du Lab 6.2, cette fois avec une vraie API et un fichier Compose. L'API est une petite application Flask qui compte les visites dans une base PostgreSQL. Son code vous est fourni : vous n'avez pas besoin de connaître Python.
flowchart LR
U["Hôte<br/>127.0.0.1:8080"] --> P["proxy<br/>nginx"]
subgraph F["Réseau frontend"]
P
A["api"]
end
subgraph B["Réseau backend (interne)"]
A2["api"]
D["db<br/>PostgreSQL"]
end
P --- A
A2 --- D
D --- V[("Volume pgdata")]
| Service | Rôle | Image | Réseaux |
|---|---|---|---|
proxy |
Point d'entrée, transmet les requêtes à l'API | nginx:1.27-alpine |
frontend |
api |
Application Flask, construite par vous | construite depuis ./api |
frontend et backend |
db |
Base de données | postgres:16-alpine |
backend |
Selon votre option
Sous PowerShell, utilisez curl.exe et écrivez les commandes sur une seule ligne. Dans les fichiers YAML, n'utilisez que des espaces pour l'indentation.
Étapes¶
Étape 1 : Préparer les fichiers fournis¶
- Objectif : disposer du code de l'API et de la configuration du proxy.
- Indices : créez le dossier
tp-trois-niveauxet placez-vous dedans, puis créez l'arborescence suivante :
tp-trois-niveaux/
├── api/
│ ├── app.py
│ ├── requirements.txt
│ └── Dockerfile
├── nginx/
│ └── default.conf
└── .env
api/requirements.txt
api/app.py
import os
import psycopg2
from flask import Flask
app = Flask(__name__)
def connexion():
return psycopg2.connect(
host=os.environ["DB_HOST"],
dbname=os.environ["DB_NAME"],
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
)
@app.route("/")
def visites():
conn = connexion()
with conn:
with conn.cursor() as cur:
cur.execute(
"CREATE TABLE IF NOT EXISTS visites "
"(id serial PRIMARY KEY, date timestamp DEFAULT now())"
)
cur.execute("INSERT INTO visites DEFAULT VALUES")
cur.execute("SELECT count(*) FROM visites")
total = cur.fetchone()[0]
conn.close()
return f"Nombre de visites : {total}\n"
@app.route("/sante")
def sante():
return "ok\n"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Deux routes
/ enregistre une visite dans la base et renvoie le total. /sante répond ok sans toucher à la base : elle servira au healthcheck de l'API.
api/Dockerfile (l'ordre des instructions est celui que vous avez optimisé au Lab 3.2)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
nginx/default.conf : le proxy transmet toutes les requêtes au service api, désigné par son nom.
.env
Créez aussi un .gitignore contenant .env, et un .env.example avec des valeurs fictives (voir le Lab 7.1).
Point de contrôle
- L'arborescence est conforme et les fichiers ne portent pas d'extension
.txtsupplémentaire - Le port 8080 n'est utilisé par aucun conteneur (
docker ps)
Étape 2 : Écrire le fichier compose.yaml¶
- Objectif : décrire les trois services, les deux réseaux, le volume, les healthchecks et les dépendances.
- Indices :
- Réseaux :
frontendordinaire,backendinterne (section 4 du cours). db: imagepostgres:16-alpine, réseaubackendseulement, volume nommépgdata, aucune publication de port. Ses variablesPOSTGRES_USER,POSTGRES_PASSWORDetPOSTGRES_DBproviennent du fichier.env. Healthcheck avecpg_isready(section 7.2).api: image construite depuis le dossier./api(clébuild), réseauxfrontendetbackend, aucun port publié. VariablesDB_HOST(le nom du service de la base),DB_NAME,DB_USER,DB_PASSWORD. Elle ne doit démarrer que lorsquedbest healthy. Pour son healthcheck, l'image ne contient pascurl: utilisez Python.
- Réseaux :
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/sante')"]
interval: 5s
timeout: 3s
retries: 5
start_period: 5s
- **`proxy`** : image `nginx:1.27-alpine`, réseau `frontend`, port publié **uniquement sur l'hôte** (`127.0.0.1`) avec `${WEB_PORT}` comme port de l'hôte, fichier `./nginx/default.conf` monté en lecture seule sur `/etc/nginx/conf.d/default.conf`. Il ne doit démarrer que lorsque `api` est **healthy**.
Validez avec docker compose config.
Point de contrôle
-
docker compose configs'exécute sans erreur et les valeurs de.envsont substituées - Seul
proxypossède une sectionports -
backendest déclaréinternal: true -
apietproxyont une conditionservice_healthysur le service dont ils dépendent
Étape 3 : Démarrer et tester la pile¶
- Objectif : démarrer l'application et observer l'enchaînement des démarrages.
- Indices :
- Lancez
docker compose up -d --build, puis surveillez l'état avecdocker compose pspendant quelques secondes :db, puisapi, puisproxy. - Testez trois fois de suite :
curl http://localhost:8080. - Consultez les journaux :
docker compose logs api.
- Lancez
Point de contrôle
- Les trois services sont
running,dbetapisonthealthy - Les trois requêtes renvoient
Nombre de visites : 1, puis2, puis3 - Les journaux de
apimontrent les requêtes reçues du proxy
Étape 4 : Vérifier le cloisonnement¶
- Objectif : confirmer que l'architecture respecte le plan.
- Indices : complétez le tableau en prédisant d'abord le résultat, puis en testant avec
docker compose exec <service> ping -c 2 <cible>.
| Source | Cible | Résultat attendu | Résultat observé |
|---|---|---|---|
proxy |
api |
À déterminer | |
proxy |
db |
À déterminer | |
api |
db |
À déterminer |
Vérifiez aussi qu'aucun port n'est exposé en dehors de celui du proxy : docker compose ps et docker ps.
Point de contrôle
-
proxyjointapimais pasdb -
apijointdb - Une seule publication de port apparaît, celle de
proxy, limitée à127.0.0.1
Étape 5 : Observer une panne de la base¶
- Objectif : constater l'effet d'un service arrêté, puis le retour à la normale.
- Indices :
- Arrêtez uniquement la base :
docker compose stop db. - Testez
curl http://localhost:8080, puis consultezdocker compose logs apietdocker compose ps. - Redémarrez la base :
docker compose start db, attendez qu'elle soithealthy, puis testez de nouveau.
- Arrêtez uniquement la base :
Point de contrôle
- Pendant la panne,
curlrenvoie une erreur (les journaux deapimontrent l'échec de connexion) alors queapiresterunning - Après le redémarrage de
db, le service reprend et le compteur continue de la valeur précédente - Vous savez expliquer pourquoi l'API reste
healthypendant la panne (voir les questions de réflexion)
Étape 6 : Vérifier la persistance¶
- Objectif : s'assurer que les données survivent à la suppression de la pile.
- Indices :
- Notez le compteur, puis exécutez
docker compose down, puisdocker compose up -d. - Testez : le compteur doit continuer.
- Exécutez
docker compose down -v, relancez, testez : le compteur repart de 1.
- Notez le compteur, puis exécutez
Point de contrôle
- Après
downpuisup, le compteur continue - Après
down -vpuisup, le compteur repart de 1
Étape 7 (facultative) : Modifier l'API¶
- Objectif : reconstruire un seul service.
- Indices :
- Modifiez le message renvoyé par
app.py, puis lancezdocker compose up -d --build. - Observez quels services sont recréés.
- Si le proxy ne répond plus après la recréation de
api, redémarrez-le avecdocker compose restart proxy.
- Modifiez le message renvoyé par
Point de contrôle
- Le nouveau message est servi
- Vous avez identifié les services recréés
Questions de réflexion¶
Pourquoi proxy ne peut-il pas joindre db ?
Il est branché sur frontend seulement, alors que db est sur backend seulement : ils ne partagent aucun réseau. Seule api est branchée sur les deux.
Pourquoi la base n'a-t-elle aucun port publié alors que l'API s'y connecte ?
L'API et la base sont sur le même réseau : elles communiquent par le nom de service et le port du conteneur (5432). Publier le port ne servirait qu'à exposer la base à l'hôte.
À quoi sert condition: service_healthy pour l'API ?
L'API ne démarre que lorsque la base accepte des connexions. Sans cette condition, depends_on ne garantit que l'ordre de démarrage des conteneurs, et l'API pourrait démarrer avant que la base soit prête.
Pourquoi l'API reste-t-elle healthy pendant la panne de la base ?
Son healthcheck interroge la route /sante, qui ne touche pas à la base. Il vérifie que le processus de l'API répond, pas que toutes ses dépendances fonctionnent. Choisir ce que mesure un healthcheck est un choix de conception.
Pourquoi les identifiants sont-ils dans .env et non dans compose.yaml ?
Le fichier Compose peut être versionné et partagé, alors que .env reste propre à la machine et exclu du dépôt.
Dépannage¶
| Symptôme | Pistes |
|---|---|
api reste unhealthy |
Consultez docker compose logs api : erreur de syntaxe Python, ou dépendance manquante |
could not translate host name "db" dans les journaux de api |
api n'est pas sur le réseau backend, ou DB_HOST n'est pas le nom du service |
password authentication failed |
Le volume contient déjà une base créée avec un autre mot de passe : utilisez-le, ou docker compose down -v |
host not found in upstream "api" dans les journaux de proxy |
Le proxy a démarré sans pouvoir résoudre api : vérifiez qu'il est sur frontend et qu'il dépend de api |
502 Bad Gateway |
api ne répond pas : consultez ses journaux et son état |
port is already allocated |
Le port est déjà utilisé : changez WEB_PORT dans .env |
Erreur à l'installation de psycopg2-binary pendant --build |
Notez le message exact et signalez-le à l'enseignant : la version épinglée est à valider |
Sous PowerShell, curl ne se comporte pas comme attendu |
Utilisez curl.exe |
Nettoyage et reprise¶
(Le nom de l'image construite est celui du projet suivi du nom du service ; vérifiez-le avec docker images.) Conservez le dossier tp-trois-niveaux : il sert de point de départ au Mini-projet 2.