GitLab CI/CD · 실행 모델 — 순서·조건·전달 · 이론
artifacts 와 cache 는 다른 물건이다
한 줄 요약
artifacts 는 잡과 잡 사이로 건너가는 산출물이라 없으면 뒤 잡이 실패해야 맞고, cache 는 실행과 실행 사이에서 시간을 아끼는 임시 자산이라 없어도 모든 잡이 그냥 돌아야 맞다.
왜 이게 필요했나
둘 다 "디렉터리를 지정하면 어딘가에 보관됐다가 나중에 돌아온다"처럼 보인다. 그래서 처음에는 아무거나 쓰게 되고, 얼마 뒤에 두 종류의 사고가 난다.
캐시를 산출물처럼 쓴 팀은 어느 날 배포 잡이 빈 디렉터리를 배포한다. 캐시는 보장이 아니라 최적화라서 러너가 바뀌거나 만료되면 그냥 비어 있고, 그때 파이프라인은 실패하지 않고 아무것도 없는 상태로 조용히 성공한다. 반대로 산출물을 캐시처럼 쓴 팀은 저장소 용량 경고를 받는다. 만료를 정하지 않은 아티팩트가 커밋마다 쌓이기 때문이다.
어떻게 동작하나
artifacts 는 잡이 끝날 때 지정한 경로를 GitLab 서버로 올린다. 그리고 뒤 잡이 시작할 때 내려받는다. 여기서 중요한 것은 누가 내려받는가인데, 기본값이 "앞 스테이지 전부"라서 아무 설정도 안 하면 배포 잡이 쓰지도 않을 테스트 리포트까지 전부 내려받는다. 파이프라인이 느려지는 흔한 이유가 이것이다.
받는 쪽을 좁히는 방법은 needs 의 긴 형태다. needs 를 문자열 목록 대신 {job: build-app, artifacts: true} 형태로 쓰면 잡별로 받을지 말지를 끌 수 있다. 순서만 기다리고 파일은 필요 없는 잡에는 artifacts: false 를 명시한다. 그리고 expire_in 은 선택이 아니라 사실상 필수다. 되돌릴 대상이 되는 릴리스 산출물만 길게 두고 나머지는 며칠로 짧게 잡는다.
cache 는 다르다. 잡이 시작할 때 키에 맞는 압축 파일을 내려 풀고, 끝날 때 다시 올린다. 전부 최적화이므로 실패해도 잡은 계속 간다. 그래서 캐시 설계의 핵심은 무엇을 넣느냐가 아니라 키를 무엇으로 만드느냐다.
키를 고정 문자열로 두면 의존성이 바뀌어도 같은 키를 계속 써서 낡은 캐시를 끌고 다닌다. 그래서 잠금 파일의 해시를 키에 넣는다. key: {files: [requirements.txt]} 처럼 쓰면 그 파일이 바뀔 때 키가 저절로 달라져 캐시가 자동으로 무효화된다. 여기에 policy 를 곁들이면 한 번 더 아낀다. 캐시를 만들어 올리는 잡 하나만 pull-push 로 두고 나머지 소비자는 pull 로 두면, 소비자들이 끝날 때마다 같은 내용을 다시 압축해 올리는 시간이 통째로 사라진다.
마지막으로 두 목록이 겹치면 안 된다. 같은 디렉터리를 아티팩트로도 캐시로도 관리하면 어느 쪽이 최신인지 아무도 모르게 되고, 그 혼선은 재현이 어렵다.
현장에서 만나는 모습
캐시 관련 사고 중 가장 위험한 것은 성능이 아니라 신뢰다. 포크에서 온 병합 요청이 악의적인 의존성을 캐시에 심어 두면, 그 뒤의 빌드가 그 캐시를 그대로 가져다 쓴다. 캐시 포이즈닝이라고 부르는 이 문제 때문에 신뢰 경계를 넘는 캐시는 스코프를 분리한다.
용량 쪽도 현실적인 제약이다. 캐시 저장 공간에는 한도가 있고 넘으면 오래된 것부터 밀려난다. 그래서 커밋마다 새 캐시를 만들도록 키를 지나치게 잘게 쪼개면 캐시끼리 서로를 밀어내 히트율이 오히려 떨어진다. 키는 재사용 단위로 잡아야 한다.
다음 실습에서 할 것
지금까지 읽은 것을 설정 파일 한 장으로 직접 쌓아 올린다. 스테이지와 첫 잡에서 시작해 숨김 잡과 extends, needs 로 만드는 DAG, rules 의 브랜치 조건과 수동 승인, artifacts 와 cache 까지 여덟 단계로 붙여 나간다. 채점은 파일을 눈으로 훑는 것이 아니라 YAML 파서로 읽어 구조를 확인한다. 마지막 단계에서는 파이프라인 해석기를 직접 짜서, 처음 보는 설정에서도 잡이 몇 번째 웨이브에 출발하는지 계산하고 needs 순환을 잡아낸다.