Published on

GitLab CI 캐시 꼬임 - 빌드 완전 초기화 가이드

Authors

서론

GitLab CI를 오래 운영하다 보면 “로컬에서는 되는데 CI에서만 깨짐”, “어제까지 되던 빌드가 갑자기 실패”, “캐시 지우면 되는데 다음 파이프라인에서 또 꼬임” 같은 현상을 한 번쯤 겪습니다. 대부분의 경우 캐시(cache), 아티팩트(artifacts), Runner 워크스페이스(workdir), 컨테이너 레이어/패키지 매니저 캐시가 서로 다른 수명주기를 가지면서 불일치가 생기거나, 캐시 키 설계가 부정확해 서로 다른 빌드가 같은 캐시를 공유하면서 오염되는 것이 원인입니다.

이 글에서는 GitLab CI의 캐시가 꼬였을 때 “완전 초기화”를 어떤 순서로, 어떤 범위까지 해야 재발을 줄일 수 있는지 정리합니다. 단순히 cache: []로 임시 회피하는 수준이 아니라, 원인 분류 → 안전한 캐시 키 설계 → 단계별 초기화(프로젝트/Runner/스토리지) → 재발 방지까지 다룹니다.

> 운영 환경에서 장애가 반복되면 원인-가설-검증 루프가 길어집니다. 비슷한 원리로, 인증/권한 문제도 체크리스트가 없으면 같은 실수를 반복합니다. 참고로 인증 오류를 체계적으로 점검하는 방식은 OpenAI Responses API 401 403 인증오류 점검 가이드 같은 글의 접근법이 CI 문제 분석에도 그대로 도움이 됩니다.


GitLab CI 캐시가 “꼬였다”는 신호들

다음 증상 중 2개 이상이면 캐시/워크스페이스 오염을 의심해볼 만합니다.

  • 동일 커밋/동일 파이프라인이라도 재시도(retry) 때 성공/실패가 바뀜
  • 의존성 설치 단계에서 파일이 “존재해야 하는데 없음” 또는 “이미 있는데 권한/형식이 다름”
  • node_modules, .gradle, .m2, .cache/pip 등이 부분만 존재하거나 깨진 lockfile 상태
  • Docker 빌드에서 이전 레이어가 잘못 재사용되어 실제 소스 변경이 반영되지 않음
  • 캐시를 지우면 바로 해결되지만, 며칠 내 재발

핵심은 캐시가 ‘가속’이 아니라 ‘상태(state)’가 되어버린 상황입니다. 캐시는 언제든 날아가도 빌드가 재현 가능해야 합니다.


캐시/아티팩트/워크스페이스 차이부터 정리

GitLab CI에서 자주 혼동되는 개념을 짚고 넘어가면 초기화 범위를 정확히 잡을 수 있습니다.

cache

  • 목적: 다음 파이프라인/잡에서 재사용할 “속도 최적화” 데이터
  • 저장 위치: Runner 설정에 따라 로컬 디스크 또는 S3/GCS 같은 외부 스토리지
  • 특징: 키(key)로 식별, 브랜치/커밋/잡 간 공유 가능

artifacts

  • 목적: 잡 결과물을 다음 잡으로 전달(예: 빌드 산출물, 테스트 리포트)
  • 저장 위치: GitLab 서버(또는 Object Storage)
  • 특징: 만료(expire_in) 기반, 파이프라인 단위로 추적

Runner workdir (워크스페이스)

  • 목적: 잡이 실제로 실행되는 작업 디렉터리
  • 특징: Executor에 따라 다름
    • Docker/Kubernetes executor: 보통 매 잡마다 깨끗한 컨테이너/파드로 시작(단, 볼륨 마운트/캐시 볼륨 사용 시 예외)
    • Shell executor: 이전 작업 디렉터리가 남기 쉬움 → 꼬임의 주요 원인

“완전 초기화”는 보통 cache + workdir + (필요 시) artifacts + (필요 시) 외부 스토리지까지를 의미합니다.


1단계: 문제를 재현 가능하게 만들기(캐시를 끄고 확인)

초기화 전에 가장 먼저 해야 할 일은 “정말 캐시 문제인지”를 분리하는 것입니다. 다음처럼 캐시를 임시로 끈 파이프라인을 한 번 돌려보세요.

방법 A) .gitlab-ci.yml에서 캐시 비활성화(임시)

build:
  stage: build
  cache: []
  script:
    - npm ci
    - npm run build

방법 B) 캐시 키를 강제로 변경(버전 bump)

