CircleCIのジョブのスキップを使った依存関係のキャッシュ活用の効率化
CircleCI のチューニングを行う際、依存パッケージのインストール周りのジョブ(あるいは step)がそこそこ時間がかかるケースはよくある。 特にフロントエンドプロジェクトにおいては HEAVIEST OBJEXCTS IN THE UNIVERSE と揶揄されるように、依存パッケージ自体の数が膨大になりがちで、それに比例してインストール時間も伸びる。
circleci 公式 Doc にもあるように、この対策として依存管理ツールの作る *lock
ファイル(あるいは go.sum
など)のチェックサムを key
にして、 依存パッケージのキャッシュを CircleCI のキャッシュ機能 (save_cache
/ restore_cache
) を用いて保存/復元することで、依存インストールプロセスの実行時間の改善が期待できることがある。1
下記はキャッシュ活用した依存インストールの一例:
executors:
node:
docker:
- image: circleci/node:14
working_directory: /tmp/app
commands:
install_deps:
steps:
- restore_cache:
keys:
- &cache-key-yarn yarn-{{ .Environment.YARN_CACHE_KEY }}-{{ arch }}-{{ checksum "/tmp/app/yarn.lock" }}
- yarn-{{ .Environment.YARN_CACHE_KEY }}-{{ arch }}
- run: yarn --frozen-lockfile --silent --cache-folder ~.cache/yarn
- save_cache:
key: *cache-key-yarn
paths:
- ~/.cache/yarn
restore_deps:
steps:
- restore_cache:
keys:
- *cache-key-yarn
- run: yarn --frozen-lockfile --silent --cache-folder ~.cache/yarn
jobs:
setup:
executor: node
steps:
- checkout
- install_deps
lint:
executor: node
steps:
- checkout
- restore_deps
- run: yarn lint
unit_test:
executor: node
steps:
- checkout
- restore_deps
- run: yarn e2e-test
e2e_test:
executor: node
steps:
- checkout
- restore_deps
- run: yarn unit-test
workflows:
version: 2
build:
jobs:
- setup:
filters:
branches:
only: /.*/
- lint:
requires:
- setup
filters:
branches:
only: /.*/
- unit_test:
requires:
- setup
filters:
branches:
only: /.*/
- e2e_test:
requires:
- setup
filters:
branches:
only: /.*/
上記例では、最初に setup
というジョブが実行され、その完了を待って lint
unit_test
e2e_test
という後続ジョブが並列実行される。
各ジョブでそれぞれ checkout
して、依存関係を下記のように扱い:
restore_cache
でキャッシュ復元yarn
で依存パッケージのインストールsave_cache
でキャッシュ保存(setup
ジョブでのみ)
その後、それぞれのジョブ名の示す内容を実行する、という流れになっている。
*lock
が完全一致していたときの無駄な step 実行
このとき一つ問題があって、 *lock
ファイルのチェックサムが完全一致してキャッシュが hit した場合、yarn
によって作られるパッケージキャッシュは restore_cache
で復元したものと一致して更新されず、その後の save_cache
も全く同じファイルが保存される(*lock
ファイルの存在意義からしてそのはずである)。
yarn
(パッケージのインストール) 、save_cache
はどちらも、たとえ差分がなかったとしてもファイル数が多いとそれなりに時間がかかるので、不要なら省略したい。
ならば *lock
ファイルが完全に一致する場合のみ yarn
と save_cache
の工程をスキップしちゃえばいいというのが本題。
commands:
install_deps:
steps:
+ - restore_cache:
+ key: &find-cache-key-yarn yarn.lock.cachekey-{{ .Environment.YARN_CACHE_KEY }}-{{ arch }}-{{ checksum "/tmp/app/yarn.lock" }}
+ - run:
+ name: if cache exists exit
+ command: |
+ FILE=/tmp/cacheflags/yarn.lock
+ if test -f "$FILE"; then
+ circleci-agent step halt
+ fi
- restore_cache:
keys:
- yarn-{{ .Environment.YARN_CACHE_KEY }}-{{ arch }}
- run: yarn --frozen-lockfile --silent --cache-folder ~.cache/yarn
- save_cache:
key: &cache-key-yarn yarn-{{ .Environment.YARN_CACHE_KEY }}-{{ arch }}-{{ checksum "/tmp/works/yarn.lock" }}
paths:
- ~/.cache/yarn
+ - run:
+ name: create match flag file
+ command: mkdir -p /tmp/cacheflags/ && touch /tmp/cacheflags/yarn.lock
+ - save_cache:
+ key: *find-cache-key-yarn
+ paths:
+ - /tmp/cacheflags/yarn.lock
上記はそれを実現するための install_deps
の差分になる。
- キャッシュ復元
- 依存インストール
- キャッシュ保存
というシンプルな steps
を少し工夫して、
- (追加)キャッシュが存在しているフラグの復元(無のファイルなのでめちゃくちゃ早い)
- (追加)フラグがあったら
steps
を抜けてジョブを正常終了 - 部分キャッシュリストア
- 依存インストール
- キャッシュ保存
- (追加)キャッシュフラグファイルを作成
- (追加)キャッシュフラグファイルをキャッシュとして保存
このように「*lock
ファイルのチェックサムに該当するキャッシュがあるかどうかのファイルをキャッシュ」 して、「もしファイルがあった場合には以後の処理をスキップしてジョブを正常終了」が達成でき、無駄な依存インストールと save_cache
を省略してジョブの実行時間を削ることができる。
まとめ
-
キャッシュ復元/保存は(ファイル数次第で)それなりに時間かかるから不要ならスキップしましょう
- プロジェクト規模にもよると思うんだけど、上記例で
checkout
をそれぞれのジョブでやってるのもその文脈 - もちろんキャッシュ使ったほうがはやいパターンも有る あくまでケースバイケース
- プロジェクト規模にもよると思うんだけど、上記例で
-
step
をスキップするにはジョブを正常終了して抜けられるcircleci-agent step halt
便利- throw / raise じゃなくて early return ってやつ
って言う話でした。他にもいいやり方あるよ〜というのがあれば、ぜひ教えて下さい。
参考
-
discuss.circleci.com の Topic Skip job if cache exists
- ここで上がってるコード例を元にしてます
- Ending a job from within a
step
- 依存関係のキャッシュ
-
状況によってはキャッシュの保存/復元にかかる時間より package repository から普通にダウンロードするほうが早いというケースがあり、転送量的に無駄が多くて申し訳ない気持ちにもなるけど、各並列ジョブで
↩yarn
叩くことで高速化したプロジェクトがありました・・・。