CIを日常的に行うようになると、気になってくるのがジョブの実行時間です。無視できないほど長いビルドの待ち時間は、開発における大きなボトルネックです。それにCircleCIでは利用した時間に応じて課金が行われますので、なるべく無駄な処理は慎み、短い時間でジョブを完了したいところです。
CI/CDにかかる時間を短縮するテクニックは色々あるのですが、最も簡単に導入でき、かつ効果的なのがキャッシュの活用です。
キャッシュとは
CircleCIにおけるキャッシュとは、次回以降のジョブ実行時に、ワークフローを跨いでデータを再利用するための仕組みです。最も一般的なユースケースは、依存ライブラリをキャッシュすることでしょう。
一般的にアプリケーションは、様々なライブラリに依存しています。これらはRubyであればbundle install
、PHPであればcomposer install
などを利用して、テスト前に必要なパッケージ一式をインストールしますが、このインストールにかかる時間は意外とバカになりません。しかもインストールされるライブラリは、バージョンが変わらなければ基本的に同一であるため、ジョブが起動される度にインターネットから取得し直すのは無駄と言えます。かといってライブラリそのものをアプリのリポジトリ内に抱え込むのは悪手です。キャシュを活用すれば、こうした問題を上手く解決することができます。
注意する点としては、キャッシュは同一ワークフローの後続ジョブとデータを共有する機能とは別である点です。この機能はワークスペースと呼ばれています(本エントリーでは解説しません)。
キャッシュの保存とリストア
それではvendor/bundle
以下にインストールしたRubyGemsのライブラリを例に、実際にキャッシュを保存する設定を見てみましょう。.circleci/config.yml
のジョブ定義内で、bundle install
が完了した後にsave_cache
ステップを追加します。
- run: name: bundle install command: | bundle config set path 'vendor/bundle' && \ bundle install # bundle install後にキャッシュを保存 - save_cache: name: save Gems paths: - ./vendor/bundle key: v1-dependencies-{{ checksum "Gemfile.lock" }}
キャッシュの保存時には、対象とするディレクトリのパスと、キャッシュを一意に特定するためのキーを指定します。パスやジョブのworking_directoryからの相対パス、もしくは絶対パスで指定してください。ある状態のキャッシュを一意に特定するため、ライブラリをキャッシュする場合はlockファイルのチェックサムをキーとして利用するのが定石です。
このキャッシュをリストアする設定は以下のようになります。bundle install
より前に、restore_cache
ステップを追加してください。
# bundle install前にキャッシュをリストア - restore_cache: name: restore Gems keys: - v1-dependencies-{{ checksum "Gemfile.lock" }} - v1-dependencies- - run: name: bundle install command: | bundle config set path 'vendor/bundle' && \ bundle install (...略...)
前回実行時よりGemfile.lock
ファイルが変化していなければ、チェックサムは当然変化しません。そのためsave_cache
で保存したキャッシュがヒットし、vendor/bundle
以下にリストアされるというわけです。結果として直後に実行されるbundle install
内の処理はスキップされるため、大幅に実行時間を短縮することが可能です。
なお見ての通り、リストア時のキーは複数指定することが可能です。キーは指定した順に前方一致検索されるため、ここではこの仕様を利用して、キャッシュが完全にヒットしなかった場合の部分キャッシュリストアを行っています(後述)。
部分キャッシュを活用する
一部のライブラリがアップデートされ、lockファイルのチェックサムが変化してしまったとしましょう。すると当然ですが、キャッシュのキーがヒットしなくなってしまいます。とはいえ、vendor/bundle
以下の一部が変更されただけで、既存のキャッシュを丸ごと放棄し、またゼロからやり直すのは効率がよくありません。こうした場合に有効なのがキャッシュの部分リストアです。
さて、もう一度前述のリストア部分を見てみましょう。
- restore_cache: name: restore Gems keys: - v1-dependencies-{{ checksum "Gemfile.lock" }} - v1-dependencies-
仮に前回の実行でv1-dependencies-XXXX
というキーでキャッシュが作成されたものの、Gemfile.lock
が更新され、v1-dependencies-YYYY
になってしまったとしましょう。すると当然1つ目のキーにはヒットしません。しかしキーが複数列挙されているため、続いて2つ目のv1-dependencies-
が評価されます。前述の通りキャッシュのキーは前方一致で検索されるため、この行によってv1-dependencies-*
というキーを持つキャッシュのうち、最新のものがヒットするのです。結果として(完全一致していないものの)前回作成されたキャッシュがリストアされ、続くbundle install
ではバージョンが変わった部分のみインストール処理が行われます。
このように、キャッシュのキーを{固定値}-{可変値}
にしておくことで、キャッシュが完全にヒットしなかった場合も、最新のキャッシュにフォールバックすることができます。部分キャッシュリストアを活用すれば、既存キャッシュの一部分のみを再利用することで、変更があった際にもその影響を最小限に抑えることが可能です。
CircleCIのキャッシュについて、さらに詳しくは依存関係のキャッシュも合わせて参照してください。