Aller au contenu
English

Configuration à l'exécution

  • Leçon 8
  • 45 min
  • Module 3 : Réseau, configuration et persistance
Leçon 8 sur 15 · L'essentiel de Docker et du nuage pour les développeurs en IAConfiguration à l'exécution

Ce que vous saurez faire

Exécutez une image à plusieurs endroits, chacun avec ses paramètres et ses secrets, fournis au démarrage du conteneur. Gardez la clé d’API hors de la configuration du conteneur. Faites refuser le démarrage de l’API sans la clé dont elle a besoin.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 45 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Testé avec Docker Desktop 4.81.0 (Docker Engine 29.6.1) sur macOS, avec une puce Apple. Les étapes Windows et Linux ne sont pas testées.

Amira démarre deux conteneurs à partir de la même image. Un seul paramètre diffère :

Terminal
docker run -d --name docs-on -p 127.0.0.1:8000:8000 ticket-api:1.0.0
docker run -d --name docs-off -p 127.0.0.1:8001:8000 -e SHOW_DOCS=false ticket-api:1.0.0
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/docs
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8001/docs

La sortie, après les deux identifiants de conteneur :

200
404

-w "%{http_code}\n" indique à curl d’afficher seulement le code d’état. Même image, même code, deux comportements. -e NAME=value définit une variable d’environnement dans le conteneur. C’est une valeur nommée que le système d’exploitation fournit à un programme au démarrage. L’application la lit dans config.py, comme dans le cours sur les API.

C’est la principale raison de garder les paramètres hors de l’image. Vous construisez une image, la testez, puis exécutez exactement cette image sur votre ordinateur, dans Compose et dans le nuage. Seuls les paramètres changent. Si un paramètre était dans l’image, chaque endroit aurait besoin de sa propre construction. Vous déploieriez alors quelque chose que vous n’avez pas testé.

Terminal
docker rm -f docs-on docs-off

Les deux conteneurs ci-dessus n’avaient aucune clé d’API. Après les lignes de démarrage, le journal de docs-on (docker logs docs-on) contenait un avertissement :

WARNING ticket_api: API_KEY is not set: /v1/classify accepts requests without a key

Sur votre ordinateur, c’est utile. Dans le nuage, c’est un risque : un secret manquant rend l’API ouverte. Le seul signe est une ligne de journal. Un paramètre peut être :

  • facultatif : l’application a une valeur par défaut sûre, par exemple SHOW_DOCS vaut true, LOG_LEVEL vaut INFO;
  • obligatoire : l’application ne doit pas démarrer sans lui.

La clé est facultative sur votre ordinateur et obligatoire dans le nuage. Cette leçon ajoute donc un paramètre qui indique lequel des deux cas s’applique : REQUIRE_API_KEY. Avec REQUIRE_API_KEY=true, l’API s’arrête au démarrage si elle n’a pas de clé. Un déploiement sans secret échoue alors de façon visible, plutôt que de fonctionner sans protection.

Les paramètres de l’API à la fin de ce module :

Paramètre Obligatoire Valeur par défaut Son rôle
API_KEY ou API_KEY_FILE Quand REQUIRE_API_KEY vaut true aucune La clé que les clients envoient dans X-API-Key. API_KEY_FILE est le chemin d’un fichier qui la contient.
REQUIRE_API_KEY Non false true : l’API ne démarre pas sans clé.
DATABASE_URL ou DATABASE_URL_FILE Non aucune Une URL PostgreSQL. Sans elle, l’historique est désactivé (prochaine leçon).
ALLOWED_ORIGINS Non aucune Les sites qui peuvent appeler l’API depuis un navigateur, séparés par des virgules.
SHOW_DOCS Non true false masque /docs, /redoc et /openapi.json.
CLASSIFIER_MODE, CLASSIFIER_TIMEOUT, LOG_LEVEL Non keywords, 2.0, INFO Comme dans le cours sur les API.

