Estrategias Efectivas de Caché de Compilación en Jenkins para Acelerar CI/CD
Acelera tus pipelines de CI/CD en Jenkins dominando las estrategias de caché de compilación. Esta guía detalla métodos prácticos para reutilizar dependencias, salidas del compilador y capas de Docker entre compilaciones. Aprende cómo aprovechar la retención del espacio de trabajo, las opciones de compilación de Docker y las técnicas de caché compartida para minimizar tareas redundantes y acelerar significativamente tus procesos de integración e implementación.
Estrategias Efectivas de Caché de Compilación en Jenkins para Acelerar CI/CD
El caché de compilación de Jenkins no es una característica única. Es un conjunto de decisiones sobre qué se puede reutilizar de manera segura entre compilaciones. Un buen caché ahorra descargas de dependencias, capas de Docker, trabajo del compilador y metadatos del gestor de paquetes. Un mal caché oculta compilaciones rotas, llena discos o hace que un pipeline pase en un agente y falle en otro.
Comienza observando el paso repetido más lento en el registro del trabajo. Si cada compilación pasa dos minutos descargando artefactos de Maven, almacena en caché Maven. Si Docker reconstruye las mismas capas base cada vez, arregla el caché de Docker. Si las pruebas son lentas porque la compilación comienza desde cero en cada ejecución, usa el propio caché de la herramienta de compilación antes de inventar un archivo a nivel de Jenkins.
Mantén los espacios de trabajo cuando ayuden, límpialos cuando perjudiquen
El caché más simple es el espacio de trabajo existente de Jenkins en un agente persistente. Si el mismo trabajo se ejecuta en el mismo nodo, los archivos dejados de la compilación anterior se pueden reutilizar.
Esto puede ayudar con herramientas como Maven, Gradle, npm, pnpm, Cargo y Go. También puede causar fallos extraños cuando archivos generados, informes de prueba antiguos o salidas de compilación obsoletas permanecen en el espacio de trabajo.
Un compromiso común es limpiar solo el árbol fuente y mantener directorios de caché dedicados fuera de él:
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'
}
}
}
}
Esto mantiene el espacio de trabajo reproducible mientras se reutilizan las dependencias descargadas. Asegúrate de que los permisos del directorio de caché coincidan con el usuario que ejecuta el agente.
Almacena en caché las dependencias con las reglas de la herramienta
Los cachés de dependencias funcionan mejor cuando el gestor de paquetes los controla. No archives ni restaures node_modules entre agentes no relacionados a menos que tengas una razón sólida. Generalmente es más seguro almacenar en caché el almacén de descargas del gestor de paquetes.
Para npm:
environment {
npm_config_cache = "${WORKSPACE}/.npm-cache"
}
steps {
sh 'npm ci'
}
Para pnpm:
environment {
PNPM_STORE_PATH = "${WORKSPACE}/.pnpm-store"
}
steps {
sh 'pnpm install --frozen-lockfile'
}
Para Maven, usa una ruta de repositorio local estable:
sh 'mvn -B -Dmaven.repo.local=/var/cache/jenkins/m2 test'
Para Gradle, mantén GRADLE_USER_HOME estable y habilita el caché de compilación de Gradle en el proyecto cuando sea apropiado:
environment {
GRADLE_USER_HOME = '/var/cache/jenkins/gradle'
}
steps {
sh './gradlew test --build-cache'
}
El archivo de bloqueo es tu barra de seguridad. Si package-lock.json, pnpm-lock.yaml, pom.xml o build.gradle cambian, la herramienta debería obtener las nuevas dependencias correctas. Si un caché sigue sirviendo paquetes antiguos o corruptos, elimínalo y deja que la herramienta lo reconstruya.
Caché de capas de Docker
El caché de Docker depende de dónde almacena las capas el demonio de Docker. En un agente VM de larga duración, docker build normal puede reutilizar capas automáticamente. En agentes efímeros de Kubernetes, la siguiente compilación a menudo aterriza en un pod nuevo sin historial de capas.
Para agentes Docker persistentes, coloca las líneas de Dockerfile que cambian lentamente primero:
FROM node:22-bookworm
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm test
Si copias todo el repositorio antes de npm ci, cada cambio en el código fuente invalida la capa de dependencias.
Para agentes efímeros, usa el caché de registro de 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 .
Ese caché se comparte a través del registro en lugar del demonio local. Generalmente es más adecuado para agentes Jenkins basados en Kubernetes que intentar montar un directorio de capas Docker escribible en muchos pods.
Archiva artefactos, no cachés, a menos que sea intencional
archiveArtifacts es útil para salidas de compilación que deseas conservar: JARs, informes de prueba, archivos de cobertura, paquetes generados. No es un almacén de caché de propósito general. Los archivos grandes de dependencias ralentizan el controlador, aumentan la presión de almacenamiento y dificultan la limpieza.
Si necesitas un caché entre agentes, prefiere una ubicación de caché externa real: un repositorio de artefactos, un bucket de almacenamiento de objetos, un proxy de paquetes o un caché de registro. Para compilaciones Java, un gestor de repositorios como Nexus o Artifactory a menudo da mejores resultados que copiar .m2 entre agentes Jenkins. Para Docker, un caché BuildKit respaldado por registro es más predecible que empaquetar directorios de capas.
Haz visibles las claves de caché
Un caché debe tener una razón para ser válido. Las buenas claves de caché incluyen el sistema operativo, la arquitectura, la versión principal del lenguaje, la versión del gestor de paquetes y el hash del archivo de bloqueo.
Por ejemplo, una clave de caché de Node podría basarse en:
linux-amd64-node22-npm10-sha256(package-lock.json)
No necesitas un plugin de caché sofisticado para aplicar la idea. Incluso un nombre de directorio puede llevar la clave:
sh '''
LOCK_HASH=$(sha256sum package-lock.json | awk '{print $1}')
export npm_config_cache="/var/cache/jenkins/npm/node22-${LOCK_HASH}"
npm ci
'''
Esto evita reutilizar un caché de dependencias entre versiones de herramientas incompatibles. También facilita la limpieza porque las claves antiguas son fáciles de identificar.
Observa los modos de fallo
El almacenamiento en caché tiene algunos patrones de fallo familiares:
- Las compilaciones pasan solo en un agente porque ese agente tiene un archivo oculto en el espacio de trabajo.
- El disco se llena porque los cachés antiguos nunca se podan.
- Las imágenes Docker se reconstruyen desde cero porque el Dockerfile copia archivos volátiles demasiado pronto.
- Un caché compartido se corrompe cuando múltiples compilaciones escriben en él al mismo tiempo.
- Restaurar un caché enorme lleva más tiempo que descargar dependencias de un proxy de paquetes cercano.
Los registros de compilación deben mostrar si el caché se está utilizando. Maven indica cuándo descarga dependencias. Docker muestra CACHED o fallos de caché con la salida de BuildKit. Gradle tiene escaneo de compilación y salida de consola para el comportamiento del caché. Si un caché es invisible, agrega registro alrededor de su ruta, tamaño y clave.
Un plan de implementación práctico
Elige un pipeline y un cuello de botella. Agrega un caché solo para ese paso. Ejecuta el mismo commit dos veces y compara los registros. Luego ejecuta después de cambiar el archivo de bloqueo de dependencias y confirma que el caché se invalida correctamente. Finalmente, agrega limpieza.
Para agentes persistentes, la limpieza puede ser un trabajo cron o un trabajo de mantenimiento de 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'
Ajusta esos comandos a tu entorno. No ejecutes comandos de poda amplios en hosts compartidos sin verificar qué más usa el mismo demonio Docker o sistema de archivos.
La mejor estrategia de caché de compilación de Jenkins es aburrida: almacena en caché el trabajo repetitivo costoso, deja que la herramienta de compilación valide la corrección, mantén las rutas de caché explícitas y elimina los datos antiguos antes de que el disco del agente se convierta en el próximo cuello de botella.
Adapta el caché al modelo de agente
Los agentes VM persistentes y los agentes en la nube desechables necesitan diferentes diseños de caché. En una VM persistente, los directorios locales son baratos y rápidos. Un caché de Maven en /var/cache/jenkins/m2 o un caché de capas de Docker en el demonio pueden sobrevivir durante semanas. El riesgo es el crecimiento del disco y el estado obsoleto.
En agentes desechables, los cachés locales desaparecen después de cada compilación. Aún puedes almacenar en caché, pero el caché tiene que vivir en otro lugar: almacenamiento de objetos, un proxy de paquetes, un registro de contenedores o un volumen persistente. Los volúmenes persistentes en Kubernetes pueden funcionar, pero los cachés compartidos escribibles necesitan cuidado. Dos compilaciones escribiendo en el mismo caché al mismo tiempo pueden crear contención de bloqueo o corromper descargas parciales dependiendo de la herramienta.
Para muchos equipos, la mejor primera inversión no es un plugin de Jenkins. Es un proxy de dependencias cercano:
- Maven o Gradle a través de Nexus o Artifactory.
- npm a través de un registro privado o proxy.
- Docker a través de un espejo de registro.
- Python a través de un espejo de paquetes o índice interno.
Eso mejora cada agente sin restaurar grandes tarballs al inicio de cada trabajo.
Sabe cuándo no almacenar en caché
No almacenes en caché secretos, credenciales generadas, manifiestos de implementación que contengan tokens, o directorios que mezclen salida de compilación con configuración específica del entorno. No almacenes en caché bases de datos de prueba a menos que el conjunto de pruebas esté explícitamente diseñado para la restauración de instantáneas. No compartas cachés mutables entre trabajos confiables y no confiables.
También sé escéptico sobre el almacenamiento en caché cuando el paso de restauración es más grande que el trabajo que ahorra. Un archivo de 2 GB que tarda 90 segundos en descargarse y descomprimirse no es útil si el gestor de paquetes puede instalar limpiamente en 45 segundos desde un proxy local.
Mide compilaciones frías y cálidas:
compilación fría: sin caché local
compilación cálida: mismo commit, misma clave de caché
compilación con dependencias cambiadas: archivo de bloqueo cambiado
compilación con código fuente cambiado: solo código fuente cambiado
Esas cuatro ejecuciones te dicen si el caché ayuda al flujo de trabajo real o solo hace que una reconstrucción artificial se vea bien.
Haz que la limpieza del caché sea parte del diseño
Cada caché necesita una historia de caducidad antes de que se active. En agentes estáticos, programa la limpieza fuera de las horas pico de compilación. En registros compartidos o almacenamiento de objetos, usa políticas de ciclo de vida. En Jenkins, rastrea el tamaño del caché como una métrica operativa, no como una ocurrencia tardía.
Es normal eliminar cachés durante un incidente. Un buen pipeline debería volverse más lento después de la eliminación del caché, no roto. Si eliminar un caché rompe la compilación, el caché está ocultando una declaración de dependencia faltante o una suposición de entorno.