- Published on
GitHub Actions 캐시가 안 먹을 때 - key·restore-keys·권한
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
GitHub Actions에서 actions/cache를 붙였는데도 매번 Cache not found가 뜨거나, 분명 Cache restored가 떴는데 빌드가 여전히 느린 경우가 있습니다. 이 문제는 "캐시가 없다"가 아니라, 캐시 키 설계가 매번 달라지거나, restore-keys가 기대와 다르게 동작하거나, 권한/이벤트 제약으로 저장 자체가 안 되는 경우가 대부분입니다.
이 글에서는 캐시가 안 먹는 상황을 로그로 확정하고, key/restore-keys를 올바르게 설계하며, 권한과 이벤트(특히 PR) 제약까지 포함해 실전에서 안정적으로 캐시를 굴리는 방법을 정리합니다.
1) 먼저 로그로 “안 먹는 이유”를 분류하기
actions/cache@v4는 로그에 힌트를 꽤 많이 남깁니다. 아래 문구를 기준으로 원인을 빠르게 분류하세요.
1-1. 아예 복원이 안 됨: Cache not found
Cache not found for input keys: ...- key가 매번 바뀌는지
- restore-keys가 너무 구체적이라 매칭이 안 되는지
- 경로(path)가 잘못됐는지
- OS/아키텍처가 바뀌었는지(특히 self-hosted)
1-2. 복원은 됐는데 저장이 안 됨: “not saved” 류
Cache restored from key: ...는 떴는데 다음 실행에서 다시 miss- 저장 단계에서:
Cache save failed: ...(권한/용량/경로)Cache not saved because...(이미 동일 key 존재, 조건문, 실패로 step skip)
- 저장 단계에서:
1-3. 복원은 됐는데 효과가 없음
- 캐시 대상이 실제로 빌드가 참조하는 디렉터리가 아닌 경우
- 패키지 매니저가 캐시를 무시하도록 설정된 경우
- lockfile이 바뀌어 재설치가 발생하는 경우
캐시 문제는 네트워크/권한/만료처럼 “인프라성” 원인도 많습니다. 비슷한 결의 권한/토큰 이슈 트러블슈팅은 EKS ImagePullBackOff 403 - ECR 권한·토큰 만료 해결처럼 원인 분류 → 로그로 확정 → 최소 재현 순서로 접근하면 빨라집니다.
2) key가 매번 바뀌는 대표 패턴 (캐시 miss 1순위)
캐시가 안 먹는 가장 흔한 이유는 key가 안정적이지 않아서입니다. 특히 아래 패턴을 조심하세요.
2-1. 커밋 SHA, run_id를 key에 넣는 실수
다음처럼 하면 매 실행마다 key가 달라져서 100% miss입니다.
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ github.sha }}
SHA는 “정확히 동일한 결과물”에만 캐시를 쓰고 싶을 때 일부러 쓰는 경우가 있지만, 의존성 캐시에는 보통 과합니다.
2-2. 너무 넓은 hashFiles / 너무 좁은 hashFiles
- 너무 넓음:
**/*같은 범위는 README 변경에도 캐시가 깨집니다. - 너무 좁음: lockfile 외에 실제 의존성에 영향을 주는 파일을 빼먹으면, 복원은 되지만 설치가 다시 일어나거나(무효화), 더 나쁘게는 “오염된 캐시”를 쓰게 됩니다.
권장: 의존성 캐시는 lockfile 중심
- Node:
package-lock.json,pnpm-lock.yaml,yarn.lock - Python:
poetry.lock,requirements.txt(+ constraints),uv.lock - Rust:
Cargo.lock
2-3. 멀티 OS/멀티 버전에서 키에 버전이 빠짐
Node 18과 20을 매트릭스로 돌리는데 key에 node 버전을 포함하지 않으면, 서로 다른 런타임이 같은 캐시를 공유하며 예측 불가능해집니다.
3) restore-keys 동작 방식: “부분 일치”가 아니라 “prefix 매칭”
restore-keys는 prefix(접두사) 매칭입니다. 그리고 GitHub는 “가장 최근에 저장된 캐시”를 우선적으로 찾습니다.
3-1. restore-keys를 제대로 설계하는 기본형
아래는 Python pip 캐시의 안정적인 패턴입니다.
- name: Cache pip
id: cache-pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-py${{ matrix.python-version }}-
pip-${{ runner.os }}-
- name: Install deps
run: pip install -r requirements.txt
핵심:
key는 “정확히 이 lockfile 조합이면 이 캐시”restore-keys는 “lockfile이 바뀌었어도 최대한 가까운 캐시를 가져와서 다운로드량을 줄이자”
3-2. restore-keys를 너무 구체적으로 쓰면 의미가 사라짐
restore-keys: |
pip-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
이건 사실상 key와 동일해서 miss면 restore도 miss입니다.
3-3. restore-keys가 “항상 최신 캐시”를 가져오는 함정
prefix가 넓을수록(예: pip-linux-) 의도치 않게 오래된/다른 브랜치의 캐시를 가져올 수 있습니다.
대규모 모노레포라면 브랜치/워크스페이스 단위 prefix를 고려하세요.
restore-keys: |
pnpm-${{ runner.os }}-${{ github.ref_name }}-
pnpm-${{ runner.os }}-
단, 브랜치명을 넣으면 브랜치가 많을 때 캐시가 분산되어 효율이 떨어질 수 있습니다. “속도 vs 히트율 vs 오염 위험”의 트레이드오프입니다.
4) 권한/이벤트 제약: PR에서 캐시가 저장되지 않는 케이스
캐시는 복원(restore) 과 저장(save) 이 별개입니다. 특히 저장은 권한/이벤트에 영향을 받습니다.
4-1. pull_request(fork)에서는 저장이 막히는 경우가 많다
외부 fork에서 들어오는 PR은 보안상 토큰 권한이 제한됩니다. 이때는 캐시 저장이 실패하거나 아예 시도되지 않을 수 있습니다.
대응 전략:
- fork PR에서는 캐시 restore만 허용하고 save는 하지 않기
- 또는
pull_request_target를 쓰되(주의 필요), 체크아웃/스크립트 실행 보안 모델을 엄격히 분리
예: fork PR에서는 저장 스텝을 건너뛰기
- name: Cache node_modules
id: cache-node
uses: actions/cache@v4
with:
path: |
~/.npm
.next/cache
key: node-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
node-${{ runner.os }}-
- name: Build
run: npm ci && npm run build
# 저장은 actions/cache가 자동으로 처리하지만,
# 보안/권한 이슈가 있다면 아예 job 자체를 분리하거나
# fork PR에서는 캐시 job을 돌리지 않는 방식이 안전합니다.
4-2. GITHUB_TOKEN permissions 설정 확인
일부 조직/레포는 기본 권한이 축소되어 있습니다. 워크플로 상단에 명시적으로 권한을 선언해 문제를 줄일 수 있습니다.
permissions:
contents: read
actions: read
캐시는 내부적으로 Actions 서비스와 상호작용합니다. 환경에 따라 actions: write가 필요하다고 오해하는 경우가 있는데, 보통은 그렇지 않습니다. 다만 조직 정책/엔터프라이즈 설정으로 인해 제약이 생길 수 있으니, 실패 로그의 HTTP 코드/메시지를 기준으로 판단하세요.
권한 문제는 증상이 “그냥 안 됨”으로 나타나서 시간을 많이 잡아먹습니다. AWS에서 STS 토큰 만료로 403이 나는 케이스처럼(EKS Pod에서 STS 403 ExpiredToken 해결법) 실패 조건을 먼저 확정하는 게 핵심입니다.
5) path가 틀리면: 복원은 됐는데 빌드는 여전히 느리다
캐시의 path는 “존재하는 디렉터리”이면서 “실제로 도구가 읽는 위치”여야 합니다.
5-1. Node/Next.js에서 자주 하는 실수
node_modules를 캐시: 가능은 하지만 용량이 크고 깨지기 쉬움- 더 나은 선택: 패키지 매니저 캐시 + 빌드 캐시
예: Next.js는 .next/cache가 체감이 큽니다.
- uses: actions/cache@v4
with:
path: |
~/.npm
.next/cache
key: next-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('next.config.*', 'tsconfig.json') }}
restore-keys: |
next-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-
next-${{ runner.os }}-
Next.js 빌드/하이드레이션 이슈를 잡는 과정에서도 “캐시로 가려진 문제”가 나올 수 있습니다. 프론트 빌드가 이상하게 느리거나 경고가 반복되면 캐시를 의심하기 전에 증상을 분리해보세요: Next.js 14 Hydration failed 경고 10분 해결법
5-2. Python에서 pip/poetry/uv 캐시 위치 확인
- pip:
~/.cache/pip - poetry:
~/.cache/pypoetry또는~/Library/Caches/pypoetry(mac) - uv:
~/.cache/uv
도구 버전이 바뀌면 캐시 구조가 달라져 히트해도 효과가 줄 수 있습니다.
6) 캐시 키 설계 레시피 (실전용)
아래 원칙을 지키면 대부분의 “캐시 안 먹음”은 사라집니다.
6-1. 의존성 캐시 키는 lockfile 해시 + 런타임 버전
- Node:
key: node-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('pnpm-lock.yaml') }}
- Python:
key: pip-${{ runner.os }}-py${{ matrix.python }}-${{ hashFiles('requirements.txt') }}
6-2. restore-keys는 2단계 정도로만
너무 넓히면 오염 위험이 커집니다.
- 1단계: 같은 OS+런타임
- 2단계: 같은 OS
6-3. 캐시 대상은 “다운로드/컴파일 비용이 큰 것” 위주
- 패키지 매니저 다운로드 캐시
- 빌드 캐시(Next
.next/cache, Gradle~/.gradle/caches, Rusttarget는 신중)
6-4. 캐시가 커지면 오히려 느려진다
캐시는 압축/업로드/다운로드 비용이 있습니다.
- 캐시가 수 GB면 네트워크가 병목이 되어 이득이 줄어듭니다.
node_modules캐시가 대표적입니다.
7) “정말로 저장이 됐는지” 확인하는 최소 점검 체크리스트
- 같은 브랜치에서 연속 2번 실행해보기(첫 실행 save, 두 번째 restore 확인)
- 로그에서 아래를 확인
- restore:
Cache restored from key: - save: job 끝에서
Cache saved successfully
- restore:
- key에 변동 요소가 없는지 점검
${{ github.sha }},${{ github.run_id }}, 날짜 등을 제거
hashFiles()범위가 적절한지- lockfile/의존성 정의 파일만 포함
- PR(fork) 이벤트인지 확인
- 저장이 제한될 수 있음
- path가 실제로 존재하고, 도구가 참조하는 경로인지
8) 결론: 캐시 문제는 “키 설계 + 저장 조건” 문제다
GitHub Actions 캐시가 안 먹을 때는 대개 세 갈래입니다.
- key가 불안정해서 매번 miss
- restore-keys를 오해해서 매칭이 안 됨(혹은 너무 넓어서 오염)
- 권한/이벤트 제약으로 저장이 안 됨(특히 fork PR)
가장 빠른 해결법은 “추측으로 YAML을 바꾸기”가 아니라, 로그로 restore/save 여부를 먼저 확정하고, 그 다음에 key/restore-keys를 최소 단위로 단순화해 재구성하는 것입니다. 이 원칙만 지키면 캐시는 ‘가끔 되는 마법’이 아니라, 재현 가능한 성능 도구가 됩니다.