Stratégies efficaces de mise en cache des builds Jenkins pour accélérer l'IC/CD

Accélérez vos pipelines CI/CD Jenkins en maîtrisant les stratégies de mise en cache des builds. Ce guide détaille des méthodes pratiques pour réutiliser les dépendances, les sorties du compilateur et les couches Docker entre les builds. Apprenez à tirer parti de la rétention d'espace de travail, des options de build Docker et des techniques de mise en cache partagée pour minimiser les tâches redondantes et accélérer considérablement vos processus d'intégration et de déploiement.

Stratégies efficaces de mise en cache des builds Jenkins pour accélérer l'IC/CD

La mise en cache des builds Jenkins n'est pas une fonctionnalité unique. C'est un ensemble de décisions sur ce qui peut être réutilisé en toute sécurité entre les builds. Un bon cache économise les téléchargements de dépendances, les couches Docker, le travail du compilateur et les métadonnées du gestionnaire de paquets. Un mauvais cache cache les builds cassées, remplit les disques ou fait qu'un pipeline réussit sur un agent et échoue sur un autre.

Commencez par examiner l'étape répétée la plus lente dans le journal des tâches. Si chaque build passe deux minutes à télécharger des artefacts Maven, mettez Maven en cache. Si Docker reconstruit les mêmes couches de base à chaque fois, corrigez la mise en cache Docker. Si les tests sont lents parce que la compilation repart de zéro à chaque exécution, utilisez le cache de l'outil de build avant d'inventer une archive au niveau Jenkins.

Conserver les espaces de travail quand ils aident, les nettoyer quand ils nuisent

Le cache le plus simple est l'espace de travail Jenkins existant sur un agent persistant. Si la même tâche s'exécute sur le même nœud, les fichiers laissés par la build précédente peuvent être réutilisés.

Cela peut aider avec des outils tels que Maven, Gradle, npm, pnpm, Cargo et Go. Cela peut également provoquer des échecs étranges lorsque des fichiers générés, d'anciens rapports de test ou des sorties de build obsolètes restent dans l'espace de travail.

Un compromis courant consiste à nettoyer uniquement l'arborescence source et à conserver les répertoires de cache dédiés en dehors de celle-ci :

pipeline {
  agent { label 'linux-build' }
  environment {
    MAVEN_OPTS = '-Dmaven.repo.local=/var/cache/jenkins/maven'
    npm_config_cache = '/var/cache/jenkins/npm'
  }
  stages {
    stage('Checkout') {
      steps {
        deleteDir()
        checkout scm
      }
    }
    stage('Build') {
      steps {
        sh 'mvn -B test'
      }
    }
  }
}

Cela maintient l'espace de travail reproductible tout en réutilisant les dépendances téléchargées. Assurez-vous que les permissions du répertoire de cache correspondent à l'utilisateur qui exécute l'agent.

Mettre en cache les dépendances avec les règles de l'outil

Les caches de dépendances fonctionnent mieux lorsque le gestionnaire de paquets les contrôle. N'archivez pas et ne restaurez pas node_modules sur des agents non liés, sauf si vous avez une raison impérieuse. Il est généralement plus sûr de mettre en cache le magasin de téléchargement du gestionnaire de paquets.

Pour npm :

environment {
  npm_config_cache = "${WORKSPACE}/.npm-cache"
}
steps {
  sh 'npm ci'
}

Pour pnpm :

environment {
  PNPM_STORE_PATH = "${WORKSPACE}/.pnpm-store"
}
steps {
  sh 'pnpm install --frozen-lockfile'
}

Pour Maven, utilisez un chemin de dépôt local stable :

sh 'mvn -B -Dmaven.repo.local=/var/cache/jenkins/m2 test'

Pour Gradle, gardez GRADLE_USER_HOME stable et activez le cache de build Gradle dans le projet le cas échéant :

environment {
  GRADLE_USER_HOME = '/var/cache/jenkins/gradle'
}
steps {
  sh './gradlew test --build-cache'
}

Le fichier de verrouillage est votre rail de sécurité. Si package-lock.json, pnpm-lock.yaml, pom.xml ou build.gradle change, l'outil doit récupérer les bonnes nouvelles dépendances. Si un cache continue de servir des paquets obsolètes ou corrompus, supprimez-le et laissez l'outil le reconstruire.

