CodeGym /Cours /Docker for Spring /Image immuable et configuration runtime

Image immuable et configuration runtime

Docker for Spring
Niveau 11, Leçon 0
Disponible

1. La tentation de reconstruire l'image pour chaque mode

Il y a une raison très humaine pour laquelle les débutants commencent à multiplier les images pour différents modes. Quand quelque chose ne fonctionne pas, on veut « figer » l'état de fonctionnement pour toujours : « Voici l'image pour standalone, voici l'image pour postgres, voici l'image pour… oups, déjà la troisième ». Cela semble logique : puisque le mode est différent, l'image doit être différente. Mais c'est comme traiter un rhume en achetant un nouveau thermomètre : le symptôme change, mais la cause demeure.

Le problème est qu'une image Docker est un artefact qui doit vivre longtemps et être lancée plusieurs fois. Si vous commencez à créer des images séparées pour chaque petit réglage, vous tombez presque instantanément dans le « zoo des tags » : catalog-standalone, catalog-postgres, catalog-postgres-new-try-final2. À ce moment-là, vous perdez le principal avantage de la conteneurisation : la reproductibilité. Parce que la reproductibilité, ce n'est pas « ça marche chez moi », mais « toute l'équipe lance la même chose et comprend en quoi elle diffère uniquement par les paramètres de lancement ».

Jetons un coup d'œil un instant à un scénario typique « incorrect ». Vous voulez changer le port — et au lieu de passer une autre valeur au lancement du conteneur, vous modifiez le Dockerfile, reconstruisez l'image et l'appelez « 8081 ». Ensuite, vous voulez activer un autre mode de stockage — encore une reconstruction. Ensuite, vous voulez un autre niveau de journalisation — encore une reconstruction. Au final, le Dockerfile cesse d'être une instruction de construction et devient une collection d'« incantations chamaniques », où chaque incantation semble fonctionner, mais le prix est le chaos.

2. Image immuable : un artefact, plusieurs exécutions

Le mot immutable donne l'impression que nous allons partir en philosophie, mais en réalité c'est une idée très terre-à-terre : l'image construite ne change pas « au gré des humeurs ». À l'intérieur se trouvent le code de l'application (notre jar Spring Boot) et des valeurs par défaut raisonnables. C'est tout. Vous pouvez lancer cette image 1 fois, 10 fois ou 100 fois, et elle reste la même à chaque fois — comme la même clé USB avec un programme que vous branchez simplement dans différents ordinateurs.

Pour garder cela en tête plus facilement, il est utile de séparer deux événements : build et run. Build — c'est quand vous créez l'artefact. Run — c'est quand vous lancez cet artefact dans des conditions spécifiques. Si vous reconstruisez l'artefact à chaque fois selon les conditions, vous mélangez ces événements, et avec eux vous mélangez la responsabilité. Et ensuite vous vous étonnez que « ça marche sur ma machine, mais pas chez mon collègue » — parce que vous n'avez plus un artefact, mais plusieurs similaires, et ils diffèrent plus que vous ne le pensez.

Voici un schéma simple de ce que nous voulons avoir en tête :

flowchart LR
  A["Dockerfile + sources/artefact"] -->|docker build| B["Image (immuable)"]
  B -->|docker run + runtime config| C["Conteneur"]
  C --> D["Spring Boot démarre et assemble les valeurs finales des propriétés"]

Remarquez la pensée « subtile » : le Dockerfile est un moyen de créer une image, et non une « liste de différentes exécutions ». Les exécutions doivent différer par les valeurs que nous avons fournies au démarrage, et non par le nombre de fois que nous avons réécrit le Dockerfile en essayant de « convaincre » l'application de se comporter différemment.

3. Configuration runtime : modifier les valeurs d'entrée

Quand nous parlons de runtime configuration, nous entendons les valeurs qui arrivent au moment du lancement de l'application. Cela peut être le port, le profil actif, l'adresse de la base de données, les indicateurs de comportement (par exemple, « read-only »), le chemin du répertoire d'export — tout ce qui dépend de l'environnement, et non des sources. Et dans le monde des conteneurs, c'est particulièrement important, car le conteneur est justement créé pour l'idée : « une image — plusieurs exécutions ».

Pour voir cela sans bruit superflu, nous allons utiliser une simple clé app.mode. C'est un indicateur pédagogique : il est pratique pour analyser l'externalized config sans mélanger immédiatement tout avec un modèle de configuration plus riche de l'application.

