효과적인 Jenkins 빌드 캐싱 전략: CI/CD 속도 향상
빌드 캐싱 전략을 마스터하여 Jenkins CI/CD 파이프라인을 가속화하세요. 이 가이드에서는 빌드 간 종속성, 컴파일러 출력, Docker 레이어를 재사용하는 실용적인 방법을 자세히 설명합니다. 워크스페이스 유지, Docker 빌드 옵션, 공유 캐싱 기법을 활용하여 중복 작업을 최소화하고 통합 및 배포 프로세스를 크게 단축하는 방법을 알아보세요.
효과적인 Jenkins 빌드 캐싱 전략: CI/CD 속도 향상
Jenkins 빌드 캐싱은 하나의 기능이 아닙니다. 빌드 간에 안전하게 재사용할 수 있는 항목에 대한 일련의 결정입니다. 좋은 캐시는 종속성 다운로드, Docker 레이어, 컴파일러 작업, 패키지 관리자 메타데이터를 절약합니다. 나쁜 캐시는 빌드를 망가뜨리거나, 디스크를 가득 채우거나, 파이프라인이 한 에이전트에서는 통과하고 다른 에이전트에서는 실패하게 만듭니다.
작업 로그에서 가장 느린 반복 단계를 살펴보는 것부터 시작하세요. 모든 빌드가 Maven 아티팩트를 다운로드하는 데 2분을 소비한다면 Maven을 캐싱하세요. Docker가 매번 동일한 기본 레이어를 다시 빌드한다면 Docker 캐싱을 수정하세요. 테스트가 느린 이유가 실행할 때마다 컴파일이 처음부터 시작되기 때문이라면 Jenkins 수준 아카이브를 고안하기 전에 빌드 도구 자체의 캐시를 사용하세요.
도움이 될 때 워크스페이스를 유지하고, 해가 될 때 정리하세요
가장 간단한 캐시는 영구 에이전트에 있는 기존 Jenkins 워크스페이스입니다. 동일한 작업이 동일한 노드에서 실행되면 이전 빌드에서 남은 파일을 재사용할 수 있습니다.
이는 Maven, Gradle, npm, pnpm, Cargo, Go와 같은 도구에 도움이 될 수 있습니다. 그러나 생성된 파일, 오래된 테스트 보고서, 또는 낡은 빌드 출력물이 워크스페이스에 남아 있을 때 이상한 실패를 유발할 수도 있습니다.
일반적인 절충안은 소스 트리만 정리하고 전용 캐시 디렉토리는 그 외부에 유지하는 것입니다:
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'
}
}
}
}
이렇게 하면 다운로드한 종속성을 재사용하면서 워크스페이스를 재현 가능하게 유지할 수 있습니다. 캐시 디렉토리 권한이 에이전트를 실행하는 사용자와 일치하는지 확인하세요.
도구의 규칙에 따라 종속성 캐싱
종속성 캐시는 패키지 관리자가 제어할 때 가장 잘 작동합니다. 강력한 이유가 없다면 관련 없는 에이전트 간에 node_modules를 아카이브하고 복원하지 마세요. 일반적으로 패키지 관리자의 다운로드 저장소를 캐싱하는 것이 더 안전합니다.
npm의 경우:
environment {
npm_config_cache = "${WORKSPACE}/.npm-cache"
}
steps {
sh 'npm ci'
}
pnpm의 경우:
environment {
PNPM_STORE_PATH = "${WORKSPACE}/.pnpm-store"
}
steps {
sh 'pnpm install --frozen-lockfile'
}
Maven의 경우 안정적인 로컬 저장소 경로를 사용하세요:
sh 'mvn -B -Dmaven.repo.local=/var/cache/jenkins/m2 test'
Gradle의 경우 GRADLE_USER_HOME을 안정적으로 유지하고 적절할 때 프로젝트에서 Gradle 빌드 캐시를 활성화하세요:
environment {
GRADLE_USER_HOME = '/var/cache/jenkins/gradle'
}
steps {
sh './gradlew test --build-cache'
}
Lockfile은 안전 레일입니다. package-lock.json, pnpm-lock.yaml, pom.xml, 또는 build.gradle이 변경되면 도구가 올바른 새 종속성을 가져와야 합니다. 캐시가 계속 오래되었거나 손상된 패키지를 제공한다면 삭제하고 도구가 다시 빌드하도록 하세요.
Docker 레이어 캐싱
Docker 캐싱은 Docker 데몬이 레이어를 저장하는 위치에 따라 달라집니다. 장기 실행 VM 에이전트에서는 일반 docker build가 레이어를 자동으로 재사용할 수 있습니다. 임시 Kubernetes 에이전트에서는 다음 빌드가 레이어 기록이 없는 새 파드에 도달하는 경우가 많습니다.
영구 Docker 에이전트의 경우 느리게 변경되는 Dockerfile 줄을 먼저 배치하세요:
FROM node:22-bookworm
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm test
npm ci 전에 전체 저장소를 복사하면 모든 소스 변경이 종속성 레이어를 무효화합니다.
임시 에이전트의 경우 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 .
해당 캐시는 로컬 데몬 대신 레지스트리를 통해 공유됩니다. 일반적으로 Kubernetes 기반 Jenkins 에이전트에 대해 쓰기 가능한 Docker 레이어 디렉토리를 여러 파드에 마운트하려고 시도하는 것보다 더 적합합니다.
의도하지 않았다면 캐시가 아닌 아티팩트를 아카이브하세요
archiveArtifacts는 보관하려는 빌드 출력물(JAR, 테스트 보고서, 커버리지 파일, 생성된 패키지)에 유용합니다. 범용 캐시 저장소로는 적합하지 않습니다. 대규모 종속성 아카이브는 컨트롤러를 느리게 하고, 스토리지 압력을 증가시키며, 정리를 어렵게 만듭니다.
교차 에이전트 캐시가 필요하다면 실제 외부 캐시 위치(아티팩트 저장소, 객체 스토리지 버킷, 패키지 프록시, 레지스트리 캐시)를 선호하세요. Java 빌드의 경우 Nexus 또는 Artifactory와 같은 저장소 관리자가 Jenkins 에이전트 간에 .m2를 복사하는 것보다 더 나은 결과를 제공하는 경우가 많습니다. Docker의 경우 레지스트리 기반 BuildKit 캐시가 레이어 디렉토리를 tarball로 만드는 것보다 더 예측 가능합니다.
캐시 키를 명확하게 만드세요
캐시는 유효한 이유가 있어야 합니다. 좋은 캐시 키에는 운영 체제, 아키텍처, 주요 언어 버전, 패키지 관리자 버전, lockfile 해시가 포함됩니다.
예를 들어 Node 캐시 키는 다음을 기반으로 할 수 있습니다:
linux-amd64-node22-npm10-sha256(package-lock.json)
아이디어를 적용하기 위해 화려한 캐시 플러그인이 필요하지 않습니다. 디렉토리 이름에도 키를 포함할 수 있습니다:
sh '''
LOCK_HASH=$(sha256sum package-lock.json | awk '{print $1}')
export npm_config_cache="/var/cache/jenkins/npm/node22-${LOCK_HASH}"
npm ci
'''
이렇게 하면 호환되지 않는 도구 버전 간에 종속성 캐시를 재사용하는 것을 방지할 수 있습니다. 또한 오래된 키를 쉽게 식별할 수 있기 때문에 정리가 덜 모호해집니다.
실패 패턴을 주시하세요
캐싱에는 몇 가지 친숙한 실패 패턴이 있습니다:
- 해당 에이전트의 워크스페이스에 숨겨진 파일이 있기 때문에 빌드가 하나의 에이전트에서만 통과합니다.
- 오래된 캐시가 정리되지 않아 디스크가 가득 찹니다.
- Dockerfile이 휘발성 파일을 너무 일찍 복사하기 때문에 Docker 이미지가 처음부터 다시 빌드됩니다.
- 여러 빌드가 동시에 쓰기를 시도할 때 공유 캐시가 손상됩니다.
- 거대한 캐시를 복원하는 것이 가까운 패키지 프록시에서 종속성을 다운로드하는 것보다 더 오래 걸립니다.
빌드 로그에는 캐시가 사용 중인지 표시되어야 합니다. Maven은 종속성을 다운로드할 때 알려줍니다. Docker는 BuildKit 출력으로 CACHED 또는 캐시 미스를 보여줍니다. Gradle은 캐시 동작에 대한 빌드 스캔 및 콘솔 출력을 제공합니다. 캐시가 보이지 않으면 해당 경로, 크기 및 키에 로깅을 추가하세요.
실용적인 롤아웃 계획
하나의 파이프라인과 하나의 병목 지점을 선택하세요. 해당 단계에 대해서만 캐시를 추가하세요. 동일한 커밋을 두 번 실행하고 로그를 비교하세요. 그런 다음 종속성 lockfile을 변경한 후 실행하고 캐시가 올바르게 무효화되는지 확인하세요. 마지막으로 정리를 추가하세요.
영구 에이전트의 경우 정리는 cron 작업 또는 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'
해당 명령을 환경에 맞게 조정하세요. 동일한 Docker 데몬이나 파일 시스템을 사용하는 다른 항목이 없는지 확인하지 않고 공유 호스트에서 광범위한 prune 명령을 실행하지 마세요.
최고의 Jenkins 빌드 캐싱 전략은 지루합니다: 값비싼 반복 작업을 캐싱하고, 빌드 도구가 정확성을 검증하도록 하고, 캐시 경로를 명시적으로 유지하고, 에이전트 디스크가 다음 병목이 되기 전에 오래된 데이터를 삭제하세요.
캐시를 에이전트 모델에 맞추세요
영구 VM 에이전트와 일회용 클라우드 에이전트는 다른 캐시 설계가 필요합니다. 영구 VM에서는 로컬 디렉토리가 저렴하고 빠릅니다. /var/cache/jenkins/m2의 Maven 캐시 또는 데몬의 Docker 레이어 캐시는 몇 주 동안 지속될 수 있습니다. 위험은 디스크 증가와 오래된 상태입니다.
일회용 에이전트에서는 로컬 캐시가 각 빌드 후에 사라집니다. 여전히 캐싱할 수 있지만 캐시는 다른 곳(객체 스토리지, 패키지 프록시, 컨테이너 레지스트리, 영구 볼륨)에 있어야 합니다. Kubernetes의 영구 볼륨은 작동할 수 있지만 공유 쓰기 가능 캐시는 주의가 필요합니다. 동시에 동일한 캐시에 쓰는 두 빌드는 도구에 따라 잠금 경합을 만들거나 부분 다운로드를 손상시킬 수 있습니다.
많은 팀에게 최고의 첫 투자는 Jenkins 플러그인이 아닙니다. 가까운 종속성 프록시입니다:
- Nexus 또는 Artifactory를 통한 Maven 또는 Gradle.
- 개인 레지스트리 또는 프록시를 통한 npm.
- 레지스트리 미러를 통한 Docker.
- 패키지 미러 또는 내부 인덱스를 통한 Python.
이는 각 작업 시작 시 거대한 tarball을 복원하지 않고도 모든 에이전트를 개선합니다.
캐싱하지 말아야 할 때를 알세요
비밀, 생성된 자격 증명, 토큰이 포함된 배포 매니페스트, 또는 빌드 출력과 환경별 구성을 혼합하는 디렉토리는 캐싱하지 마세요. 테스트 스위트가 명시적으로 스냅샷 복원을 위해 설계되지 않은 한 테스트 데이터베이스를 캐싱하지 마세요. 신뢰할 수 있는 작업과 신뢰할 수 없는 작업 간에 변경 가능한 캐시를 공유하지 마세요.
또한 복원 단계가 절약하는 작업보다 더 큰 경우 캐싱에 회의적이어야 합니다. 2GB 아카이브를 다운로드하고 푸는 데 90초가 걸린다면 패키지 관리자가 로컬 프록시에서 45초 만에 깔끔하게 설치할 수 있다면 도움이 되지 않습니다.
콜드 및 웜 빌드를 측정하세요:
cold build: 로컬 캐시 없음
warm build: 동일한 커밋, 동일한 캐시 키
changed dependency build: lockfile 변경됨
changed source build: 소스만 변경됨
이 네 번의 실행은 캐시가 실제 워크플로우에 도움이 되는지 아니면 하나의 인위적인 재빌드만 좋게 보이게 하는지 알려줍니다.
캐시 정리를 설계의 일부로 만드세요
모든 캐시는 운영 전에 만료 계획이 필요합니다. 정적 에이전트에서는 피크 빌드 시간 외에 정리를 예약하세요. 공유 레지스트리나 객체 스토리지에서는 수명 주기 정책을 사용하세요. Jenkins에서는 캐시 크기를 사후 고려 사항이 아닌 운영 메트릭으로 추적하세요.
인시던트 중에 캐시를 삭제하는 것은 정상입니다. 좋은 파이프라인은 캐시 삭제 후 느려져야 하며, 망가지면 안 됩니다. 캐시 삭제가 빌드를 망가뜨린다면 캐시가 누락된 종속성 선언이나 환경 가정을 숨기고 있는 것입니다.