Estratégias Eficazes de Cache de Build no Jenkins para Velocidade em CI/CD

Acelere seus pipelines de CI/CD no Jenkins dominando estratégias de cache de build. Este guia detalha métodos práticos para reutilizar dependências, saídas de compiladores e camadas Docker entre builds. Aprenda a aproveitar a retenção de workspace, opções de build do Docker e técnicas de cache compartilhado para minimizar tarefas redundantes e acelerar significativamente seus processos de integração e implantação.

Estratégias Eficazes de Cache de Build no Jenkins para Velocidade em CI/CD

O cache de build no Jenkins não é um recurso único. É um conjunto de decisões sobre o que pode ser reutilizado com segurança entre builds. Um bom cache economiza downloads de dependências, camadas Docker, trabalho do compilador e metadados do gerenciador de pacotes. Um cache ruim esconde builds quebrados, enche discos ou faz com que um pipeline passe em um agente e falhe em outro.

Comece observando a etapa repetida mais lenta no log do job. Se todo build gasta dois minutos baixando artefatos Maven, faça cache do Maven. Se o Docker reconstrói as mesmas camadas base toda vez, corrija o cache do Docker. Se os testes são lentos porque a compilação começa do zero a cada execução, use o cache da própria ferramenta de build antes de inventar um arquivo em nível de Jenkins.

Mantenha workspaces quando ajudam, limpe-os quando atrapalham

O cache mais simples é o workspace existente do Jenkins em um agente persistente. Se o mesmo job roda no mesmo nó, os arquivos deixados do build anterior podem ser reutilizados.

Isso pode ajudar com ferramentas como Maven, Gradle, npm, pnpm, Cargo e Go. Também pode causar falhas estranhas quando arquivos gerados, relatórios de teste antigos ou saídas de build obsoletas permanecem no workspace.

Um compromisso comum é limpar apenas a árvore de código-fonte e manter diretórios de cache dedicados fora dela:

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

Isso mantém o workspace reproduzível enquanto ainda reutiliza dependências baixadas. Certifique-se de que as permissões do diretório de cache correspondam ao usuário que executa o agente.

Faça cache de dependências com as regras da ferramenta

Caches de dependência funcionam melhor quando o gerenciador de pacotes os controla. Não arquive e restaure node_modules entre agentes não relacionados, a menos que tenha um motivo forte. Geralmente é mais seguro armazenar em cache o repositório de download do gerenciador de pacotes.

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, use um caminho de repositório local estável:

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

Para Gradle, mantenha GRADLE_USER_HOME estável e ative o cache de build do Gradle no projeto quando apropriado:

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

O arquivo de bloqueio é sua barra de segurança. Se package-lock.json, pnpm-lock.yaml, pom.xml ou build.gradle mudar, a ferramenta deve buscar as novas dependências corretas. Se um cache continuar servindo pacotes antigos ou corrompidos, exclua-o e deixe a ferramenta reconstruí-lo.

Cache de camadas Docker

O cache Docker depende de onde o daemon Docker armazena as camadas. Em um agente VM de longa duração, o docker build normal pode reutilizar camadas automaticamente. Em agentes Kubernetes efêmeros, o próximo build geralmente cai em um pod novo sem histórico de camadas.

Para agentes Docker persistentes, coloque linhas do Dockerfile que mudam lentamente primeiro:

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

Se você copiar todo o repositório antes de npm ci, cada alteração no código-fonte invalida a camada de dependência.

Para agentes efêmeros, use cache de registro do 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 .

Esse cache é compartilhado através do registro em vez do daemon local. Geralmente é mais adequado para agentes Jenkins baseados em Kubernetes do que tentar montar um diretório de camadas Docker gravável em vários pods.

Arquive artefatos, não caches, a menos que você realmente queira

archiveArtifacts é útil para saídas de build que você deseja manter: JARs, relatórios de teste, arquivos de cobertura, pacotes gerados. Não é um ótimo armazenamento de cache de uso geral. Arquivos de dependência grandes sobrecarregam o controlador, aumentam a pressão de armazenamento e dificultam a limpeza.

Se você precisar de um cache entre agentes, prefira um local de cache externo real: um repositório de artefatos, um bucket de armazenamento de objetos, um proxy de pacotes ou um cache de registro. Para builds Java, um gerenciador de repositório como Nexus ou Artifactory geralmente dá melhores resultados do que copiar .m2 entre agentes Jenkins. Para Docker, um cache BuildKit baseado em registro é mais previsível do que empacotar diretórios de camadas.

Torne as chaves de cache visíveis

Um cache deve ter um motivo para ser válido. Boas chaves de cache incluem o sistema operacional, arquitetura, versão principal da linguagem, versão do gerenciador de pacotes e hash do arquivo de bloqueio.

Por exemplo, uma chave de cache Node pode ser baseada em:

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

