Construire des Images Docker dans les Pipelines CI/CD

Vous avez un Dockerfile qui fonctionne parfaitement sur votre machine. Vous lancez docker build, l'image se construit, et votre application tourne. Mais quand vous poussez ce même Dockerfile dans un pipeline, les choses commencent à se comporter différemment.

La construction qui prenait deux minutes sur votre machine en prend maintenant quinze. L'image qui fonctionnait hier échoue aujourd'hui sans raison apparente. Et quand vous devez reconstruire une ancienne version pour un rollback, l'image obtenue se comporte différemment de l'originale.

Ces problèmes ne sont pas aléatoires. Ils viennent du fossé entre la construction d'images sur une machine de développeur et leur construction dans un pipeline automatisé. Comprendre ce fossé est la première étape pour le résoudre.

Ce Qui Change Quand Vous Passez à un Pipeline

Quand vous construisez une image dans un pipeline, trois choses changent fondamentalement.

Premièrement, la construction doit s'exécuter sur la machine de quelqu'un d'autre. Cette machine peut avoir des ressources, des conditions réseau et des systèmes de fichiers différents. Votre Dockerfile doit fonctionner indépendamment de l'endroit où il est exécuté.

Deuxièmement, le pipeline doit reconstruire l'image à chaque changement de code. Ce n'est pas optionnel. Si vous sautez les reconstructions, votre déploiement utilisera un code obsolète. Mais reconstruire à chaque fois signifie que chaque commit déclenche le processus complet de construction, et ce processus doit être assez rapide pour ne pas ralentir le développement.

Troisièmement, la construction doit être reproductible. Le même code source doit produire la même image, indépendamment du moment ou du lieu où la construction est exécutée. Sans reproductibilité, vous ne pouvez pas avoir confiance dans le fait que revenir à un ancien commit restaurera exactement le même comportement de l'application.

Le diagramme ci-dessous montre les étapes typiques d'un pipeline de construction Docker, du code source au push vers le registre.