Mise en cache des couches Docker

La mise en cache Docker dépend de l'endroit où le démon Docker stocke les couches. Sur un agent VM de longue durée, docker build normal peut réutiliser les couches automatiquement. Sur les agents Kubernetes éphémères, la build suivante atterrit souvent sur un nouveau pod sans historique de couches.

Pour les agents Docker persistants, placez les lignes Dockerfile qui changent lentement en premier :

FROM node:22-bookworm
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm test

Si vous copiez l'intégralité du dépôt avant npm ci, chaque modification de source invalide la couche de dépendances.

Pour les agents éphémères, utilisez la mise en cache du registre BuildKit :

docker buildx build \
  --cache-from type=registry,ref=registry.example.com/my-app:buildcache \
  --cache-to type=registry,ref=registry.example.com/my-app:buildcache,mode=max \
  -t registry.example.com/my-app:${GIT_COMMIT} \
  --push .

Ce cache est partagé via le registre au lieu du démon local. C'est généralement un meilleur ajustement pour les agents Jenkins basés sur Kubernetes que d'essayer de monter un répertoire de couches Docker inscriptible dans de nombreux pods.

Archiver les artefacts, pas les caches, sauf si vous le voulez vraiment

archiveArtifacts est utile pour les sorties de build que vous souhaitez conserver : JARs, rapports de test, fichiers de couverture, paquets générés. Ce n'est pas un bon magasin de cache général. Les grandes archives de dépendances ralentissent le contrôleur, augmentent la pression de stockage et rendent le nettoyage pénible.

Si vous avez besoin d'un cache inter-agents, préférez un emplacement de cache externe réel : un dépôt d'artefacts, un bucket de stockage d'objets, un proxy de paquets ou un cache de registre. Pour les builds Java, un gestionnaire de dépôt tel que Nexus ou Artifactory donne souvent de meilleurs résultats que la copie de .m2 entre les agents Jenkins. Pour Docker, un cache BuildKit basé sur un registre est plus prévisible que la mise en archive des répertoires de couches.

Rendre les clés de cache visibles

Un cache doit avoir une raison d'être valide. Les bonnes clés de cache incluent le système d'exploitation, l'architecture, la version majeure du langage, la version du gestionnaire de paquets et le hachage du fichier de verrouillage.

Par exemple, une clé de cache Node pourrait être basée sur :

linux-amd64-node22-npm10-sha256(package-lock.json)

Vous n'avez pas besoin d'un plugin de cache sophistiqué pour appliquer l'idée. Même un nom de répertoire peut porter la clé :

sh '''
LOCK_HASH=$(sha256sum package-lock.json | awk '{print $1}')
export npm_config_cache="/var/cache/jenkins/npm/node22-${LOCK_HASH}"
npm ci
'''

Cela évite de réutiliser un cache de dépendances entre des versions d'outils incompatibles. Cela rend également le nettoyage moins mystérieux car les anciennes clés sont faciles à identifier.

Surveiller les modes de défaillance

La mise en cache présente quelques schémas d'échec familiers :

  • Les builds ne réussissent que sur un seul agent parce que cet agent a un fichier caché dans l'espace de travail.
  • Le disque se remplit parce que les anciens caches ne sont jamais nettoyés.
  • Les images Docker se reconstruisent à partir de zéro parce que le Dockerfile copie des fichiers volatils trop tôt.
  • Un cache partagé est corrompu lorsque plusieurs builds y écrivent en même temps.
  • La restauration d'un cache énorme prend plus de temps que le téléchargement des dépendances depuis un proxy de paquets proche.

Les journaux de build doivent montrer si le cache est utilisé. Maven indique quand il télécharge des dépendances. Docker montre CACHED ou des échecs de cache avec la sortie BuildKit. Gradle a une analyse de build et une sortie console pour le comportement du cache. Si un cache est invisible, ajoutez une journalisation autour de son chemin, de sa taille et de sa clé.

Un plan de déploiement pratique

