キャッシュに入れたビルド結果がデプロイジョブで消えた
目標
パイプラインを何度も動かして、キャッシュが当たったり外れたり無効化されたりする様子をログで見て、アーティファクトの除外・受け取る側の制限・dotenvの値の受け渡しを確認したあとで、アーティファクトの点検スクリプトを作ります。
なぜ重要なのか
アーティファクトはなければ後ろのジョブが失敗するのが正しく、キャッシュはなくてもすべてのジョブが動くのが正しいです。2つを混ぜると、キャッシュが空になる日にデプロイジョブが空のディレクトリを静かに出荷したり、有効期限のないアーティファクトがストレージの容量を埋めたりします。キャッシュはキーを何で作るかがすべてで、キーを固定文字列にすると古い依存関係を引きずり回し、利用側にまでアップロードさせると汚染が広がります。アーティファクトは、「何がアップロードされたか」を人が毎回確認することはできないので、点検を自動化しておくほうが安全です。
ステップ
/root/glci-transferをgitリポジトリにし(.gitignoreに.gitlab-ci-local/を置く)、.gitlab-ci.ymlにstages[build, test, deploy]とジョブ2つを置いてください。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を1行置き、コミット対象に入れてください。ジョブdeps(build)は、cacheをkey: files: [requirements.txt]、paths: [.deps/]、policy: pull-pushにし、mkdir -p .depsのあとで、.deps/installedがあればcache-hit、なければcache-missを出力して、そのファイルを作ります。2回続けて実行すると、missのあとにhitが出るはずです。採点ツールは、コピーでrequirements.txtを変えて、再びmissになるかも確認します。- ジョブを2つ追加してください。
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を、2つのリストに変えてください。1つ目は
key: files: [requirements.txt]にprefix: pyを加えた.deps/(pull-push)、2つ目はkey: tools-$CI_COMMIT_REF_SLUGの.tools/です。depsのスクリプトに、.tools/lintがあればtools-hit、なければtools-missを出力して、それを作る行を追加します。unit・verify-cacheのキーにも、同じprefixを置きます。2回実行すると、2回目の実行で2つのキャッシュがどちらもhitになるはずです。 - ジョブ
version(build)がVERSION=1.4.2-${CI_COMMIT_SHORT_SHA}を1行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 <잡>/<경로> …(空白区切り、ソート済み)を1行出力して3で終了します。なければOKを出力して0で、パイプラインが失敗すればERRORを出力して1で終了します(プレースホルダーは、順にリポジトリ、ジョブ名、パスです)。レポート(.gitlab-ci-reports/の下)は点検から除きます。元のリポジトリには何も残しません。採点ツールは、このリポジトリ(OK)と、アーティファクトに漏れるファイルを入れたコピーで確認します。
参考
- このVMにはGitLabサーバーもランナーもなく、gitlab-ci-local 4.75.1が.gitlab-ci.ymlをGitLabと同じルールで解釈し、シェルでジョブを実行します。
image:を書くとDockerで動かそうとするので使いません。保護変数・マスキング・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]とジョブ2つを置いてください。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を1行置き、コミット対象に入れてください。ジョブdeps(build)は、cacheをkey: files: [requirements.txt]、paths: [.deps/]、policy: pull-pushにし、mkdir -p .depsのあとで、.deps/installedがあればcache-hit、なければcache-missを出力して、そのファイルを作ります。2回続けて実行すると、missのあとにhitが出るはずです。採点ツールは、コピーでrequirements.txtを変えて、再びmissになるかも確認します。
キャッシュのキーにファイルの一覧を与えると、そのファイルの内容のハッシュがキーになります。依存関係の一覧が変わればキーがひとりでに変わるので、古いキャッシュを引きずり回しません。キャッシュがなくてもジョブが最後まで動けるように書くのが、キャッシュの約束です。
利用側はキャッシュを受け取るだけにする
ジョブを2つ追加してください。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は、開始時に受け取るだけで、終了時にアップロードしません。キャッシュを作るジョブ1つだけをpull-pushにすれば、利用側が同じ内容を再び圧縮してアップロードする時間がなくなり、利用側がキャッシュを汚染することも防げます。
寿命の違うキャッシュはキーを分ける
depsのcacheを、2つのリストに変えてください。1つ目はkey: files: [requirements.txt]にprefix: pyを加えた.deps/(pull-push)、2つ目はkey: tools-$CI_COMMIT_REF_SLUGの.tools/です。depsのスクリプトに、.tools/lintがあればtools-hit、なければtools-missを出力して、それを作る行を追加します。unit・verify-cacheのキーにも、同じprefixを置きます。2回実行すると、2回目の実行で2つのキャッシュがどちらもhitになるはずです。
1つのジョブにキャッシュを複数置けます。依存関係のようにロックファイルに従って変わるものと、ブランチごとに別々に置きたいツールのキャッシュを、1つのキーに混ぜると、片方の変更がもう片方まで無効化します。prefixは、同じファイルのハッシュを使う別のキャッシュと、名前が重ならないようにします。
前のジョブが計算した値を後ろのジョブに渡す
ジョブversion(build)がVERSION=1.4.2-${CI_COMMIT_SHORT_SHA}を1行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 <잡>/<경로> …(空白区切り、ソート済み)を1行出力して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単位で数えられます。