LabHub
学习 学习路径 课程

GitLab CI/CD

放进缓存的构建产物在部署作业里不见了

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

파이프라인을 여러 번 돌려 캐시가 맞고 빗나가고 무효화되는 것을 로그로 보고, 산출물의 제외·받는 쪽 제한·dotenv 값 전달을 확인한 뒤 산출물 점검 스크립트를 만듭니다.

왜 중요한가

산출물은 없으면 뒤 잡이 실패해야 맞고, 캐시는 없어도 모든 잡이 돌아야 맞습니다. 둘을 섞으면 캐시가 비는 날 배포 잡이 빈 디렉터리를 조용히 내보내거나, 만료 없는 산출물이 저장소 용량을 채웁니다. 캐시는 키를 무엇으로 만드느냐가 전부라서, 키를 고정 문자열로 두면 낡은 의존성을 끌고 다니고 소비자까지 올리게 두면 오염이 번집니다. 산출물은 '무엇이 올라갔나' 를 사람이 매번 확인할 수 없으니 점검을 자동화해 두는 편이 안전합니다.

단계

  1. /root/glci-transfer 을 git 저장소로 만들고(.gitignore 에 .gitlab-ci-local/), .gitlab-ci.yml 에 stages [build, test, deploy] 와 잡 둘을 두세요. package(build)는 dist/app.tgzdist/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 이어야 합니다.
  2. docs(build)를 더해 public/index.html 을 만들고 artifacts 로 public/(expire_in 2 days)을 올리세요. ship 에는 dependencies: [package] 를 두어 package 의 산출물만 받게 합니다. 실행하면 ship 로그가 여전히 has-tgz·no-map·no-public 이어야 합니다.
  3. requirements.txtrequests==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 가 나는지도 봅니다.
  4. 잡 둘을 더하세요. 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 가 나와야 합니다.
  5. 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 이어야 합니다.
  6. 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-<짧은 커밋 해시> 여야 합니다.
  7. /root/glci-transfer/artifact-audit.sh <저장소> 를 만드세요. 저장소를 임시 사본으로 떠 모든 파일을 커밋하고 파이프라인을 돌린 뒤, 올라간 산출물(.gitlab-ci-local/artifacts 아래)에 *.map·*.env·*.pem 이나 1MiB 가 넘는 파일이 있으면 BAD <잡>/<경로> …(공백 구분, 정렬) 한 줄과 3, 없으면 OK 와 0, 파이프라인이 실패하면 ERROR 와 1 로 끝냅니다. 리포트(.gitlab-ci-reports/ 아래)는 점검에서 뺍니다. 원래 저장소에는 아무것도 남기지 않습니다. 채점기는 이 저장소(OK)와 산출물에 새는 파일을 넣은 사본으로 확인합니다.

참고

산출물은 필요한 것만 올린다

/root/glci-transfer 을 git 저장소로 만들고(.gitignore 에 .gitlab-ci-local/), .gitlab-ci.yml 에 stages [build, test, deploy] 와 잡 둘을 두세요. package(build)는 dist/app.tgzdist/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.txtrequests==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 단위로 셀 수 있습니다.