cache:
  key: "v3-${CI_PROJECT_ID}-${CI_DEFAULT_BRANCH}"
  paths:
    - node_modules/
  • 캐시 비활성화/키 변경 후 문제가 사라지면, 원인은 거의 확실히 캐시/워크스페이스입니다.
  • 그래도 깨지면, 캐시가 아니라 환경/의존성/권한/네트워크 문제일 수 있습니다.

2단계: 캐시 키 설계가 꼬임을 만드는 대표 패턴

완전 초기화만으로는 재발을 막기 어렵습니다. “왜 꼬였는지”를 키 설계 관점에서 확인하세요.

나쁜 예 1) 프로젝트 전체가 하나의 캐시를 공유

cache:
  key: "global"
  paths:
    - node_modules/
  • 브랜치/커밋/런타임 버전이 다른 빌드가 같은 캐시를 덮어씁니다.

나쁜 예 2) lockfile 변경을 키에 반영하지 않음

  • package-lock.json, yarn.lock, poetry.lock, gradle.lockfile 등이 바뀌어도 캐시가 그대로면 충돌 확률이 높습니다.

권장 예) lockfile 기반 키 + 브랜치 범위 제한

GitLab은 cache:key:files를 지원합니다(파일 해시 기반).

cache:
  key:
    files:
      - package-lock.json
    prefix: "node-${CI_COMMIT_REF_SLUG}"
  paths:
    - .npm/

여기서 포인트:

  • node_modules/ 자체를 캐시하기보다 **패키지 매니저 다운로드 캐시(.npm, .yarn/cache)**를 캐시하면 오염 위험이 줄어듭니다.
  • 브랜치 단위(prefix)로 격리하면 “feature 브랜치가 main 캐시를 오염”시키는 일을 줄입니다.

3단계: GitLab UI에서 프로젝트 캐시/아티팩트 정리

가장 빠른 초기화는 프로젝트 단에서 시작합니다.

캐시 정리

  • GitLab 프로젝트 → CI/CD → (버전에 따라) Pipelines 또는 Jobs 관련 메뉴
  • Clear runner caches(러너 캐시 삭제) 버튼이 제공되는 경우가 있습니다.

다만 주의할 점:

  • 이 버튼은 “프로젝트 관점”에서 캐시를 지우는 것이며, 실제로는 Runner/스토리지 구성에 따라 일부만 삭제될 수 있습니다.

아티팩트 정리

  • 깨진 산출물이 downstream 잡에 영향을 주는 경우(예: 잘못된 빌드 결과가 배포 잡에 전달)
  • 해당 파이프라인/잡의 artifacts를 지우거나, expire_in을 줄여 재발 영향을 낮춥니다.
test:
  stage: test
  artifacts:
    when: always
    expire_in: 1 day
    paths:
      - junit.xml

4단계: Runner 워크스페이스 “완전 청소” (특히 Shell Executor)

캐시보다 더 위험한 게 Shell executor의 잔존 파일입니다. 같은 디렉터리에서 git clean이 제대로 안 되면, 이전 빌드의 산출물이 다음 빌드에 섞입니다.

잡에서 강제 청소(응급처치)

before_script:
  - git reset --hard
  - git clean -ffdx
  • -x.gitignore된 파일까지 지웁니다(빌드 산출물/캐시 파일이 남아있는 문제를 해결).
  • -d는 디렉터리 삭제.

Runner 머신에서 workdir 삭제(근본)

Runner의 config.toml에 따라 빌드 디렉터리가 다르지만, 보통 아래 중 하나입니다.

  • /builds/<group>/<project>
  • C:\GitLab-Runner\builds\...

Runner 호스트에서:

