Aller au contenu
high

CI/CD d'images : tags, digests, cache, multi-arch et promotion

21 min de lecture

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.

Pipeline CI/CD d'images conteneurs

  • 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

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.

Fenêtre de terminal
# Tag : peut changer !
docker pull nginx:1.25
# Digest : toujours la même image
docker pull nginx@sha256:6926dd802f40e5e7257fded83e0d8030039642e4e10c4a98a6478e9c6fe06153
AspectTagDigest
Formatimage:tagimage@sha256:abc123...
Mutable✅ Oui❌ Non
Lisiblenginx:1.25❌ Hash de 64 caractères
Reproductible❌ Non garanti✅ Toujours identique
AuditDifficileFacile

Règle pratique :

  • Développement : tags semver (1.2.3) ou branch (main)
  • Production : digest ou tag semver strict
  • Audit/compliance : toujours le digest

Une bonne stratégie de tags permet de tracer quelle version du code produit quelle image.

TagQuandExempleUsage
Git SHAToujoursabc1234Traçabilité exacte
SemverRelease1.2.3Production
latestMerge mainlatestDev/test uniquement
PRPull requestspr-123Tests d'intégration

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=pr

Résultat pour un push de tag v1.2.3 :

  • ghcr.io/mon-org/mon-app:1.2.3
  • ghcr.io/mon-org/mon-app:1.2
  • ghcr.io/mon-org/mon-app:abc1234

Sans cache, chaque build repart de zéro. Avec le cache BuildKit, seules les couches modifiées sont reconstruites.

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=max

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 cacheAvantagesInconvénients
RegistryPartagé entre runners, persistantPlus lent (réseau)
GHARapide, local10 Go par défaut (extensible selon plan)
LocalLe plus rapideNon partagé entre runners

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.

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.

Fenêtre de terminal
# Créer un builder multi-plateforme
docker buildx create --name multiarch --use
# Build et push pour AMD64 et ARM64
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/mon-org/mon-app:1.0.0 \
--push \
.
- 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.

Fenêtre de terminal
# Voir les architectures disponibles
docker manifest inspect ghcr.io/mon-org/mon-app:1.0.0

La promotion consiste à faire passer une image déjà buildée d'un environnement à l'autre, sans la reconstruire.

Workflow de promotion d'images entre environnements

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.

ApprocheRisque
Rebuild pour chaque envL'image prod peut différer de celle testée
Promouvoir la même image✅ Garantie que prod = ce qui a été testé

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)

Fenêtre de terminal
# Copier l'image de staging vers prod sans la télécharger
skopeo copy \
docker://ghcr.io/mon-org/mon-app:staging \
docker://ghcr.io/mon-org/mon-app:prod

Option 2 : Re-tagger avec Crane

Fenêtre de terminal
# Copier avec crane (Google)
crane copy ghcr.io/mon-org/mon-app:staging ghcr.io/mon-org/mon-app:prod

Option 3 : En CI avec docker/build-push-action (sans rebuild)

# Promotion manuelle via workflow_dispatch
on:
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.

1. Merge PR → build image:sha-abc1234, image:main
2. Tests auto → si OK, tag image:staging
3. Tests staging → si OK, promotion manuelle vers image:prod
4. Release → tag image:1.2.3 sur le digest de prod

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 }}

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èmeCause probableSolution
Build lent malgré le cacheCache invalide (layers changés)Vérifier l'ordre des instructions Dockerfile, mettre les dépendances avant le code
cache-from ne fonctionne pasImage cache n'existe pas encorePremier build sans cache, puis les suivants l'utilisent
Multi-arch échoue sur ARMQEMU non configuréAjouter docker/setup-qemu-action avant buildx
denied: permission deniedToken sans packages:writeVérifier les permissions GitHub Actions
Digest différent après rebuildMétadonnées variables (date, run ID) ou dépendances non pinnéesFixer les LABEL, utiliser lockfiles (package-lock.json, requirements.txt)
Image corrompue après promotionRegistry intermédiaire incompatibleUtiliser skopeo copy --all pour copier toutes les architectures

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)

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.txt avant 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)
  • Labels OCI : ajouter org.opencontainers.image.revision avec le SHA
  • Stocker le digest dans les metadata de release
  • SBOM : générer avec Syft et attacher à l'image

Voici un fil rouge bout-en-bout pour une application api-users :

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.

Fenêtre de terminal
# Tags générés
ghcr.io/acme/api-users:abc1234 # Git SHA (traçabilité)
ghcr.io/acme/api-users:main # Branch
ghcr.io/acme/api-users:latest # Alias dev
# Digest obtenu
sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

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.

Fenêtre de terminal
# Après validation des tests staging
skopeo copy \
docker://ghcr.io/acme/api-users@sha256:e3b0c44... \
docker://ghcr.io/acme/api-users:prod
# Release tag
crane tag ghcr.io/acme/api-users@sha256:e3b0c44... 1.2.3

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:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Résultat : traçabilité complète du commit au pod en production.

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

7 questions
5 min.
80% requis

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

  1. Promouvoir, ne pas rebuilder : l'image prod = celle testée (même digest)
  2. Le digest est le contrat CI→CD : stocker et propager le sha256:...
  3. Toujours tagger avec le Git SHA : traçabilité code ↔ image
  4. Cache BuildKit : cache-from / cache-to pour des builds 10× plus rapides
  5. Multi-arch : runners natifs + merge manifest (éviter l'émulation)
  6. Immutabilité : un tag peut être réécrit, pas un digest, vérifiez les policies registry
  7. Pinner les actions par SHA : sécurité supply chain

Pour comprendre pourquoi un tag peut pointer vers différentes images et comment fonctionne le multi-arch, plongez dans le standard OCI.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn