Published on

GitLab CI 캐시 안 먹힐 때 key·policy·권한 점검

Authors

GitLab CI에서 캐시를 걸어도 매번 Cache not found가 뜨거나, 분명히 cache:paths를 지정했는데 다음 잡에서 전혀 재사용되지 않는 경우가 자주 있습니다. 이 문제는 단순히 paths를 잘못 쓴 게 아니라, 캐시의 키(key) 설계, policy 동작 방식, 러너의 실행기(executor)·권한·스토리지 설정이 얽혀 발생합니다.

이 글은 “왜 캐시가 안 먹는지”를 로그로 확정하는 방법부터, 실제로 많이 터지는 케이스(브랜치마다 키가 바뀜, pull-only라 업로드가 안 됨, Kubernetes 러너에서 권한 문제로 업로드 실패, S3/MinIO 권한 오류 등)를 순서대로 해결합니다.

1) 캐시와 아티팩트부터 구분하기

먼저 GitLab CI의 cacheartifacts는 목적이 다릅니다.

  • cache: 속도 최적화(의존성, 빌드 캐시 등). 없어도 파이프라인은 돌아가야 정상입니다.
  • artifacts: 잡 결과물 전달(빌드 산출물, 테스트 리포트 등). 다음 스테이지에 반드시 전달돼야 할 때 사용합니다.

캐시가 “다음 잡에서 꼭 있어야 한다”는 전제로 설계하면 자주 깨집니다. 반대로 캐시가 안 먹는 상황에서 아티팩트를 써야 하는데 캐시로 해결하려고 하면 끝이 없습니다.

2) 가장 먼저 볼 것: 잡 로그의 캐시 라인

원인 분석은 설정 파일보다 로그가 더 정확합니다. 잡 로그에서 다음 문구를 찾으세요.

  • Checking cache for ...
  • No URL provided, cache will not be uploaded to shared cache server (공유 캐시 서버가 없거나 러너 설정 문제)
  • Cache file does not exist (업로드 자체가 안 됨)
  • WARNING: ... permission denied (권한 문제)
  • Uploading cache... / Downloading cache... (정상 흐름)

특히 No URL provided...는 “캐시가 로컬 러너에만 남는다”는 의미일 수 있어, 러너가 매번 새로 뜨는 환경(예: Kubernetes executor, autoscaling runner)에서는 사실상 매번 미스가 납니다.

3) key가 매번 바뀌면 캐시는 영원히 미스난다

캐시가 안 먹는 가장 흔한 이유는 key가 파이프라인마다 달라지는 것입니다.

3.1 브랜치/커밋 기반 key의 함정

예를 들어 아래처럼 CI_COMMIT_SHA를 키에 넣으면 커밋마다 키가 바뀌어 재사용이 거의 불가능합니다.

cache:
  key: "npm-${CI_COMMIT_SHA}"
  paths:
    - node_modules/

브랜치마다 분리하고 싶다면 CI_COMMIT_REF_SLUG 정도가 적당합니다.

cache:
  key: "npm-${CI_COMMIT_REF_SLUG}"
  paths:
    - node_modules/

하지만 이것도 브랜치가 많으면 캐시가 쪼개져 히트율이 떨어집니다.

3.2 lockfile 기반 key로 “의존성 변경 시에만” 갱신

Node/Java/Python 모두 의존성 캐시는 lockfile이 진짜 기준입니다. GitLab은 cache:key:files를 지원합니다.

cache:
  key:
    files:
      - package-lock.json
  paths:
    - node_modules/

Yarn이면 yarn.lock, pnpm이면 pnpm-lock.yaml, Gradle이면 gradle.lockfile 또는 build.gradle 변경을 기준으로 잡는 식으로 설계합니다.

이 방식의 장점은 “커밋이 바뀌어도 lockfile이 같으면 캐시를 재사용”한다는 점입니다.

3.3 모노레포는 prefix 없으면 충돌한다

모노레포에서 여러 패키지가 각자 node_modules를 캐시하면, 키가 같을 때 서로 덮어쓰는 문제가 생깁니다. 이 경우 prefix를 패키지 경로로 분리하세요.

cache:
  key:
    files:
      - apps/web/package-lock.json
    prefix: "apps-web"
  paths:
    - apps/web/node_modules/

4) policy 때문에 업로드가 안 되는 케이스

GitLab 캐시는 policy로 다운로드/업로드 동작을 제어합니다.

  • pull: 다운로드만
  • push: 업로드만
  • pull-push: 둘 다(기본)

4.1 자주 하는 실수: pull로만 둬서 영원히 캐시가 없다

cache:
  policy: pull
  paths:
    - .gradle/caches/

이렇게 하면 “기존 캐시가 있을 때만” 내려받고, 새로 생성된 캐시는 업로드하지 않습니다. 처음부터 캐시가 없는 프로젝트라면 영원히 Cache not found가 됩니다.

해결은 pull-push로 바꾸거나, 첫 번째로 캐시를 만드는 잡에서만 push를 허용하는 식으로 분리합니다.

build:
  stage: build
  cache:
    policy: pull-push
    key:
      files:
        - gradle.lockfile
    paths:
      - .gradle/caches/

4.2 protected 브랜치/태그에서만 업로드가 되는 경우

러너가 protected로 설정되어 있거나, 잡이 protected에서만 실행되도록 제한된 경우, feature 브랜치에서는 캐시 업로드가 막히는 것처럼 보일 수 있습니다.

  • 러너 설정에서 Run untagged jobs / Protected 옵션 확인
  • 잡에 tags를 붙였다면, 해당 태그 러너가 실제로 feature 브랜치에서도 잡을 실행하는지 확인

이 문제는 “캐시”가 아니라 “잡 실행 권한/러너 매칭” 문제인 경우가 많습니다. 비슷한 결로 환경변수가 안 먹는 문제를 다룬 글도 참고할 만합니다: Jenkins Declarative Pipeline에서 env가 안 먹는 6가지 이유

5) paths가 잘못되어 실제로 캐시할 게 없는 경우

캐시는 지정한 경로가 실제로 존재하고, 잡 종료 시점에 파일이 있어야 업로드됩니다.

5.1 상대 경로 기준은 프로젝트 루트

GitLab 러너는 기본적으로 체크아웃된 프로젝트 디렉터리에서 실행합니다. cd로 다른 곳으로 이동한 뒤 생성한 파일을 캐시하려면 paths가 그 위치를 정확히 가리켜야 합니다.

script:
  - cd apps/web
  - npm ci
cache:
  paths:
    - node_modules/  # 실제 위치는 apps/web/node_modules 일 수 있음

올바르게는:

cache:
  paths:
    - apps/web/node_modules/

5.2 캐시 업로드 전에 삭제되는 패턴

일부 빌드 스크립트가 용량 절감을 위해 node_modules.gradle을 마지막에 지우는 경우가 있습니다. 그러면 업로드 단계에서 “없음”으로 처리됩니다.

  • 잡 스크립트에서 삭제 로직이 있는지 확인
  • after_script에서 정리하는 경우 특히 주의

6) 러너 실행기별 “캐시가 안 먹는” 구조적 원인

6.1 Docker executor: 로컬 캐시는 러너 머신에만 남는다

Docker executor에서 공유 캐시 서버(S3/MinIO 등)를 설정하지 않으면, 캐시는 러너 머신 로컬에만 남습니다.

  • 단일 러너 머신에서 계속 실행되면 히트할 수 있음
  • autoscaling(매번 새 VM)이라면 매번 미스

로그에 No URL provided...가 보이면 이 케이스를 의심하세요.

6.2 Kubernetes executor: 파드가 매번 새로 떠서 로컬 캐시가 없다

Kubernetes executor는 잡 파드가 매번 새로 만들어지는 경우가 많아 로컬 캐시에 의존하면 히트율이 0에 수렴합니다. 해결책은:

  • 러너에 S3/MinIO 기반 shared cache 설정
  • 또는 PV를 붙여서 캐시 디렉터리를 유지(운영 복잡도 증가)

Kubernetes 환경에서 리소스/네트워크 이슈가 겹치면 원인 파악이 더 어려워지는데, 인프라성 병목을 진단하는 글도 함께 보면 도움이 됩니다: Kubernetes CNI IP 부족으로 Pod Pending 해결 가이드

7) 권한 문제: 캐시는 “쓰기”가 되어야 업로드된다

캐시가 다운로드는 되는데 업로드가 실패하거나, 특정 디렉터리만 캐시가 비어 있다면 권한을 의심해야 합니다.