sudo systemctl stop gitlab-runner
sudo rm -rf /builds/*
sudo systemctl start gitlab-runner

> systemd로 관리되는 Runner가 재시작 루프에 빠지는 경우도 있습니다. 그때는 CI 문제가 아니라 서비스 관리 문제일 수 있으니 systemd 서비스 무한 재시작 - Exit code 203 해결처럼 유닛 파일/실행 경로를 점검하세요.


5단계: Docker/Kubernetes Executor에서의 “숨은 캐시” 제거

Docker/K8s executor는 매번 깨끗해 보이지만, 다음 요소가 상태를 남깁니다.

(1) Docker 레이어 캐시

Runner가 Docker-in-Docker(dind) 또는 호스트 Docker를 공유하면 레이어 캐시가 남습니다.

  • 호스트에서 정리:
docker system prune -af --volumes
  • 특정 이미지/빌더만 정리(더 안전):
docker builder prune -af

(2) Kubernetes PVC/EmptyDir 캐시 볼륨

K8s executor에서 캐시를 PVC로 붙여 쓰면, 파드가 바뀌어도 캐시는 남습니다. 이 경우 “프로젝트 캐시 삭제”만으로는 해결이 안 됩니다.

  • 캐시 PVC를 식별하고 삭제하거나
  • 캐시 경로를 바꾸거나
  • 캐시 키를 강제 롤링(prefix 버전업)

로 대응합니다.


6단계: 외부 캐시 스토리지(S3/GCS)까지 완전 초기화

Runner 캐시를 S3에 저장하는 구성은 대규모에서 흔합니다. 이때는 GitLab UI에서 지워도 S3 오브젝트가 남거나, 다른 Runner가 같은 버킷을 공유할 수 있습니다.

S3 캐시 오브젝트 삭제(예시)

버킷/프리픽스는 환경마다 다르지만, 보통 runner/<runner-id>/project/<project-id>/... 형태입니다.

aws s3 rm s3://my-gitlab-runner-cache/ --recursive --exclude "*" --include "*project/1234*"

권장 운영 팁:

  • 버킷 정책/라이프사이클로 캐시 TTL을 강제
  • 프로젝트/브랜치/런타임별 prefix를 명확히

7단계: “완전 초기화”를 파이프라인에서 안전하게 실행하는 패턴

운영 중에 매번 수동으로 지우기 어렵다면, 초기화 전용 잡을 만들어 필요할 때만 실행하는 패턴이 유용합니다.

예: 수동 트리거로 캐시 키 롤링 + 청소 잡

stages:
  - prepare
  - build

variables:
  CACHE_VERSION: "v1"  # 필요 시 v2, v3로 올려 전체 캐시 무효화

cache:
  key:
    files:
      - package-lock.json
    prefix: "${CACHE_VERSION}-${CI_COMMIT_REF_SLUG}"
  paths:
    - .npm/

clean_workspace:
  stage: prepare
  when: manual
  allow_failure: false
  script:
    - echo "Cleaning workspace..."
    - git reset --hard
    - git clean -ffdx
    - rm -rf node_modules dist .cache || true

build:
  stage: build
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run build

이 구성의 장점:

  • 평소에는 캐시 이점을 유지
  • 문제가 생길 때만 clean_workspace를 눌러 강제 초기화
  • 그래도 재발하면 CACHE_VERSION을 올려 “전체 캐시 무효화”를 즉시 수행

8단계: 재발 방지 체크리스트

마지막으로, 캐시 꼬임이 반복되지 않게 만드는 운영 체크리스트입니다.

  1. 캐시는 상태가 아니라 가속이어야 한다
    • 캐시가 없어도 빌드는 재현 가능해야 합니다(npm ci, pip install --require-hashes, gradle --refresh-dependencies 등).
  2. 키는 lockfile/런타임/브랜치 기준으로 분리
    • Node 버전이 바뀌면 네이티브 모듈이 깨질 수 있으니 node-${NODE_VERSION} 같은 prefix를 고려.
  3. node_modules/ 같은 결과물 캐시를 최소화
    • 다운로드 캐시(.npm, .m2, .gradle/caches) 중심으로.
  4. Shell executor라면 workdir 청소를 기본값으로
    • git clean -ffdx를 기본 방어선으로 두세요.
  5. 외부 스토리지 캐시는 TTL을 강제
    • S3 lifecycle로 오래된 캐시 자동 삭제.

결론

GitLab CI에서 “캐시가 꼬였다”는 문제는 단순히 캐시를 한 번 지우는 것으로 끝나지 않는 경우가 많습니다. (1) 캐시 키 설계 오류로 인한 오염, (2) Runner 워크스페이스 잔존, (3) Docker/K8s 레이어·볼륨·외부 스토리지에 남은 숨은 캐시가 겹치면, 증상은 랜덤하게 보이고 재현이 어려워집니다.

따라서 해결 순서는 다음이 가장 효율적입니다.

  • 캐시 비활성화/키 변경으로 원인 분리 →
  • 프로젝트 캐시/아티팩트 정리 →
  • Runner workdir 청소(특히 Shell) →
  • Docker/K8s 숨은 캐시 제거 →
  • S3/GCS까지 포함한 완전 초기화 →
  • 키 설계/TTL/워크스페이스 정책으로 재발 방지

이 절차대로 하면 “지웠더니 잠깐 낫다”가 아니라, 안정적으로 재현 가능한 CI에 가까워질 것입니다.