Você não precisa de um plugin de cache sofisticado para aplicar a ideia. Até um nome de diretório pode carregar a chave:

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

Isso evita reutilizar um cache de dependência entre versões de ferramentas incompatíveis. Também torna a limpeza menos misteriosa porque chaves antigas são fáceis de identificar.

Observe os modos de falha

O cache tem alguns padrões de falha familiares:

  • Builds passam apenas em um agente porque esse agente tem um arquivo oculto no workspace.
  • O disco enche porque caches antigos nunca são podados.
  • Imagens Docker são reconstruídas do zero porque o Dockerfile copia arquivos voláteis muito cedo.
  • Um cache compartilhado é corrompido quando vários builds escrevem nele ao mesmo tempo.
  • Restaurar um cache enorme leva mais tempo do que baixar dependências de um proxy de pacotes próximo.

Os logs de build devem mostrar se o cache está sendo usado. O Maven diz quando baixa dependências. O Docker mostra CACHED ou falhas de cache com a saída do BuildKit. O Gradle tem varredura de build e saída de console para comportamento de cache. Se um cache é invisível, adicione registro em log em torno de seu caminho, tamanho e chave.

Um plano de implantação prático

Escolha um pipeline e um gargalo. Adicione um cache apenas para essa etapa. Execute o mesmo commit duas vezes e compare os logs. Em seguida, execute após alterar o arquivo de bloqueio de dependência e confirme que o cache invalida corretamente. Finalmente, adicione limpeza.

Para agentes persistentes, a limpeza pode ser um cron job ou um job de manutenção do 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'

Ajuste esses comandos ao seu ambiente. Não execute comandos de poda amplos em hosts compartilhados sem verificar o que mais usa o mesmo daemon Docker ou sistema de arquivos.

A melhor estratégia de cache de build do Jenkins é entediante: armazene em cache o trabalho repetitivo caro, deixe a ferramenta de build validar a correção, mantenha os caminhos de cache explícitos e exclua dados antigos antes que o disco do agente se torne o próximo gargalo.

Combine o cache ao modelo de agente

Agentes VM persistentes e agentes de nuvem descartáveis precisam de designs de cache diferentes. Em uma VM persistente, diretórios locais são baratos e rápidos. Um cache Maven em /var/cache/jenkins/m2 ou um cache de camada Docker no daemon pode sobreviver por semanas. O risco é crescimento de disco e estado obsoleto.

Em agentes descartáveis, caches locais desaparecem após cada build. Você ainda pode armazenar em cache, mas o cache tem que viver em outro lugar: armazenamento de objetos, um proxy de pacotes, um registro de contêiner ou um volume persistente. Volumes persistentes no Kubernetes podem funcionar, mas caches compartilhados graváveis precisam de cuidado. Dois builds escrevendo no mesmo cache ao mesmo tempo podem criar contenção de bloqueio ou corromper downloads parciais, dependendo da ferramenta.

Para muitas equipes, o melhor primeiro investimento não é um plugin Jenkins. É um proxy de dependência próximo:

  • Maven ou Gradle através de Nexus ou Artifactory.
  • npm através de um registro privado ou proxy.
  • Docker através de um espelho de registro.
  • Python através de um espelho de pacotes ou índice interno.

Isso melhora todos os agentes sem restaurar tarballs enormes no início de cada job.

Saiba quando não armazenar em cache

Não armazene em cache segredos, credenciais geradas, manifestos de implantação contendo tokens ou diretórios que misturam saída de build com configuração específica do ambiente. Não armazene em cache bancos de dados de teste, a menos que o conjunto de testes seja explicitamente projetado para restauração de snapshot. Não compartilhe caches mutáveis entre jobs confiáveis e não confiáveis.

Também seja cético em relação ao cache quando a etapa de restauração for maior que o trabalho que ela economiza. Um arquivo de 2 GB que leva 90 segundos para baixar e descompactar não é útil se o gerenciador de pacotes pode instalar limpo em 45 segundos a partir de um proxy local.

Meça builds frios e quentes:

build frio: sem cache local
build quente: mesmo commit, mesma chave de cache
build com dependência alterada: arquivo de bloqueio alterado
build com código-fonte alterado: apenas código-fonte alterado

Essas quatro execuções dizem se o cache ajuda o fluxo de trabalho real ou apenas faz um rebuild artificial parecer bom.

Torne a limpeza de cache parte do design

Todo cache precisa de uma história de expiração antes de entrar em operação. Em agentes estáticos, agende a limpeza fora do horário de pico de build. Em registros compartilhados ou armazenamento de objetos, use políticas de ciclo de vida. No Jenkins, acompanhe o tamanho do cache como uma métrica operacional, não um pensamento posterior.

É normal excluir caches durante um incidente. Um bom pipeline deve ficar mais lento após a exclusão do cache, não quebrado. Se excluir um cache quebra o build, o cache está escondendo uma declaração de dependência ausente ou uma suposição de ambiente.