Il est très important d'accepter une formule pratique que nous allons répéter comme un mantra (et oui, les mantras existent aussi en IT) : le code change → reconstruction nécessaire ; la configuration change → reconstruction inutile. Ce n'est pas une religion ni une « best practice pour la best practice ». C'est simplement une économie de temps et de nerfs, et, plus important encore, une économie de confiance dans le système. Si chaque éternuement de configuration crée une nouvelle image, personne dans l'équipe n'est plus sûr de quel code est en train de fonctionner.

Spring Boot est notre allié par défaut. Il est conçu dès le départ pour que la configuration puisse venir de l'extérieur : des fichiers, des paramètres de lancement, de l'environnement. Il y a une chose importante ici : l'application ne se soucie pas d'où vient la valeur ; ce qui compte pour elle, c'est la valeur finale obtenue.

4. Ce qui est à l'intérieur de l'image, ce qui est à l'extérieur

Quand vous entendez pour la première fois « image immuable », la question naturelle suivante est : « Alors qu'est-ce qu'on peut “cuire” à l'intérieur de l'image ? ». La réponse est simple : tout ce qui concerne le code et les valeurs par défaut sûres. Tout ce qui concerne l'environnement et le mode d'exécution doit venir de l'extérieur. Sinon, vous obtenez une « image qui en sait trop », ce qui est presque toujours une mauvaise idée.

Ci-dessous un tableau pratique que vous pouvez garder comme antisèche quand la main tend à modifier le Dockerfile pour un nouveau port.

Où cela se rapporte Exemples Pourquoi ainsi
À l'intérieur de l'image (partie immuable) jar de l'application, dépendances, structure de base des fichiers, paramètres par défaut sûrs pour tous C'est la partie « produit ». Si cela change, l'artefact change, donc un nouveau image est nécessaire.
Dans la configuration runtime (partie mutable) port, profil actif, URL de la base, user/password, indicateurs de comportement (read-only), niveau de journalisation, « activer/désactiver » l'intégration C'est la partie « environnement ». Doit changer sans reconstruction : sinon vous transformez la construction en rituel quotidien.

Un autre petit tableau (mais très utile) — sur les cas où une reconstruction est vraiment justifiée.

Ce qui a changé Reconstruction de l'image nécessaire ? Critère simple
Vous avez modifié le code Java (contrôleur, service, repository) Oui Le jar change — donc le contenu de l'image change
Vous avez modifié les dépendances (Gradle) Oui Le classpath runtime change — un nouvel artefact nécessaire
Vous avez changé le port, le profil, l'URL de la base, le flag read-only Non Ce sont des paramètres d'entrée de lancement, pas une « nouvelle version de l'application »
Vous avez modifié le Dockerfile parce que « je veux un autre mode » Presque toujours non Si seul le mode change — le problème n'est pas dans le Dockerfile, mais dans la configuration de lancement

5. Endpoints runtime pour un instantané de la configuration

Pour que ce principe ne reste pas une abstraction, nous avons besoin d'une sonde de diagnostic temporaire — une petite interface qui affichera les valeurs finales après le démarrage. Ne transportez pas tous les fragments ci-dessous dans le projet comme des contrôleurs de production séparés : ce sont des mini-illustrations indépendantes. Généralement, un seul endpoint simple suffit, dans lequel vous ajoutez tour à tour la propriété souhaitée.

Pour cela, il est pratique d'utiliser @Value — une annotation qui injecte dans le champ la valeur finale de la propriété. L'idée clé ici : le code du contrôleur ne sait pas de quelle source provient la valeur. Il reçoit simplement le résultat de la configuration avec lequel l'application a démarré. C'est parfait pour démontrer l'externalized configuration : le même code, mais des valeurs différentes au lancement.

Ajoutons les propriétés de base dans application.yml

Pour que les exemples soient reproductibles, définissons des valeurs par défaut raisonnables. Dans un projet réel, une partie des valeurs par défaut existe déjà, mais aujourd'hui il est important pour nous de voir le mécanisme sur des clés simples.

server:
  port: 8080 # port par défaut si rien n'a été passé au lancement

app:
  mode: standalone # mode par défaut, sûr pour un démarrage local

catalog:
  read-only: false # défaut : fonctionnement en mode normal, pas en "lecture seule"

Fragment pour vérifier app.mode

// Ce fragment peut être ajouté dans n'importe quel contrôleur de diagnostic temporaire
@Value("${app.mode}")
String mode;

@GetMapping("/api/runtime/mode")
String mode() {
    return mode;
}