flowchart TD A[Code Source] --> B[Checkout] B --> C[Définir le Contexte de Build] C --> D[Build Docker] D --> E{Cache Hit ?} E -- Oui --> F[Réutiliser les Couches en Cache] E -- Non --> G[Construire Nouvelles Couches] F --> H[Taguer l'Image] G --> H H --> I[Push vers le Registre]

Maîtrisez Votre Contexte de Build

Le contexte de build est le dossier que vous envoyez au démon Docker quand vous lancez docker build. Sur votre machine, c'est généralement votre dossier de projet. Dans un pipeline, la même chose se produit, mais les conséquences d'un contexte volumineux sont pires.

Chaque fichier dans le contexte de build est envoyé au démon Docker. Si votre dépôt contient des node_modules, des environnements virtuels Python, ou des binaires compilés, ces fichiers sont transférés même s'ils ne sont pas nécessaires à la construction. Cela ralentit chaque exécution du pipeline.

La solution est un fichier .dockerignore. Il fonctionne comme .gitignore mais pour les builds Docker. Listez tout ce qui n'est pas nécessaire pour l'image : dossiers de dépendances, répertoires de cache, historique .git, fixtures de test et documentation. Un contexte de build léger signifie des constructions plus rapides et moins de trafic réseau.

Faites Fonctionner le Cache pour Vous

Docker construit les images en couches. Chaque instruction dans votre Dockerfile crée une couche. Lors d'une reconstruction, Docker vérifie si chaque couche a changé. Si une couche est inchangée, Docker réutilise le résultat en cache de la construction précédente.

Ce mécanisme de cache est votre meilleur allié pour des constructions rapides. Mais il ne fonctionne que si le cache survit entre les exécutions du pipeline.

En développement local, le cache vit sur votre machine. Dans un pipeline, le cache disparaît quand le runner se termine, sauf si vous le sauvegardez explicitement. Certains systèmes CI fournissent une mise en cache intégrée des couches Docker. Si ce n'est pas le cas, vous devez la configurer manuellement ou accepter que chaque construction parte de zéro.

Même avec un cache fonctionnel, l'ordre des instructions dans votre Dockerfile détermine la quantité de cache que vous utilisez réellement. La règle d'or est : copiez d'abord les éléments qui changent le moins souvent.

Pour une application Node.js, cela signifie copier package.json et package-lock.json avant le reste du code source. Exécutez npm install juste après avoir copié ces fichiers. Copiez ensuite le code de l'application. Avec cet ordre, l'installation des dépendances ne se relance que lorsque vos dépendances changent réellement, pas quand vous modifiez une seule ligne de code applicatif.

Le même principe s'applique à n'importe quel langage. Les projets Python doivent copier requirements.txt ou pyproject.toml en premier. Les projets Go doivent copier go.mod et go.sum en premier. Le modèle est universel : séparez les dépendances stables du code applicatif changeant.

Utilisez des Arguments de Build pour la Flexibilité

Votre Dockerfile ne doit pas contenir en dur des valeurs qui changent entre les environnements. La version d'une image de base, le nom de l'environnement, ou un jeton d'accès pour un registre privé doivent venir de l'extérieur du Dockerfile.

Docker fournit ARG à cet effet. Vous définissez un espace réservé dans votre Dockerfile, et le pipeline le remplit pendant la construction.

ARG BASE_IMAGE_VERSION=20.04
FROM ubuntu:${BASE_IMAGE_VERSION}

Dans votre pipeline, vous passez la valeur réelle :

docker build --build-arg BASE_IMAGE_VERSION=22.04 .

Cela rend votre Dockerfile générique et réutilisable. Un seul Dockerfile peut servir pour les builds de développement, de staging et de production sans duplication.

Assurez des Constructions Reproductibles

La reproductibilité signifie que construire le même code source à des moments différents produit la même image. Sans cela, vous ne pouvez pas faire confiance à votre stratégie de rollback, à votre piste d'audit, ou à votre scan de sécurité.

Trois éléments compromettent couramment la reproductibilité.

Premièrement, utiliser les tags latest pour les images de base. Le tag latest change avec le temps. Le latest d'aujourd'hui n'est pas celui de demain. Ancrez votre image de base sur un tag de version spécifique comme ubuntu:22.04 ou node:20-alpine.

Deuxièmement, ne pas verrouiller les versions des dépendances. Votre package.json peut spécifier une plage de versions comme ^4.0.0. Cette plage se résout en différentes versions au fil du temps. Utilisez des fichiers de verrouillage comme package-lock.json ou yarn.lock pour geler les versions exactes.

Troisièmement, intégrer des horodatages de construction ou des métadonnées de build dans l'image. Si votre processus de construction écrit la date courante dans un fichier à l'intérieur de l'image, ce fichier différera entre les constructions même lorsque le code source est identique. Évitez cela sauf si vous avez une raison opérationnelle spécifique.

Stockez l'Image dans un Registre

Une fois que le pipeline a construit l'image, elle a besoin d'un endroit où vivre pour que les serveurs et les clusters Kubernetes puissent la récupérer. Cet endroit est un registre de conteneurs.

Votre pipeline doit taguer l'image avec des identifiants significatifs. Un modèle courant consiste à utiliser le SHA du commit comme tag principal, avec des tags supplémentaires pour les branches ou les versions sémantiques.

docker tag myapp:${COMMIT_SHA} registry.example.com/myapp:${COMMIT_SHA}
docker push registry.example.com/myapp:${COMMIT_SHA}

Cela vous donne une référence permanente à chaque image jamais construite. Vous pouvez toujours revenir à n'importe quel commit et récupérer l'image exacte qui a été construite à partir de celui-ci.

Liste de Vérification Pratique

Avant de pousser votre build Docker dans un pipeline, vérifiez ces éléments :

  • .dockerignore exclut les dossiers de dépendances, le cache et .git
  • Le Dockerfile copie les fichiers de dépendances avant le code applicatif
  • Les tags des images de base sont ancrés sur des versions spécifiques, pas latest
  • Les versions des dépendances sont verrouillées avec des fichiers de verrouillage
  • Les arguments de build sont utilisés pour les valeurs spécifiques à l'environnement
  • Le pipeline sauvegarde le cache des couches Docker entre les exécutions
  • Les images sont taguées avec le SHA du commit et poussées vers un registre

Ce Que Cela Signifie pour Votre Pipeline

Construire des images dans un pipeline ne consiste pas seulement à exécuter docker build sur une machine différente. Il s'agit de concevoir votre Dockerfile et votre pipeline pour qu'ils fonctionnent ensemble. Un Dockerfile bien structuré qui respecte l'ordre des couches et le contexte de build se construira plus rapidement. Un pipeline qui préserve le cache et tague correctement les images vous donnera des artefacts fiables et reproductibles.

L'image que vous construisez aujourd'hui devrait être la même image que vous pouvez reconstruire six mois plus tard à partir du même commit. Cette cohérence est ce qui rend les déploiements prévisibles et les rollbacks sûrs.