キャッシュに入れたビルド結果がデプロイジョブで消えた
한국어 원문으로 표시합니다.
목표
파이프라인을 여러 번 돌려 캐시가 맞고 빗나가고 무효화되는 것을 로그로 보고, 산출물의 제외·받는 쪽 제한·dotenv 값 전달을 확인한 뒤 산출물 점검 스크립트를 만듭니다.
왜 중요한가
산출물은 없으면 뒤 잡이 실패해야 맞고, 캐시는 없어도 모든 잡이 돌아야 맞습니다. 둘을 섞으면 캐시가 비는 날 배포 잡이 빈 디렉터리를 조용히 내보내거나, 만료 없는 산출물이 저장소 용량을 채웁니다. 캐시는 키를 무엇으로 만드느냐가 전부라서, 키를 고정 문자열로 두면 낡은 의존성을 끌고 다니고 소비자까지 올리게 두면 오염이 번집니다. 산출물은 '무엇이 올라갔나' 를 사람이 매번 확인할 수 없으니 점검을 자동화해 두는 편이 안전합니다.
단계
/root/glci-transfer을 git 저장소로 만들고(.gitignore 에.gitlab-ci-local/),.gitlab-ci.yml에 stages[build, test, deploy]와 잡 둘을 두세요.package(build)는dist/app.tgz와dist/app.js.map을 만들고 artifacts 로dist/를 올리되dist/*.map은 제외하고expire_in: 1 week를 둡니다.ship(deploy)은test -f dist/app.tgz && echo has-tgz,test -e dist/app.js.map && echo has-map || echo no-map,test -d public && echo has-public || echo no-public를 실행합니다. 실행하면 ship 로그가 has-tgz·no-map·no-public 이어야 합니다.- 잡
docs(build)를 더해public/index.html을 만들고 artifacts 로public/(expire_in 2 days)을 올리세요.ship에는dependencies: [package]를 두어 package 의 산출물만 받게 합니다. 실행하면 ship 로그가 여전히 has-tgz·no-map·no-public 이어야 합니다. requirements.txt에requests==2.32.3한 줄을 두고 커밋 대상에 넣으세요. 잡deps(build)는 cache 를key: files: [requirements.txt],paths: [.deps/],policy: pull-push로 두고,mkdir -p .deps뒤.deps/installed가 있으면cache-hit, 없으면cache-miss를 출력하고 그 파일을 만듭니다. 두 번 연속 실행하면 miss 다음 hit 이 나와야 합니다. 채점기는 사본에서 requirements.txt 를 바꿔 다시 miss 가 나는지도 봅니다.- 잡 둘을 더하세요.
unit(test)은 deps 와 같은 키·경로의 cache 를policy: pull로 두고.deps/installed가 있으면unit-hit, 없으면unit-miss를 출력한 뒤.deps/junk를 만듭니다.verify-cache(deploy)도 같은 캐시를 pull 로 받아.deps/junk가 있으면junk-saved, 없으면junk-not-saved를 출력합니다. 실행하면 unit-hit 과 junk-not-saved 가 나와야 합니다. - deps 의 cache 를 목록 두 개로 바꾸세요. 첫째는
key: files: [requirements.txt]에prefix: py를 더한.deps/(pull-push), 둘째는key: tools-$CI_COMMIT_REF_SLUG인.tools/입니다. deps 스크립트에.tools/lint가 있으면tools-hit, 없으면tools-miss를 출력하고 만드는 줄을 더합니다. unit·verify-cache 의 키에도 같은 prefix 를 둡니다. 두 번 실행하면 둘째 실행에서 두 캐시가 모두 hit 이어야 합니다. - 잡
version(build)이VERSION=1.4.2-${CI_COMMIT_SHORT_SHA}한 줄을build.env에 쓰고artifacts: reports: dotenv: build.env로 올리게 하세요. 잡announce(deploy)는needs: [version]으로echo "version=$VERSION"을 실행합니다. 실행하면 announce 로그가version=1.4.2-<짧은 커밋 해시>여야 합니다. /root/glci-transfer/artifact-audit.sh <저장소>를 만드세요. 저장소를 임시 사본으로 떠 모든 파일을 커밋하고 파이프라인을 돌린 뒤, 올라간 산출물(.gitlab-ci-local/artifacts아래)에*.map·*.env·*.pem이나 1MiB 가 넘는 파일이 있으면BAD <잡>/<경로> …(공백 구분, 정렬) 한 줄과 3, 없으면OK와 0, 파이프라인이 실패하면ERROR와 1 로 끝냅니다. 리포트(.gitlab-ci-reports/아래)는 점검에서 뺍니다. 원래 저장소에는 아무것도 남기지 않습니다. 채점기는 이 저장소(OK)와 산출물에 새는 파일을 넣은 사본으로 확인합니다.
참고
- 이 VM 에는 GitLab 서버와 러너가 없고, gitlab-ci-local 4.75.1 이 .gitlab-ci.yml 을 GitLab 과 같은 규칙으로 해석해 shell 로 잡을 실행합니다.
image:를 적으면 도커로 돌리려 하므로 쓰지 않습니다. 보호 변수·마스킹·CI_JOB_TOKEN·러너 태그·병합 요청 파이프라인 생성은 서버 기능이라 여기서 재현되지 않습니다. - 실행: 저장소 루트에서
gitlab-ci-local --shell-isolation --no-artifacts-to-source(잡마다 따로 된 작업 디렉터리, 산출물을 저장소에 되쓰지 않음), 잡 목록:gitlab-ci-local --list-csv-all, 해석된 설정:gitlab-ci-local --preview. gitlab-ci-local 은 git 이 추적하는 파일만 잡에 넘기므로 파일을 만들면git add하세요. 채점기는 저장소를 사본으로 떠 모든 파일을 커밋한 뒤 같은 도구로 다시 돌립니다. - 캐시는 저장소의
.gitlab-ci-local/cache/<키>에 쌓입니다. '러너가 바뀌어 캐시가 없는 상황' 은 이 디렉터리를 지우고 다시 돌려 보면 됩니다. artifacts:when: on_failure 는 gitlab-ci-local 에서 재현되지 않습니다(실측). - 흔한 실수: 소비자 잡을 pull-push 로 두어 매번 캐시를 다시 올리는 것. 흔한 실수: dotenv 로 넘긴 값을 뒤 잡의 rules 에서 쓰려는 것.
- Caching in GitLab CI/CD · Job artifacts · artifacts:reports(dotenv) · CI/CD YAML syntax reference
산출물은 필요한 것만 올린다
/root/glci-transfer 을 git 저장소로 만들고(.gitignore 에 .gitlab-ci-local/), .gitlab-ci.yml 에 stages [build, test, deploy] 와 잡 둘을 두세요. package(build)는 dist/app.tgz 와 dist/app.js.map 을 만들고 artifacts 로 dist/ 를 올리되 dist/*.map 은 제외하고 expire_in: 1 week 를 둡니다. ship(deploy)은 test -f dist/app.tgz && echo has-tgz, test -e dist/app.js.map && echo has-map || echo no-map, test -d public && echo has-public || echo no-public 를 실행합니다. 실행하면 ship 로그가 has-tgz·no-map·no-public 이어야 합니다.
artifacts:exclude 는 paths 로 고른 것 가운데 올리지 않을 파일을 뺍니다. 소스맵·디버그 기호처럼 크고 배포에 필요 없는 파일이 매 파이프라인마다 쌓이는 것을 막습니다. expire_in 은 언제 지울지이고, 실제 삭제는 서버가 합니다.
배포 잡은 쓰지도 않을 문서까지 받지 않는다
잡 docs(build)를 더해 public/index.html 을 만들고 artifacts 로 public/(expire_in 2 days)을 올리세요. ship 에는 dependencies: [package] 를 두어 package 의 산출물만 받게 합니다. 실행하면 ship 로그가 여전히 has-tgz·no-map·no-public 이어야 합니다.
needs 도 dependencies 도 없는 잡은 앞 스테이지의 산출물을 전부 내려받습니다. 배포 잡이 테스트 리포트·문서까지 받느라 느려지는 흔한 원인입니다. dependencies 는 순서는 스테이지대로 두고 받을 산출물만 고릅니다.
잠금 파일이 바뀌면 캐시도 바뀐다
requirements.txt 에 requests==2.32.3 한 줄을 두고 커밋 대상에 넣으세요. 잡 deps(build)는 cache 를 key: files: [requirements.txt], paths: [.deps/], policy: pull-push 로 두고, mkdir -p .deps 뒤 .deps/installed 가 있으면 cache-hit, 없으면 cache-miss 를 출력하고 그 파일을 만듭니다. 두 번 연속 실행하면 miss 다음 hit 이 나와야 합니다. 채점기는 사본에서 requirements.txt 를 바꿔 다시 miss 가 나는지도 봅니다.
캐시 키에 파일 목록을 주면 그 파일 내용의 해시가 키가 됩니다. 의존성 목록이 바뀌면 키가 저절로 달라져 낡은 캐시를 끌고 다니지 않습니다. 캐시가 없어도 잡이 끝까지 돌 수 있게 쓰는 것이 캐시의 계약입니다.
소비자는 캐시를 받기만 한다
잡 둘을 더하세요. unit(test)은 deps 와 같은 키·경로의 cache 를 policy: pull 로 두고 .deps/installed 가 있으면 unit-hit, 없으면 unit-miss 를 출력한 뒤 .deps/junk 를 만듭니다. verify-cache(deploy)도 같은 캐시를 pull 로 받아 .deps/junk 가 있으면 junk-saved, 없으면 junk-not-saved 를 출력합니다. 실행하면 unit-hit 과 junk-not-saved 가 나와야 합니다.
pull 은 시작할 때 받기만 하고 끝날 때 올리지 않습니다. 캐시를 만드는 잡 하나만 pull-push 로 두면 소비자들이 같은 내용을 다시 압축해 올리는 시간이 사라지고, 소비자가 캐시를 오염시키는 일도 막힙니다.
수명이 다른 캐시는 키를 나눈다
deps 의 cache 를 목록 두 개로 바꾸세요. 첫째는 key: files: [requirements.txt] 에 prefix: py 를 더한 .deps/(pull-push), 둘째는 key: tools-$CI_COMMIT_REF_SLUG 인 .tools/ 입니다. deps 스크립트에 .tools/lint 가 있으면 tools-hit, 없으면 tools-miss 를 출력하고 만드는 줄을 더합니다. unit·verify-cache 의 키에도 같은 prefix 를 둡니다. 두 번 실행하면 둘째 실행에서 두 캐시가 모두 hit 이어야 합니다.
잡 하나에 캐시를 여러 개 둘 수 있습니다. 의존성처럼 잠금 파일을 따라 바뀌는 것과, 브랜치마다 따로 두고 싶은 도구 캐시를 한 키에 섞으면 한쪽 변경이 다른 쪽까지 무효화합니다. prefix 는 같은 파일 해시를 쓰는 다른 캐시와 이름이 겹치지 않게 합니다.
앞 잡이 계산한 값을 뒤 잡에 넘긴다
잡 version(build)이 VERSION=1.4.2-${CI_COMMIT_SHORT_SHA} 한 줄을 build.env 에 쓰고 artifacts: reports: dotenv: build.env 로 올리게 하세요. 잡 announce(deploy)는 needs: [version] 으로 echo "version=$VERSION" 을 실행합니다. 실행하면 announce 로그가 version=1.4.2-<짧은 커밋 해시> 여야 합니다.
변수는 파이프라인을 만들 때 정해지므로, 실행 중에 계산한 값은 보통 방법으로는 뒤 잡에 못 넘깁니다. dotenv 리포트는 산출물로 올라간 KEY=VALUE 파일을 뒤 잡의 환경 변수로 넣어 줍니다. rules 는 이미 평가가 끝나 이 값을 볼 수 없습니다.
산출물에 들어가면 안 되는 파일을 잡아낸다
/root/glci-transfer/artifact-audit.sh <저장소> 를 만드세요. 저장소를 임시 사본으로 떠 모든 파일을 커밋하고 파이프라인을 돌린 뒤, 올라간 산출물(.gitlab-ci-local/artifacts 아래)에 *.map·*.env·*.pem 이나 1MiB 가 넘는 파일이 있으면 BAD <잡>/<경로> …(공백 구분, 정렬) 한 줄과 3, 없으면 OK 와 0, 파이프라인이 실패하면 ERROR 와 1 로 끝냅니다. 리포트(.gitlab-ci-reports/ 아래)는 점검에서 뺍니다. 원래 저장소에는 아무것도 남기지 않습니다. 채점기는 이 저장소(OK)와 산출물에 새는 파일을 넣은 사본으로 확인합니다.
gitlab-ci-local 은 잡마다 올린 산출물을 .gitlab-ci-local/artifacts/<잡이름>/ 아래에 둡니다. dotenv 리포트는 일부러 넘기는 값이라 gitlab-ci-local 이 .gitlab-ci-reports/ 아래에 따로 두는데, 점검에서 이 자리는 빼야 정상 설정이 BAD 가 되지 않습니다. find 의 -size 는 k 단위로 셀 수 있습니다.