Votre pipeline CI/CD build des images, mais vous ne savez jamais exactement quelle version tourne en prod ? Ce guide vous donne les fondamentaux pour industrialiser vos builds d'images : stratégie de tags, digests immuables, cache efficace, multi-architecture et promotion entre environnements. Les bases pour une CI/CD d'images fiable et traçable.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Promotion sans rebuild : garantir que prod = ce qui a été testé
- Stratégie de tags : latest, semver, git SHA, quand utiliser quoi
- Tag vs Digest : pourquoi le tag peut changer et comment pinner
- Cache en CI : accélérer les builds avec BuildKit
- Multi-arch : produire des images AMD64 et ARM64 efficacement
Tag vs Digest : la différence cruciale
Section intitulée « Tag vs Digest : la différence cruciale »Un tag est un alias mutable qui pointe vers une image. Le même tag peut pointer vers des images différentes au fil du temps.
Un digest est un hash SHA256 immutable qui identifie une image de manière unique. Il ne change jamais.
# Tag : peut changer !docker pull nginx:1.25
# Digest : toujours la même imagedocker pull nginx@sha256:6926dd802f40e5e7257fded83e0d8030039642e4e10c4a98a6478e9c6fe06153| Aspect | Tag | Digest |
|---|---|---|
| Format | image:tag | image@sha256:abc123... |
| Mutable | ✅ Oui | ❌ Non |
| Lisible | ✅ nginx:1.25 | ❌ Hash de 64 caractères |
| Reproductible | ❌ Non garanti | ✅ Toujours identique |
| Audit | Difficile | Facile |
Règle pratique :
- Développement : tags semver (
1.2.3) ou branch (main) - Production : digest ou tag semver strict
- Audit/compliance : toujours le digest
Stratégie de tags recommandée
Section intitulée « Stratégie de tags recommandée »Une bonne stratégie de tags permet de tracer quelle version du code produit quelle image.
Les tags à générer en CI
Section intitulée « Les tags à générer en CI »| Tag | Quand | Exemple | Usage |
|---|---|---|---|
| Git SHA | Toujours | abc1234 | Traçabilité exacte |
| Semver | Release | 1.2.3 | Production |
| latest | Merge main | latest | Dev/test uniquement |
| PR | Pull requests | pr-123 | Tests d'intégration |
Exemple : metadata-action (GitHub Actions)
Section intitulée « Exemple : metadata-action (GitHub Actions) »L'action docker/metadata-action calcule la liste des tags à partir de
l'événement déclencheur, ce qui évite d'écrire des conditions if partout.
Chaque ligne type= produit zéro ou plusieurs tags selon le contexte : sur une
pull request, seule la ligne type=ref,event=pr s'applique, les lignes
type=semver restent silencieuses. Deux détails comptent. pattern={{version}}
retire le v initial du tag Git, donc v1.2.3 devient l'image 1.2.3. Et le
enable= de la ligne latest est la garde qui empêche une branche de
maintenance de réécrire ce tag. La sortie utile de l'étape est
steps.meta.outputs.tags, que l'on passe ensuite à l'étape de build.
- name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ghcr.io/${{ github.repository }} tags: | # Sur push de tag v* → tag semver type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} # Sur push sur main → SHA court + latest type=sha,prefix= type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} # Sur PR → pr-123 type=ref,event=prRésultat pour un push de tag v1.2.3 :
ghcr.io/mon-org/mon-app:1.2.3ghcr.io/mon-org/mon-app:1.2ghcr.io/mon-org/mon-app:abc1234
Cache en CI : accélérer les builds
Section intitulée « Cache en CI : accélérer les builds »Sans cache, chaque build repart de zéro. Avec le cache BuildKit, seules les couches modifiées sont reconstruites.
Option 1 : Cache registry (partagé)
Section intitulée « Option 1 : Cache registry (partagé) »Le cache est publié dans le registry sous une image distincte, ici le tag
buildcache. C'est le seul mode qui survit au recyclage des runners et qui se
partage entre plusieurs jobs, y compris sur des forges différentes. À ne pas
confondre avec le cache inline (type=inline), qui embarque les
métadonnées de cache dans l'image publiée : celui-ci ne supporte pas
mode=max, donc il ne conserve pas les couches intermédiaires d'un build
multi-stage, là où l'essentiel du temps de build se joue.
- name: Build and push uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} cache-from: type=registry,ref=ghcr.io/mon-org/mon-app:buildcache cache-to: type=registry,ref=ghcr.io/mon-org/mon-app:buildcache,mode=maxOption 2 : Cache GitHub Actions (rapide)
Section intitulée « Option 2 : Cache GitHub Actions (rapide) »Utilise le cache GitHub Actions, plus rapide car local au runner.
- name: Build and push uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max| Type de cache | Avantages | Inconvénients |
|---|---|---|
| Registry | Partagé entre runners, persistant | Plus lent (réseau) |
| GHA | Rapide, local | 10 Go par défaut (extensible selon plan) |
| Local | Le plus rapide | Non partagé entre runners |
Multi-arch : AMD64 et ARM64 en un seul build
Section intitulée « Multi-arch : AMD64 et ARM64 en un seul build »Les serveurs modernes utilisent différentes architectures : AMD64 (Intel/AMD) et ARM64 (Graviton AWS, Apple Silicon, Raspberry Pi). Une image multi-arch fonctionne sur les deux.
Avec docker buildx (local)
Section intitulée « Avec docker buildx (local) »Un build multi-plateforme exige un builder dédié : le builder par défaut de
Docker Desktop ne gère qu'une architecture. La commande docker buildx create
en fabrique un, basé sur le pilote docker-container, qui sait produire
plusieurs plateformes dans le même appel. L'option --push n'est pas un
raccourci de confort : sans elle ni --load, le pilote docker-container
n'exporte rien et vous laisse simplement un avertissement, le résultat restant
dans le cache de build. Sur une machine amd64, la variante arm64 passe
par une émulation QEMU nettement plus lente que le build natif, ce qui rend
cette commande acceptable pour une vérification ponctuelle mais pas pour une
chaîne de production.
# Créer un builder multi-plateformedocker buildx create --name multiarch --use
# Build et push pour AMD64 et ARM64docker buildx build \ --platform linux/amd64,linux/arm64 \ -t ghcr.io/mon-org/mon-app:1.0.0 \ --push \ .En CI (GitHub Actions)
Section intitulée « En CI (GitHub Actions) »- name: Set up QEMU uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Build and push uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }}Comment ça marche ?
Docker crée un manifest list (ou OCI image index) qui contient les références vers les images de chaque architecture. Quand vous faites docker pull, le client télécharge automatiquement la bonne architecture.
# Voir les architectures disponiblesdocker manifest inspect ghcr.io/mon-org/mon-app:1.0.0Promotion : dev → staging → prod
Section intitulée « Promotion : dev → staging → prod »La promotion consiste à faire passer une image déjà buildée d'un environnement à l'autre, sans la reconstruire.
Pourquoi promouvoir plutôt que rebuilder ?
Section intitulée « Pourquoi promouvoir plutôt que rebuilder ? »Reconstruire l'image à chaque environnement paraît anodin puisque le
Dockerfile et le commit sont identiques. Le résultat, lui, ne l'est pas :
entre deux builds, une image de base a pu être republiée sous le même tag, un
miroir de paquets a pu avancer, une dépendance non verrouillée a pu sortir une
version. L'artefact déployé en production n'est alors plus celui qui a
traversé les tests, et le digest le prouve puisqu'il diffère. La promotion
supprime la question : un seul build, un seul digest, du premier test au
déploiement.
| Approche | Risque |
|---|---|
| Rebuild pour chaque env | L'image prod peut différer de celle testée |
| Promouvoir la même image | ✅ Garantie que prod = ce qui a été testé |
Comment promouvoir
Section intitulée « Comment promouvoir »Promouvoir revient à ajouter un tag sur un manifeste déjà présent dans le registry. Les trois options ci-dessous font la même chose ; elles se distinguent par ce qu'elles exigent de l'environnement d'exécution. Skopeo et Crane travaillent en serveur à serveur, sans démon Docker et sans télécharger les couches, ce qui les rend rapides même sur des images de plusieurs gigaoctets. Le troisième exemple est un workflow GitHub Actions qui encadre l'opération par une validation humaine : c'est le déclenchement manuel, pas l'outil, qui apporte la valeur ici.
Option 1 : Re-tagger avec Skopeo (sans pull/push local)
# Copier l'image de staging vers prod sans la téléchargerskopeo copy \ docker://ghcr.io/mon-org/mon-app:staging \ docker://ghcr.io/mon-org/mon-app:prodOption 2 : Re-tagger avec Crane
# Copier avec crane (Google)crane copy ghcr.io/mon-org/mon-app:staging ghcr.io/mon-org/mon-app:prodOption 3 : En CI avec docker/build-push-action (sans rebuild)
# Promotion manuelle via workflow_dispatchon: workflow_dispatch: inputs: source_tag: description: 'Tag à promouvoir' required: true target_env: description: 'Environnement cible' required: true type: choice options: [staging, prod]
permissions: {}
jobs: promote: runs-on: ubuntu-latest permissions: packages: write steps: - uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Promote image env: SOURCE_TAG: ${{ inputs.source_tag }} TARGET_ENV: ${{ inputs.target_env }} REPO: ${{ github.repository }} run: | skopeo copy \ "docker://ghcr.io/${REPO}:${SOURCE_TAG}" \ "docker://ghcr.io/${REPO}:${TARGET_ENV}"Deux points de durcissement dans ce workflow. permissions: {} au niveau du
fichier coupe tous les jetons par défaut, et le job ne récupère que
packages: write, strictement ce qu'exige le push vers GHCR. Surtout, la
valeur source_tag saisie par l'opérateur n'est jamais interpolée dans le
script shell : elle transite par une variable d'environnement. Une expression
${{ inputs.… }} écrite directement dans un run: est substituée avant
l'exécution du shell, ce qui laisse une injection de commande possible ;
c'est le défaut que signalent les analyseurs de pipelines comme zizmor.
Workflow de promotion typique
Section intitulée « Workflow de promotion typique »1. Merge PR → build image:sha-abc1234, image:main2. Tests auto → si OK, tag image:staging3. Tests staging → si OK, promotion manuelle vers image:prod4. Release → tag image:1.2.3 sur le digest de prodAuthentification registry
Section intitulée « Authentification registry »Pour push des images, vous devez vous authentifier auprès du registry.
- uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}- uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }}- uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2.1.6 with: registry-type: privateDépannage
Section intitulée « Dépannage »Les pannes de CI/CD d'images tombent presque toutes dans deux familles. Soit le
cache ne fait pas son travail, et le build reste lent sans erreur visible :
la cause est alors un ordre d'instructions dans le Dockerfile qui invalide le
cache trop tôt, ou une image de cache absente au premier passage. Soit
l'opération échoue franchement, et le message pointe vers une permission de
jeton ou une architecture manquante. Lisez la colonne « cause probable » avant
la solution : le même symptôme, un build lent, peut venir de deux causes
opposées.
| Problème | Cause probable | Solution |
|---|---|---|
| Build lent malgré le cache | Cache invalide (layers changés) | Vérifier l'ordre des instructions Dockerfile, mettre les dépendances avant le code |
cache-from ne fonctionne pas | Image cache n'existe pas encore | Premier build sans cache, puis les suivants l'utilisent |
| Multi-arch échoue sur ARM | QEMU non configuré | Ajouter docker/setup-qemu-action avant buildx |
denied: permission denied | Token sans packages:write | Vérifier les permissions GitHub Actions |
| Digest différent après rebuild | Métadonnées variables (date, run ID) ou dépendances non pinnées | Fixer les LABEL, utiliser lockfiles (package-lock.json, requirements.txt) |
| Image corrompue après promotion | Registry intermédiaire incompatible | Utiliser skopeo copy --all pour copier toutes les architectures |
Bonnes pratiques
Section intitulée « Bonnes pratiques »Sécurité
Section intitulée « Sécurité »La chaîne de build d'images est une cible privilégiée des attaques de supply chain : qui contrôle le pipeline contrôle ce qui part en production. Les trois premières règles ci-dessous ferment les portes d'entrée les plus courantes, un tag d'action réécrit, un jeton trop large, une image vulnérable poussée sans contrôle. La dernière, les attestations SLSA, ajoute une preuve vérifiable de l'origine du build, utile le jour où il faut démontrer qu'une image donnée provient bien de votre pipeline et non d'un poste compromis.
- Pinner les actions GitHub par SHA (pas par tag) :
uses: docker/build-push-action@263435... - Utiliser GITHUB_TOKEN plutôt que des PAT pour GHCR
- Scanner les images avec Trivy avant le push en prod
- Attestations SLSA : générer la provenance pour prouver l'origine du build (voir guide supply chain)
Performance
Section intitulée « Performance »Le temps de build se joue à l'écriture du Dockerfile, pas dans la
configuration du runner. Le principe qui gouverne les trois conseils suivants
est l'invalidation de cache par couche : BuildKit réutilise une couche tant
que ni son instruction ni les fichiers qu'elle copie n'ont changé. En plaçant
l'installation des dépendances avant la copie du code, vous évitez de
réinstaller tout l'arbre à chaque modification d'une ligne de source. Les
builds multi-stage attaquent l'autre bout du problème, la taille de l'image
finale, en laissant les outils de compilation dans une étape jetable.
- Dockerfile : copier
package.json/requirements.txtavant le code source - Multi-stage builds : séparer build et runtime pour réduire la taille
- Cache registry :
type=registry,ref=...pour partager entre runners (voir doc Docker cache)
Traçabilité
Section intitulée « Traçabilité »- Labels OCI : ajouter
org.opencontainers.image.revisionavec le SHA - Stocker le digest dans les metadata de release
- SBOM : générer avec Syft et attacher à l'image
Exemple complet : du build au déploiement
Section intitulée « Exemple complet : du build au déploiement »Voici un fil rouge bout-en-bout pour une application api-users :
1. Build en CI (merge sur main)
Section intitulée « 1. Build en CI (merge sur main) »Le point de départ est un merge sur main. Le pipeline produit une seule image
mais lui accole plusieurs tags qui répondent à des besoins distincts : le
Git SHA pour retrouver le commit exact, le nom de branche et l'alias latest
pour le confort de développement. La valeur à conserver pour la suite n'est
aucun de ces tags, c'est le digest renvoyé par le build, la seule référence
qui ne bougera plus.
# Tags générésghcr.io/acme/api-users:abc1234 # Git SHA (traçabilité)ghcr.io/acme/api-users:main # Branchghcr.io/acme/api-users:latest # Alias dev
# Digest obtenusha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8552. Promotion staging → prod
Section intitulée « 2. Promotion staging → prod »Une fois les tests de staging validés sur ce digest, la promotion vers la
production consiste à poser des tags sur ce même digest, jamais à
reconstruire. skopeo copy ajoute le tag prod, crane tag ajoute le tag de
version 1.2.3. Comme les deux pointent sur le digest déjà éprouvé, l'image de
production est bit pour bit celle qui a traversé les tests.
# Après validation des tests stagingskopeo copy \ docker://ghcr.io/acme/api-users@sha256:e3b0c44... \ docker://ghcr.io/acme/api-users:prod
# Release tagcrane tag ghcr.io/acme/api-users@sha256:e3b0c44... 1.2.33. Pin du digest en Kubernetes
Section intitulée « 3. Pin du digest en Kubernetes »Le déploiement Kubernetes ferme la boucle en référençant l'image par digest
et non par tag. Ce choix a une conséquence directe sur le comportement du
cluster : un image: en sha256: ne peut pas dériver, quelle que soit la
politique imagePullPolicy, là où un tag prod pourrait faire tourner deux
versions différentes sur deux nœuds si le tag a été réécrit entre deux
docker pull. Le manifeste devient ainsi la trace exacte de ce qui s'exécute.
# deployment.yaml : le digest garantit l'immutabilitéspec: containers: - name: api-users image: ghcr.io/acme/api-users@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Résultat : traçabilité complète du commit au pod en production.
Testez vos connaissances
Section intitulée « Testez vos connaissances »Vérifions que vous maîtrisez les bases CI/CD d'images : tags, digests, cache et promotion.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »- Promouvoir, ne pas rebuilder : l'image prod = celle testée (même digest)
- Le digest est le contrat CI→CD : stocker et propager le
sha256:... - Toujours tagger avec le Git SHA : traçabilité code ↔ image
- Cache BuildKit :
cache-from/cache-topour des builds 10× plus rapides - Multi-arch : runners natifs + merge manifest (éviter l'émulation)
- Immutabilité : un tag peut être réécrit, pas un digest, vérifiez les policies registry
- Pinner les actions par SHA : sécurité supply chain
Pour aller plus loin
Section intitulée « Pour aller plus loin »Prochaine étape
Section intitulée « Prochaine étape »Pour comprendre pourquoi un tag peut pointer vers différentes images et comment fonctionne le multi-arch, plongez dans le standard OCI.