Choisissez un pipeline et un goulot d'étranglement. Ajoutez un cache pour cette étape uniquement. Exécutez le même commit deux fois et comparez les journaux. Ensuite, exécutez après avoir modifié le fichier de verrouillage des dépendances et confirmez que le cache s'invalide correctement. Enfin, ajoutez le nettoyage.

Pour les agents persistants, le nettoyage peut être une tâche cron ou une tâche de maintenance Jenkins :

find /var/cache/jenkins/npm -mindepth 1 -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +
find /var/cache/jenkins/m2 -type f -name '*.lastUpdated' -delete
docker system prune -af --filter 'until=168h'

Adaptez ces commandes à votre environnement. N'exécutez pas de commandes de nettoyage larges sur des hôtes partagés sans vérifier ce qui utilise le même démon Docker ou le même système de fichiers.

La meilleure stratégie de mise en cache des builds Jenkins est ennuyeuse : mettez en cache le travail répétitif coûteux, laissez l'outil de build valider l'exactitude, gardez les chemins de cache explicites et supprimez les anciennes données avant que le disque de l'agent ne devienne le prochain goulot d'étranglement.

Adapter le cache au modèle d'agent

Les agents VM persistants et les agents cloud jetables nécessitent des conceptions de cache différentes. Sur une VM persistante, les répertoires locaux sont bon marché et rapides. Un cache Maven dans /var/cache/jenkins/m2 ou un cache de couches Docker sur le démon peut survivre pendant des semaines. Le risque est la croissance du disque et l'état obsolète.

Sur les agents jetables, les caches locaux disparaissent après chaque build. Vous pouvez toujours mettre en cache, mais le cache doit vivre ailleurs : stockage d'objets, proxy de paquets, registre de conteneurs ou volume persistant. Les volumes persistants dans Kubernetes peuvent fonctionner, mais les caches partagés inscriptibles nécessitent des précautions. Deux builds écrivant sur le même cache en même temps peuvent créer une contention de verrouillage ou corrompre des téléchargements partiels selon l'outil.

Pour de nombreuses équipes, le meilleur premier investissement n'est pas un plugin Jenkins. C'est un proxy de dépendances proche :

  • Maven ou Gradle via Nexus ou Artifactory.
  • npm via un registre privé ou un proxy.
  • Docker via un miroir de registre.
  • Python via un miroir de paquets ou un index interne.

Cela améliore chaque agent sans restaurer d'énormes archives au début de chaque tâche.

Savoir quand ne pas mettre en cache

Ne mettez pas en cache les secrets, les identifiants générés, les manifestes de déploiement contenant des jetons ou les répertoires qui mélangent la sortie de build avec une configuration spécifique à l'environnement. Ne mettez pas en cache les bases de données de test, sauf si la suite de tests est explicitement conçue pour la restauration d'instantanés. Ne partagez pas les caches mutables entre des tâches de confiance et non fiables.

Soyez également sceptique quant à la mise en cache lorsque l'étape de restauration est plus grande que le travail qu'elle économise. Une archive de 2 Go qui prend 90 secondes à télécharger et décompresser n'est pas utile si le gestionnaire de paquets peut s'installer proprement en 45 secondes à partir d'un proxy local.

Mesurez les builds à froid et à chaud :

build à froid : pas de cache local
build à chaud : même commit, même clé de cache
build avec dépendance modifiée : fichier de verrouillage modifié
build avec source modifiée : source modifiée uniquement

Ces quatre exécutions vous indiquent si le cache aide le flux de travail réel ou ne rend qu'une reconstruction artificielle belle.

Faire du nettoyage du cache une partie de la conception

Chaque cache a besoin d'une histoire d'expiration avant d'être mis en service. Sur les agents statiques, planifiez le nettoyage en dehors des heures de pointe de build. Sur les registres partagés ou le stockage d'objets, utilisez des politiques de cycle de vie. Dans Jenkins, suivez la taille du cache comme une métrique opérationnelle, pas une réflexion après coup.

Il est normal de supprimer les caches lors d'un incident. Un bon pipeline devrait devenir plus lent après la suppression du cache, pas cassé. Si la suppression d'un cache casse la build, le cache cache une déclaration de dépendance manquante ou une hypothèse d'environnement.