Deux fichiers changent. Dans ticket_api/config.py, les nouvelles lignes commencent par +, et les lignes supprimées commencent par - :

ticket_api/config.py
import os
from dataclasses import dataclass, field
from pathlib import Path
class SettingsError(Exception):
"""A required setting is missing or not valid. The app must not start."""
@dataclass(frozen=True)
class Settings:
api_key: str | None = field(repr=False)
allowed_origins: list[str]
classifier_mode: str
classifier_timeout: float
log_level: str
show_docs: bool
database_url: str | None = field(default=None, repr=False)
def read_secret(name: str) -> str | None:
"""Return the value of NAME, or the contents of the file in NAME_FILE."""
path = os.environ.get(f"{name}_FILE")
if path:
try:
return Path(path).read_text(encoding="utf-8").strip() or None
except OSError as error:
raise SettingsError(f"{name}_FILE: cannot read {path}: {error.strerror}")
return os.environ.get(name) or None
def load_settings() -> Settings:
"""Read the settings from environment variables, with safe defaults."""
origins = os.environ.get("ALLOWED_ORIGINS", "")
return Settings(
api_key=os.environ.get("API_KEY") or None,
settings = Settings(
api_key=read_secret("API_KEY"),
allowed_origins=[o.strip() for o in origins.split(",") if o.strip()],
classifier_mode=os.environ.get("CLASSIFIER_MODE", "keywords"),
classifier_timeout=float(os.environ.get("CLASSIFIER_TIMEOUT", "2.0")),
log_level=os.environ.get("LOG_LEVEL", "INFO").upper(),
show_docs=os.environ.get("SHOW_DOCS", "true").lower() == "true",
database_url=read_secret("DATABASE_URL"),
)
if os.environ.get("REQUIRE_API_KEY", "false").lower() == "true" and not settings.api_key:
raise SettingsError("REQUIRE_API_KEY is true, but API_KEY is not set")
return settings
Partie Pourquoi
SettingsError Un type d’erreur pour « les paramètres sont incorrects ». main.py l’intercepte et s’arrête avec un message clair.
read_secret("API_KEY") Si API_KEY_FILE est défini, cette fonction lit la clé dans ce fichier. Sinon, elle utilise API_KEY, comme avant. .strip() supprime un saut de ligne à la fin du fichier.
Un fichier illisible Déclenche SettingsError avec le chemin et la raison. Un mauvais chemin doit arrêter l’application, pas la laisser sans clé.
database_url Pour la prochaine leçon. Sa lecture suit la même méthode. Un mot de passe dans l’URL peut donc aussi venir d’un fichier.
repr=False La clé et l’URL n’apparaissent pas quand les paramètres sont affichés ou consignés dans les journaux.
REQUIRE_API_KEY La vérification à la fin : true sans clé déclenche SettingsError.

Dans ticket_api/main.py, importez l’erreur et interceptez-la à l’endroit où l’application est créée :

ticket_api/main.py
from ticket_api.config import Settings, load_settings
from ticket_api.config import Settings, SettingsError, load_settings
ticket_api/main.py (the last lines)
app = create_app()
try:
app = create_app()
except SettingsError as error:
raise SystemExit(f"Settings error: {error}") from None

SystemExit arrête Python avec le code de sortie 1 et affiche seulement le message. from None masque la trace d’erreur, qui n’aiderait pas le lecteur.

Amira exécute les tests dans son environnement virtuel, avec python -m pytest. La sortie se termine par :

17 passed in 0.19s

Aucun test n’a encore changé. La prochaine leçon ajoute les tests pour ces paramètres.

Elle construit ensuite une nouvelle image. Le code n’est pas terminé pour la version 1.1.0, alors elle lui donne l’étiquette 1.1.0-dev :

Terminal
docker build -t ticket-api:1.1.0-dev .

La sortie (abrégée) :

#9 [5/7] RUN pip install -r requirements-run.txt
#9 CACHED
#10 [6/7] COPY pyproject.toml .
#10 CACHED
#11 [7/7] COPY ticket_api ./ticket_api
#11 DONE 0.0s
...
#12 naming to docker.io/library/ticket-api:1.1.0-dev done

Seule la couche du code est nouvelle, comme dans Construire efficacement : le cache des couches.

De nombreuses options -e donnent une longue commande. --env-file les lit plutôt dans un fichier : un NAME=value par ligne. Amira crée le fichier .env dans le dossier du projet :

.env
# Settings for the container on this computer. Never commit this file.
SHOW_DOCS=false
REQUIRE_API_KEY=true
LOG_LEVEL=INFO

.env est dans .gitignore, donc Git ne l’envoie jamais. La liste d’autorisation de .dockerignore le garde aussi hors de l’image.

Prédisez : ce fichier indique qu’une clé est obligatoire, mais il n’en contient aucune. Que se passe-t-il?

Terminal
docker run --rm --env-file .env ticket-api:1.1.0-dev

La sortie :

⚡️ Starting FastAPI in production mode
🐍 Using import string: ticket_api.main:app
🌐 Server started at http://0.0.0.0:8000
Documentation at http://0.0.0.0:8000/docs
Logs:
Settings error: REQUIRE_API_KEY is true, but API_KEY is not set

Le conteneur s’arrête immédiatement, avec le code de sortie 1. Lisez toute la sortie : Server started (serveur démarré) apparaît avant l’erreur. La commande fastapi affiche cette ligne avant d’importer l’application. Elle ne signifie donc pas que l’application a démarré.

La clé pourrait aussi aller dans .env. Mais regardez d’abord ce que Docker conserve sur un conteneur qui reçoit sa clé avec -e :

Terminal
docker run -d --name ticket-api -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key -e SHOW_DOCS=false ticket-api:1.1.0-dev
docker inspect ticket-api --format "{{json .Config.Env}}"

La sortie (après l’identifiant du conteneur) :

["API_KEY=local-dev-key","SHOW_DOCS=false","PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","PYTHON_VERSION=3.14.8",...]

La clé est là en texte clair. Toute personne qui peut exécuter docker inspect sur cet ordinateur peut la lire. Les variables d’environnement apparaissent aussi ailleurs : rapports de plantage, docker exec ... printenv et certains outils de surveillance. Un secret est une valeur qui donne accès, comme une clé ou un mot de passe. Il demande plus de précautions qu’un paramètre.

Terminal
docker rm -f ticket-api

Un fichier secret garde le secret hors de la configuration du conteneur. Le secret reste dans un fichier sur votre ordinateur. Vous montez le fichier dans le conteneur, en lecture seule, et l’application le lit à cet endroit. Monter un fichier ou dossier de l’hôte dans un conteneur s’appelle un montage lié (bind mount). Le conteneur voit le fichier de l’hôte, pas une copie.

  1. Créez un dossier secrets et le fichier de clé :

    Fenêtre de terminal
    mkdir -p secrets
    printf 'local-dev-key' > secrets/api_key.txt
  2. Ouvrez .gitignore. Vérifiez qu’il contient la ligne secrets/. Si elle manque, ajoutez-la.

  3. Exécutez git status. Vérifiez que secrets/ et .env ne sont pas dans la liste.

Amira démarre l’API avec le fichier de paramètres, le fichier de clé et un paramètre de plus :

Fenêtre de terminal
docker run -d --name ticket-api -p 127.0.0.1:8000:8000 --env-file .env -e API_KEY_FILE=/run/secrets/api_key -v "$PWD/secrets/api_key.txt:/run/secrets/api_key:ro" ticket-api:1.1.0-dev
Partie Signification
-v "$PWD/secrets/api_key.txt:/run/secrets/api_key:ro" Monte le fichier de l’hôte (à gauche, un chemin complet : $PWD est le dossier courant) à /run/secrets/api_key dans le conteneur (à droite). ro signifie lecture seule.
-e API_KEY_FILE=/run/secrets/api_key Indique à read_secret où se trouve la clé. C’est un chemin, pas un secret.
/run/secrets/ Le dossier habituel des fichiers secrets dans un conteneur. Compose l’utilise aussi (prochaine leçon).

Elle vérifie l’API, sans la clé puis avec la clé :

Terminal
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:8000/v1/classify -H "Content-Type: application/json" -d @requests/billing.json
curl -s -X POST http://127.0.0.1:8000/v1/classify -H "Content-Type: application/json" -H "X-API-Key: local-dev-key" -d @requests/billing.json

La sortie :

401
{"category":"billing","priority":1,"confidence":0.9,"model_version":"keywords-1.0"}

Elle regarde ensuite de nouveau ce que Docker conserve :

Terminal
docker inspect ticket-api --format "{{json .Config.Env}}"

La sortie :

["SHOW_DOCS=false","REQUIRE_API_KEY=true","LOG_LEVEL=INFO","API_KEY_FILE=/run/secrets/api_key","PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",...]

La configuration contient le chemin de la clé, pas la clé. Le fichier est en lecture seule dans le conteneur. L’application ne peut pas le modifier, ni un attaquant qui s’introduit dans l’application :

Terminal
docker exec ticket-api sh -c "echo changed > /run/secrets/api_key"

La sortie :

sh: 1: cannot create /run/secrets/api_key: Read-only file system

Un fichier secret n’est pas chiffré. Toute personne qui peut lire le fichier sur votre ordinateur, ou exécuter docker exec dans le conteneur, peut encore lire la clé. Il retire la clé des endroits où elle peut facilement fuiter : la configuration, docker inspect, l’image et Git.

Terminal
docker rm -f ticket-api
Trois options de docker run à gauche : -e NAME=value pour un paramètre, --env-file .env pour un fichier de lignes NAME=value, et -v avec un chemin se terminant par :/run/secrets/api_key:ro pour un fichier secret en lecture seule. Les deux premières deviennent des variables d'environnement dans le conteneur (SHOW_DOCS, REQUIRE_API_KEY, API_KEY_FILE). La troisième devient le fichier /run/secrets/api_key. Les deux vont vers config.py, load_settings et read_secret. Si toutes les valeurs obligatoires sont présentes, l'application démarre avec ces paramètres. Si une valeur obligatoire manque, elle s'arrête avec Settings error (erreur de paramètres) et le code de sortie 1.
D'où viennent les paramètres de l'API au démarrage du conteneur, et ce qui arrive quand un paramètre manque.
Description textuelle du diagramme

Les paramètres arrivent dans le conteneur de trois façons. -e NAME=value définit une variable d’environnement. --env-file .env en définit plusieurs, à partir d’un fichier de lignes NAME=value. -v ...:/run/secrets/api_key:ro monte un fichier secret en lecture seule. Dans le conteneur, config.py lit les variables d’environnement (SHOW_DOCS, REQUIRE_API_KEY, API_KEY_FILE) et, avec read_secret, le fichier. Si toutes les valeurs obligatoires sont présentes, l’application démarre. Si une valeur obligatoire manque, l’application s’arrête avec Settings error: ... (erreur de paramètres) et le code de sortie 1.

L’image ticket-api:1.1.0 que vous construirez dans la prochaine leçon s’exécute à trois endroits dans ce cours. Seuls les paramètres changent :

Où Comment elle reçoit ses paramètres La clé
Votre ordinateur, seule -e, --env-file .env -e API_KEY=... ou un fichier secret
Votre ordinateur, avec une base de données (prochaine leçon) compose.yaml et .env un secret Compose provenant de secrets/api_key.txt
Azure Container Apps (module 5) les variables d’environnement de la plateforme un secret de la plateforme (secretref:)
  1. Modifiez ticket_api/config.py et ticket_api/main.py comme dans Modifier config.py et main.py.
  2. Exécutez python -m pytest dans votre environnement virtuel. Vous devriez obtenir 17 passed (17 tests réussis).
  3. Construisez l’image : docker build -t ticket-api:1.1.0-dev .
  4. Créez .env comme dans Un fichier de paramètres.
  5. Exécutez docker run --rm --env-file .env ticket-api:1.1.0-dev. Vous devriez obtenir Settings error: REQUIRE_API_KEY is true, but API_KEY is not set (erreur de paramètres : REQUIRE_API_KEY vaut true, mais API_KEY n’est pas défini).
  6. Créez secrets/api_key.txt et vérifiez .gitignore, selon les étapes de Fichiers secrets.
  7. Démarrez l’API avec --env-file, API_KEY_FILE et le montage en lecture seule, comme dans Fichiers secrets.
  8. Envoyez requests/billing.json à /v1/classify sans la clé. Vous devriez obtenir 401.
  9. Envoyez-le avec X-API-Key: local-dev-key. Vous devriez obtenir le résultat de facturation.
  10. Exécutez docker inspect ticket-api --format "{{json .Config.Env}}". Vérifiez que la clé n’est pas dans la sortie.
  11. Supprimez le conteneur : docker rm -f ticket-api.
Afficher les résultats attendus

Étape 5 : les lignes de démarrage, puis Settings error: REQUIRE_API_KEY is true, but API_KEY is not set, et la commande se termine. Étape 8 : 401. Étape 9 : {"category":"billing","priority":1,"confidence":0.9,"model_version":"keywords-1.0"}. Étape 10 : API_KEY_FILE=/run/secrets/api_key est dans la liste, mais pas local-dev-key. Si l’étape 7 s’arrête avec Settings error: API_KEY_FILE: cannot read /run/secrets/api_key: No such file or directory (erreur de paramètres : API_KEY_FILE ne peut pas lire /run/secrets/api_key : fichier ou dossier introuvable), la partie -v manque ou contient une faute de frappe.

Changez une condition : la clé. Démarrez l’API comme à l’étape 7. Écrivez ensuite une nouvelle clé dans le fichier, sans arrêter le conteneur : printf 'second-key' > secrets/api_key.txt (Windows : Set-Content -NoNewline -Encoding ascii secrets\api_key.txt 'second-key').

Faites une prédiction, puis testez avec curl : quelle clé l’API accepte-t-elle maintenant? Et après docker restart ticket-api? À la fin, remettez local-dev-key dans le fichier et supprimez le conteneur.

Afficher la réponse

docker exec ticket-api cat /run/secrets/api_key affiche immédiatement second-key, car le conteneur voit le fichier de l’hôte. Mais l’API accepte encore seulement local-dev-key : second-key reçoit 401. config.py lit la clé une seule fois, au démarrage de l’application. Après docker restart ticket-api, second-key reçoit 200, et local-dev-key reçoit 401. Voilà pourquoi les plateformes infonuagiques démarrent une nouvelle copie de l’application quand vous changez un secret (module 5).

Erreur courante

Le conteneur s'arrête juste après « Server started » (serveur démarré)

Symptôme : docker run -d affiche un identifiant, mais l’API ne répond pas. docker ps ne liste pas le conteneur. docker ps -a affiche l’état Exited (1) (terminé avec le code 1). Le journal contient Server started at http://0.0.0.0:8000 (serveur démarré à cette adresse), donc l’application semble démarrer.

Cause : un paramètre obligatoire manque ou est incorrect, et l’application s’est arrêtée. La commande fastapi affiche Server started (serveur démarré) avant d’importer l’application, donc cette ligne apparaît d’abord. Le vrai message se trouve à la fin du journal.

Solution :

  1. Lisez la fin du journal : docker logs ticket-api. Cherchez Settings error: (erreur de configuration).
  2. REQUIRE_API_KEY is true, but API_KEY is not set (REQUIRE_API_KEY est vrai, mais API_KEY n’est pas défini) : fournissez la clé avec -e API_KEY=... ou avec API_KEY_FILE et un montage. Vérifiez aussi le nom : avec une faute de frappe comme API_KEY_FLE, l’application ne voit pas la variable. Vous obtenez alors le même message.
  3. API_KEY_FILE: cannot read /run/secrets/api_key: No such file or directory (impossible de lire le fichier : fichier ou dossier introuvable) : le chemin dans API_KEY_FILE doit correspondre au côté droit de -v. Vérifiez que le fichier existe sur l’hôte.
  4. Supprimez le conteneur arrêté avec docker rm ticket-api, puis démarrez-le de nouveau.

Pour un vrai service d’IA : un service qui appelle un fournisseur de modèles a besoin de la clé de ce fournisseur. Sans vérification au démarrage, il démarre, réussit une simple vérification de santé, puis échoue à la première vraie requête d’un utilisateur. Avec un paramètre obligatoire, le déploiement échoue immédiatement, et la plateforme garde l’ancienne version en marche.

Vérifiez votre compréhension

Pourquoi le cours fournit-il la clé d'API dans un fichier avec API_KEY_FILE, plutôt qu'avec -e API_KEY=...?

Afficher la réponse

Docker conserve toutes les variables d’environnement dans la configuration du conteneur. docker inspect affiche -e API_KEY=local-dev-key en texte clair. Les variables d’environnement peuvent aussi apparaître dans les rapports de plantage et dans printenv. Avec un fichier de secret, la configuration contient seulement le chemin (API_KEY_FILE=/run/secrets/api_key). Le fichier est monté en lecture seule, donc l’application ne peut pas le modifier. Toute personne qui peut lire le fichier peut encore lire la clé. Cette méthode est donc plus sûre, sans cacher la clé à tout le monde.

Vérifiez votre compréhension

Un conteneur démarre avec REQUIRE_API_KEY=true et API_KEY_FILE=/run/secrets/api_key, mais sans option -v. Prédisez ce qui se passe.

Afficher la réponse

L’application s’arrête au démarrage avec Settings error: API_KEY_FILE: cannot read /run/secrets/api_key: No such file or directory (erreur de configuration : impossible de lire le fichier, fichier ou dossier introuvable). Le conteneur se termine avec le code 1. read_secret déclenche SettingsError quand il ne peut pas lire le fichier. Il ne revient pas à « aucune clé », car cela démarrerait une API sans protection.

Vérifiez votre compréhension

Quels paramètres de l'API de billets sont obligatoires, et lesquels sont facultatifs? Pourquoi la clé d'API est-elle facultative par défaut?

Afficher la réponse

Seule la clé est obligatoire, et seulement quand REQUIRE_API_KEY vaut true (vrai). Tous les autres paramètres ont une valeur par défaut sûre (SHOW_DOCS=true, LOG_LEVEL=INFO, aucune base de données, aucune origine autorisée). La clé est facultative par défaut pour que l’API fonctionne sur votre ordinateur et dans les tests sans configuration. Là où l’API doit être protégée, dans Compose et dans le nuage, REQUIRE_API_KEY=true rend la clé obligatoire.

Récapitulatif

  • Les paramètres appartiennent au conteneur, pas à l’image : une même image testée fonctionne partout avec des paramètres différents.
  • -e NAME=value définit une variable d’environnement. --env-file .env en définit plusieurs, sans guillemets. .env reste hors de Git et de l’image.
  • docker inspect affiche toutes les variables d’environnement. Un secret va dans un fichier en lecture seule, monté avec -v ...:ro. L’application lit le chemin dans API_KEY_FILE.
  • Avec REQUIRE_API_KEY=true, une clé manquante arrête l’application avec Settings error: ... et le code de sortie 1. Server started apparaît avant ce message.

Ensuite, vous ajoutez une base de données qui conserve l’historique des classifications. Vous exécutez l’API et la base de données ensemble avec Docker Compose.

Leçon suivante : Données persistantes