Si vous avez lancé le service avec les valeurs par défaut, une requête à GET /api/runtime/mode retournera standalone. Et encore une fois : le code ne se soucie pas de savoir si cette valeur était dans le fichier, si elle est venue comme paramètre de lancement ou d'une autre manière. Il montre simplement le résultat.

Fragment pour le flag catalog.read-only avec valeur par défaut

// Si la clé n'a pas été fournie, on prend le défaut sûr
@Value("${catalog.read-only:false}")
boolean readOnly;

@GetMapping("/api/runtime/read-only")
boolean readOnly() {
    return readOnly;
}

Cet exemple est utile car il montre : un indicateur de comportement est un candidat idéal pour la configuration runtime. Dans un lancement, vous voulez un mode read-only (par exemple pour le diagnostic ou une « visualisation » sécurisée), dans un autre, le mode complet. Modifier le Dockerfile et reconstruire l'image pour cela est une manière trop coûteuse de dire « ici true, et ici false ».

La même chose pour le profil

De la même manière, on peut aussi consulter le profil : lire à l'intérieur de l'application spring.profiles.active et retourner la valeur finale via la même sonde temporaire. Si le profil n'est pas explicitement défini, il est pratique de renvoyer default pour que la réponse soit prévisible.

Ici, nous avons besoin d'un fait pratique : le profil, comme toute autre clé runtime, arrive au lancement et ne transforme pas le mode en une image séparée. Dans les premiers exemples, nous n'en faisons pas l'interrupteur principal du mode : maintenant, c'est le mécanisme lui-même de l'externalized config qui est important.

6. Valeurs par défaut et ${key:default}

L'écriture ${key:default} a un effet étonnamment agréable : elle rend le comportement du système prévisible même lorsque vous oubliez de passer un réglage. Pour les débutants, c'est particulièrement important, car pendant le premier mois d'apprentissage de Docker (et Spring Boot aussi), vous oublierez de « passer un petit drapeau ». Et il vaut mieux que le service démarre avec une valeur par défaut sûre plutôt que de planter, vous laissant seul avec un stack-trace rouge sur un demi-écran.

Mais il est important de ne pas exagérer. Une valeur par défaut, c'est comme une roue de secours : utile tant qu'elle est « au cas où », et très étrange si vous roulez constamment dessus parce que vous avez la flemme de réparer les roues principales.

Certaines valeurs doivent être obligatoires, sinon l'application n'a pas le droit de faire semblant que « tout va bien ». Par exemple, si le mode nécessite réellement une connexion à la base de données, alors un « mot de passe de base par défaut » dans le code est une mauvaise idée.

Type de réglage Défaut approprié ? Exemple
Drapeau de comportement pratique Oui catalog.read-only:false
Valeur affectant la sécurité/correction Plutôt non mot de passe de la base, URL d'un service externe

Et encore un petit détail qui surprend souvent les débutants : si vous utilisez @Value("${app.mode}") et que la propriété n'est définie nulle part, l'application risque de ne pas démarrer parce que Spring n'a pas pu substituer la valeur. Ce n'est pas un « caprice de Spring », c'est votre protection contre la situation où « l'application a démarré mais ne fonctionne pas comme prévu, et vous ne savez même pas pourquoi ».

7. Build-time et run-time : ARG vs lancement

Dans les jours précédents, nous avons déjà vu qu'il existe différents outils dans le Dockerfile : ARG, ENV, ENTRYPOINT, CMD. Aujourd'hui, il est important de lier cela au principe de l'image immuable. L'erreur la plus fréquente ici est d'essayer d'utiliser ARG comme un « réglage normal de l'application », puis de s'étonner que pour changer de mode, un docker build soit nécessaire. Spoiler : parce que ARG vit à l'étape de construction, et ce n'est pas un bug, c'est voulu.

Voici un exemple d'approche « quasi fonctionnelle » mais méthodologiquement dangereuse. Elle est tentante : en apparence, il y a une config, mais elle est build-time.

# ARG n'existe qu'à l'étape de construction de l'image (build-time)
ARG APP_MODE=standalone

# Nous transférons la valeur d'ARG dans ENV, mais ENV est finalement "cuit" dans l'image
# (et donc changer de mode pousse à nouveau vers une reconstruction)
ENV APP_MODE=${APP_MODE}

Avec un tel Dockerfile, le changement de mode se transforme en reconstruction :

# Changement de mode via build-arg -> donc un docker build nécessaire à chaque fois
docker build --build-arg APP_MODE=postgres -t catalog:postgres .