7.1 컨테이너 유저와 파일 소유권 불일치

예를 들어 이미지가 non-root 유저로 실행되는데, 캐시 디렉터리가 root 소유로 생성되면 다음 실행에서 쓰기 실패가 납니다.

진단용으로 아래를 잡에 추가해 보세요.

script:
  - whoami
  - id
  - ls -al
  - ls -al .gradle || true

해결은 보통 아래 중 하나입니다.

  • 빌드 캐시 디렉터리를 쓰기 가능한 경로로 변경
  • 컨테이너에서 chown 처리(보안 정책상 제한될 수 있음)
  • 러너/이미지에서 실행 유저를 통일

7.2 S3/MinIO 권한(IAM) 문제

shared cache를 S3로 쓰는 경우, 러너에 설정된 키가 다음 권한을 가져야 합니다.

  • s3:GetObject
  • s3:PutObject
  • s3:ListBucket (경우에 따라 필요)
  • s3:DeleteObject (정리 정책 사용 시)

권한이 부족하면 다운로드는 되는데 업로드가 실패하거나(또는 반대), 특정 prefix만 실패할 수 있습니다.

8) 실전 추천 구성: lockfile key + 정책 분리 + 빠른 실패 진단

아래는 Node 프로젝트 기준으로 “캐시가 잘 먹고, 문제 시 로그로 바로 확인”하는 예시입니다.

stages:
  - install
  - test

default:
  image: node:20

variables:
  NPM_CONFIG_CACHE: ".npm"

cache:
  key:
    files:
      - package-lock.json
    prefix: "npm"
  paths:
    - .npm/
  policy: pull-push

install:
  stage: install
  script:
    - node -v
    - npm -v
    - npm ci --prefer-offline
    - ls -al .npm || true

test:
  stage: test
  script:
    - npm test

포인트는 다음과 같습니다.

  • node_modules 대신 .npm(npm 캐시)만 캐시하면 OS/아키텍처/바이너리 모듈 차이로 인한 깨짐이 줄어듭니다.
  • key:files로 lockfile 변경 시에만 캐시가 갱신됩니다.
  • policy: pull-push로 최초에도 캐시가 생성됩니다.

9) 그래도 안 되면: 체크리스트 10

  1. 잡 로그에 Downloading cache...가 실제로 찍히는가
  2. No URL provided...가 있다면 shared cache 설정이 있는가
  3. key가 커밋마다 바뀌지 않는가(CI_COMMIT_SHA 사용 여부)
  4. cache:key:files가 lockfile을 제대로 가리키는가
  5. 모노레포면 prefix로 충돌을 막았는가
  6. policypull로 고정되어 업로드가 막히지 않는가
  7. paths가 실제 생성 위치와 일치하는가
  8. 잡 마지막에 캐시 디렉터리를 삭제하고 있지 않은가
  9. 컨테이너 유저 권한으로 해당 디렉터리에 쓰기가 가능한가
  10. S3/MinIO 사용 시 버킷 정책과 prefix 권한이 충분한가

권한과 경로 문제는 CI에서 특히 흔합니다. 운영 환경에서 스케줄러가 작업을 못 돌리는 문제와 결이 비슷하니, 원인 접근 방식은 아래 글의 체크리스트도 참고할 만합니다: 리눅스 cron 미실행? PATH·메일로그·권한 점검

10) 마무리: 캐시를 “설계”하면 히트율이 올라간다

GitLab CI 캐시는 단순히 paths만 적는 기능이 아니라, 키 설계로 재사용 범위를 정하고, policy로 업로드/다운로드 흐름을 만들고, 러너의 저장소/권한/실행기 특성에 맞춰야 안정적으로 동작합니다.

정리하면,

  • 의존성 캐시는 key:files로 lockfile 기반으로 잡기
  • policy는 최초 생성까지 고려해 pull-push를 기본으로
  • Kubernetes/autoscaling이면 shared cache(S3/MinIO) 없이는 히트율이 낮을 수 있음
  • 로그에서 “다운로드/업로드가 실제로 수행됐는지”부터 확인

여기까지 점검하면 대부분의 “GitLab CI 캐시 안 먹힘” 문제는 재현 가능하게 원인을 특정하고 해결할 수 있습니다.