L’image API principale de l’équipe prend 6 minutes 12 secondes à builder à chaque commit. Le Dockerfile fait 18 lignes, le binaire est petit, et la plupart des builds changent un seul fichier Go. Rien ne devrait prendre autant de temps. Le coupable est simple — la configuration Docker par défaut de CodeBuild exécute chaque build à froid, sans réutilisation de couches et sans BuildKit. Trois changements de configuration ramènent le même build à 88 secondes sur un cache chaud.
Ce qui se passe
CodeBuild donne à chaque build un conteneur runtime neuf. Le daemon Docker dans ce conteneur démarre sans cache de couches. Chaque RUN apt-get install, chaque COPY package.json && npm install, chaque pull d’image de base se fait depuis zéro à chaque build. Pour une image avec un RUN coûteux (un npm install, un go mod download, un pip install), cette seule étape domine le build.
Trois mécanismes de cache existent, et ils résolvent des problèmes différents :
-
LOCAL_DOCKER_LAYER_CACHE — paramètre de CodeBuild qui épingle les builds au même hôte EC2 entre les exécutions et garde le store de couches Docker chaud sur cet hôte. Gratuit, aucun changement au Dockerfile. Bémol : fonctionne seulement sur les compute Linux Medium/Large/2XLarge et seulement quand l’hôte est réutilisé (pas de garantie, mais habituellement oui pour le même projet en quelques heures).
-
--cache-fromcontre ECR — pull explicite d’une image précédemment buildée depuis ECR et indique à BuildKit d’utiliser ses couches. Fonctionne à travers les hôtes. Nécessite BuildKit activé et une image cache taguée de manière cohérente. Pull ~50-200 Mo au début de chaque build mais réutilise les couches à travers les hôtes froids. -
Cache inline de BuildKit — fonctionnalité spécifique à BuildKit qui intègre les métadonnées de cache dans l’image poussée. Combinée avec
--cache-from, elle laisse BuildKit récupérer seulement les manifestes de couches, pas les blobs complets, pour décider quoi réutiliser.
Le bon setup combine les trois. LOCAL_DOCKER_LAYER_CACHE est une assurance gratuite pour les hôtes chauds. --cache-from couvre les hôtes froids. BuildKit rend les deux efficaces.
Vrais temps sur une application Node avec un node_modules de 280 Mo :
- Build à froid, aucun cache : 6 min 12 sec
- LOCAL_DOCKER_LAYER_CACHE chaud seulement : 1 min 48 sec
--cache-fromECR seulement, hôte froid : 2 min 12 sec- LOCAL_DOCKER_LAYER_CACHE +
--cache-from, hôte chaud : 88 sec - BuildKit + les deux caches, hôte chaud : 62 sec
L’amélioration, c’est la différence entre un développeur qui pousse quatre commits dans une matinée et un développeur qui ne le peut pas.
Comment vérifier
D’abord, activez LOCAL_DOCKER_LAYER_CACHE sur le projet CodeBuild. C’est de la configuration de projet, pas de buildspec :
aws codebuild update-project \
--name web-api \
--cache type=LOCAL,modes=LOCAL_DOCKER_LAYER_CACHE,LOCAL_SOURCE_CACHE
Ensuite activez BuildKit dans le buildspec et configurez --cache-from contre ECR :
version: 0.2
env:
variables:
DOCKER_BUILDKIT: "1"
ECR_REGISTRY: 123456789012.dkr.ecr.us-east-1.amazonaws.com
IMAGE_NAME: web-api
phases:
pre_build:
commands:
- aws ecr get-login-password --region us-east-1
| docker login --username AWS --password-stdin $ECR_REGISTRY
- docker pull $ECR_REGISTRY/$IMAGE_NAME:cache || true
build:
commands:
- export IMAGE_TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION:0:7}
- |
docker build \
--cache-from $ECR_REGISTRY/$IMAGE_NAME:cache \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG \
-t $ECR_REGISTRY/$IMAGE_NAME:cache \
.
post_build:
commands:
- docker push $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
- docker push $ECR_REGISTRY/$IMAGE_NAME:cache
Le tag :cache est la référence de cache. Chaque build réussi pousse une nouvelle image :cache avec des métadonnées inline ; chaque build la pull (|| true pour ne pas échouer au premier build) et la passe à --cache-from.
Le Dockerfile doit être cache-friendly. L’erreur classique — COPY . . && RUN npm install — invalide la couche d’install à chaque changement de source. La bonne forme est de copier les manifestes de dépendances, installer, puis copier la source :
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
La couche d’install des dépendances est en cache tant que pnpm-lock.yaml ne change pas — ce qui est la plupart des commits.
Pour les secrets de build (un jeton npm pour un registre privé, un jeton d’authentification Sentry), le mount --secret de BuildKit est le bon pattern. Le secret n’atterrit jamais dans une couche :
pre_build:
commands:
- aws secretsmanager get-secret-value
--secret-id /apps/web/npm-token
--query SecretString --output text > /tmp/npm-token
build:
commands:
- DOCKER_BUILDKIT=1 docker build
--secret id=npm-token,src=/tmp/npm-token
--cache-from $ECR_REGISTRY/$IMAGE_NAME:cache
-t $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
.
- rm /tmp/npm-token
Dans le Dockerfile, l’étape d’install monte le secret :
RUN --mount=type=secret,id=npm-token,target=/root/.npmrc \
npm install --production
Le secret est disponible pendant ce seul RUN et n’est pas stocké dans l’image. Le vieux pattern de ARG NPM_TOKEN plus --build-arg cuit le jeton dans l’historique de l’image et n’est pas sûr.
Pour confirmer que le cache fonctionne, le log de build affiche CACHED à côté de chaque étape réutilisée :
#7 [build 3/5] RUN corepack enable && pnpm install --frozen-lockfile
#7 CACHED
#8 [build 4/5] COPY . .
#8 DONE 0.4s
Sans cache, chaque étape dit DONE avec une durée. Avec cache, les étapes coûteuses disent CACHED avec une durée de zéro.
La correction
La pile de changements qui fait passer un build Docker CodeBuild de froid à chaud-rapide :
-
Activez LOCAL_DOCKER_LAYER_CACHE sur le projet. Un appel
update-projectd’une ligne. Gratuit, aucun changement de buildspec. Couvre le cas de l’hôte chaud, qui est le cas courant pour les projets actifs. -
Mettez
DOCKER_BUILDKIT: "1"dans env. BuildKit est plus rapide sur chaque opération et est requis pour le cache inline--cache-fromet--mount=type=secret. Le builder legacy est en fin de vie. -
Poussez un tag
:cache, pullez-le danspre_build, passez à--cache-from. Ça gère le cas de l’hôte froid. Le tag:cacheest le cadeau du build au build suivant. -
Réordonnez le Dockerfile pour que les installs de dépendances soient des couches stables.
COPY package.jsonavantCOPY . ..COPY go.mod go.sumavantCOPY *.go .. La couche de dépendances doit s’invalider seulement aux changements de dépendances. -
Utilisez
--secretpour les secrets de build, jamais--build-arg. Les valeurs de--build-argsont visibles dans l’historique de l’image (docker history) ;--mount=type=secretn’est pas dans l’image du tout. Le jeton fuit si quelqu’un pull l’image et exécutedocker inspect. -
Choisissez le compute selon le profil de build, pas le défaut. Le
BUILD_GENERAL1_SMALLpar défaut, c’est 3 Go RAM, 2 vCPU. Pour des builds Docker avec un grosnode_modules, le SMALL est limité par la mémoire et plus lent que le MEDIUM malgré un coût par minute inférieur. Le MEDIUM termine plus vite et le coût total par build chute. Mesurez les deux. -
N’utilisez pas le cache multi-étapes sur
--cache-fromsans cache inline. SansBUILDKIT_INLINE_CACHE=1, l’image poussée contient seulement les couches de l’étape finale. Les étapes intermédiaires ne sont pas dans le registre et pas disponibles pour--cache-from. DéfinissezBUILDKIT_INLINE_CACHE=1et BuildKit publie le graphe complet.
Deux patterns à surveiller :
-
Le piège du cache
latest. Utiliser:latestcomme tag de cache fonctionne pour les projets avec une seule branche, mais casse quand des branches de fonctionnalités poussent leurs propres caches et s’écrasent mutuellement. Utilisez un tag préfixé par branche comme:cache-main,:cache-feature-foo. -
L’exigence du mode privilégié. Builder des images Docker sur CodeBuild requiert
privilegedMode: truedans le bloc d’environnement du projet. Sans ça, le daemon Docker ne démarre pas et chaque build échoue surdocker: command not found. C’est de la config de projet, pas de buildspec.
Une dernière note sur les chiffres. Un build de 6 minutes exécuté 30 fois par jour coûte 180 minutes de build par jour. Le même build à 90 secondes coûte 45 minutes de build. Sur une année de jours ouvrables, le compute économisé paie le temps que vous avez passé à lire cet article plusieurs fois — et l’amélioration de l’expérience développeur, c’est le gain le plus important.
Stack Harbor opère CodeBuild + CodePipeline à l’échelle production dans le cadre de l’onboarding Gestion AWS et des Environnements infogérés en continu. Si vos builds Docker s’étirent à 10 minutes quand ils devraient durer 90 secondes, commencez par une consultation d’infrastructure.