Techniquement, c'est possible. Mais c'est exactement le piège dont nous nous éloignons aujourd'hui : le mode de fonctionnement est devenu une partie de la construction, alors qu'il devrait être une partie du lancement.

Le tableau correct devrait être différent. Nous laissons dans l'image une valeur par défaut (par exemple standalone), et nous passons le « commutateur » de mode au démarrage. Ici, il suffit de voir le principe lui-même : la clé peut être transmise au démarrage sans modifier l'image. Le canal spécifique de livraison de la valeur est ensuite une question de commodité et de priorité des sources.

Par exemple, un même image peut être lancé sur différents ports sans reconstruction, simplement en modifiant les arguments de lancement :

# Même image, ports différents et valeurs différentes de server.port
docker run --rm -p 8080:8080 docker-java-catalog-service --server.port=8080
docker run --rm -p 8081:8081 docker-java-catalog-service --server.port=8081

Une seule image. Un seul code. Seules les valeurs d'entrée au démarrage diffèrent — et c'est exactement ce que nous voulons.

Ici, un « débat interne » surgit souvent : « Mais on peut aussi changer ENV, non ? » Oui, on peut définir des variables d'environnement au lancement du conteneur. Mais le point clé reste le même : il faut changer au lancement, pas à la construction. ENV dans le Dockerfile convient pour des valeurs par défaut sûres, mais pas pour des « environnements différents ». Sinon, vous transférez simplement le chaos du README dans le Dockerfile, puis vous appelez fièrement cela « conteneurisation ».

8. Erreurs typiques lors de la configuration runtime

Erreur n°1 : multiplier les images par mode — catalog-standalone, catalog-postgres, catalog-postgres-final-final.
Cette approche semble d'abord pratique car « chaque image se lance sans paramètres ». Mais très vite, vous ne comprenez plus ce qu'il y a à l'intérieur de chaque image. Quelque part un jar différent, quelque part une configuration différente, quelque part des fichiers supplémentaires se sont glissés par hasard. Il est plus correct de garder une seule image et de changer uniquement les valeurs runtime au lancement.

Erreur n°2 : cuire les modes et les environnements dans le Dockerfile via ENV, transformant le Dockerfile en fichier de configuration.
ENV dans le Dockerfile fait partie de l'image. Si vous y stockez l'adresse de la base de données, les identifiants, les mots de passe ou le « mode postgres », vous rendez l'image « trop savante ». Ensuite, quelqu'un pousse cette image dans le registre, et soudainement la configuration commence à vivre séparément de la réalité. Le Dockerfile doit décrire l'empaquetage de l'application, et non remplacer la configuration externe.

Erreur n°3 : utiliser ARG comme mécanisme principal pour changer le comportement de l'application.
ARG est build-time. Il est utile quand vous devez gérer la construction (par exemple, choisir une version de quelque chose à l'étape build), mais pour changer quotidiennement le port, le mode, le profil et autres choses similaires, il conduit à des reconstructions qui ne sont pas du tout nécessaires. Si vous vous surprenez à changer --build-arg plus souvent que vous ne changez les sources, c'est presque toujours le signe que vous avez confondu « build » et « run ».

Erreur n°4 : s'attendre à ce que l'application « comprenne toute seule » si une propriété n'est pas définie.
Quand une propriété est vraiment critique, il vaut mieux que le service plante au démarrage avec une erreur compréhensible plutôt que de démarrer dans un état indéterminé. Et quand la propriété n'est pas critique (comme le flag read-only), il vaut mieux définir explicitement une valeur par défaut via ${key:default}. Le juste milieu entre ces approches ressemble généralement à « ça marche parfois, parfois non », et c'est le mode de développement le plus coûteux.

Erreur n°5 : débattre sur la source de la valeur au lieu de regarder la valeur finale.
Les débutants restent souvent bloqués sur la question « elle vient du fichier ou du lancement ? ». Dans la pratique, il est plus important d'apprendre à poser une autre question : « quelle valeur a été obtenue après le démarrage ? ». C'est pourquoi nous avons ajouté de simples endpoints runtime : ils permettent de voir le résultat de la configuration en deux secondes et d'arrêter de deviner.

1
Mission
Docker for Spring, niveau 11, leçon 0
Bloqué
`app.mode` comme paramètre d'exécution
`app.mode` comme paramètre d'exécution
1
Mission
Docker for Spring, niveau 11, leçon 0
Bloqué
Drapeau `catalog.read-only` sans nouveau build d’image
Drapeau `catalog.read-only` sans nouveau build d’image
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION