CircleCIのキャッシュ(cache_directories)の挙動を解説するよ

この記事は CircleCI Advent Calendar 2015 - Qiita の3日目の記事です。

2日目はtakusさんによる「AWS CodeDeploy と CircleCI で Docker コンテナを自動デプロイ」でした。

3日目にして早くも2つ目です。後6枠ほど空いているようなので我こそはという方は是非お願いします。

さて、今回はCircleCIのキャッシュについての記事です。

そもそもCircleCIのキャッシュとは

CircleCIはビルドする度にコンテナ(Dockerコンテナではなく、CircleCI独自のコンテナ)が起動し、その中で一連のビルドやらテスト等のCIが行われ、終了するとそのコンテナは破棄されます。

つまりCircleCIのビルドは揮発性を持つということなので、コンテナの中で毎度ライブラリのダウンロードやその他ツールのインストールを行う必要があるということです。

とはいえ、ライブラリのダウンロードのようなコストのかかる処理を毎回行っていては効率的にCIを回すことができません。そこでCircleCIでは、cache_directoriesという仕組みが用意されています。cache_directoriesの使い方はコチラに書かれていますが、習うより慣れろなので実際にどのように設定して、どのような挙動をするかを確認してみましょう。

検証プロジェクト

キャッシュの挙動を検証するためのプロジェクトを作りました。

ちなみにビルドの内容はこちらから見れます。 Continuous Integration and Deployment

中身はcircle.ymlとREADMEとゴミファイルです。とりあえずキモとなるcircle.ymlはこんな感じ。

machine:
  timezone: Asia/Tokyo

dependencies:
  cache_directories:
    - ~/advent

  override:
    - mkdir -p ~/advent
    - |
      if [ -e ~/advent/build.log ]; then
        cat ~/advent/build.log
      fi
    - echo "`date +'%Y-%m-%d %H:%M:%S'` branch=$CIRCLE_BRANCH build_number=$CIRCLE_BUILD_NUM" >> ~/advent/build.log
    - |
      if [ -e ~/advent/build.log ]; then
        cat ~/advent/build.log
      fi
test:
  override:
    - echo testtest!

基本的にdependenciesフェーズだけ見てもらえればよいです。簡単に解説すると・・・

  • ~/advent を **cache_directories** として設定する
  • ~/advent ディレクトリがなければ作成する
  • ~/advent/build.log が存在すればその内容を表示する
  • 時間とブランチ名、ビルド番号を ~/advent/build.log に**追記**する
  • 確認用にもう一度 ~/advent/build.log を表示する

という流れです。このdependenciesフェーズ後に、 ~/advent ディレクトリはキャッシュとして保存され、次回以降のビルド時にリストアされます。実際の動きは以下のような感じになります。

リストアされたbuild.logにそのビルドのログが追記されたことがわかります。では具体的な動きの検証を次の項から。

1. masterから派生ブランチfeature/advent1を作る

まずはmasterからfeature/advent1という派生ブランチを作ります。ブランチができたらそのままGitHubにpushします。

リストアされたログの末尾はmasterの最終ビルドを示しており、このビルドではfeature/advent1のログが追記されています。つまり、派生ブランチビルドのキャッシュはmasterの最終ビルドでキャッシュされたものが利用されていることがわかりますね。

2. 派生ブランチfeature/advent1をリビルドする

次に、feature/advent1をリビルドしてみるとどうでしょうか。

リストアされたログの末尾が feature/advent1 ですね。つまり、前回のfeature/advent1 のキャッシュがリストアされていることがわかります。

3. feature/advent1から派生ブランチfeature/advent2を作る

今度はfeature/advent1から派生ブランチfeature/advent2を作ります。これもそのままGitHubにpushします。

リストアされたログの末尾がmasterですね(; ´∀`)。feature/advent1の最終ビルドのキャッシュをリストアして欲しいなーという淡い期待がありましたが、どうやら新規のブランチのビルドはmasterの最終ビルドキャッシュを利用するということなのでしょう。

4. feature/advent1をmasterにマージする

新規ブランチの動きがわかったので、今度は作成したブランチをmasterにマージしてみましょう。PRしてマージ(このとき、リモートブランチは削除してます)

PRがマージされると、masterのビルドが走ります。

リストアログを見ると、masterのみが存在します。masterの最終ビルドキャッシュが利用されており、feature/advent1でのビルドキャッシュを持ち込んだりといったことはないようです。まあ、自然ですね。

5. 新たにmasterからfeature/advent1ブランチを作る

個人的には一番気になっていたところ。先ほどのfeature/advent1masterにマージされ、masterに適当にコミットを追加した状態から新たにfeature/advent1を作ってpushします。

リストアログを見ると、以前作ったfeature/advent1ビルドのログがリストアされているようです。新たにmasterから派生したブランチではありますが、同名のブランチのビルドが過去に存在した場合は前回ビルドのキャッシュを利用するということですね。

CircleCIでは、リモートブランチを消して新たに同名のブランチを作っても、履歴はずっと残り続ける仕様です(しれっと消す機能追加されてたりしないですよね?)。以下を見ると#11は再作成した方の最初のビルドですが、以前のビルドが見事に残っています。CircleCIのこの仕様からすると自然な動きかもしれません。

とはいえ、それなりの規模の開発をやってると、実は同名ブランチが存在したってことが結構出てきます。このケースでの弊害は、新しく作ったブランチなのにnpmやmaven、Gradle等の古いキャッシュに頼ったビルドになってしまうことです。

本来失敗するはずのビルドが古いキャッシュによって成功してしまったり、またその逆もあったりと何かとトラブルを引き起こします(ウチのプロジェクトでもたまに発生して、消耗するポイントになっています)。

まとめ

というわけでcache_directoriesの挙動を色々と試してみました。CircleCIを長く触ってきて、たぶんそうなんだろうなーと漠然と思っていたキャッシュの挙動について今回ようやくちゃんと検証できましたw

cache_directoriesを利用する場合(定義しなくてもnpmとかはデフォルトで効いてますからね!)は、以下の挙動を抑えておくと消耗せずにCircleCIを利用できると思われます。

  • 派生ブランチビルドのキャッシュはmasterの最終ビルドでキャッシュされたものが利用される
  • 派生ブランチからさらに派生したブランチのビルドでも、masterの最終ビルドでキャッシュされたものが利用される
  • masterビルドで利用されるキャッシュは基本的にmasterの最終ビルドでキャッシュされたもの
  • 過去に同名のブランチのビルドが存在する場合、それが再利用されてしまうので適度に**without cache**ビルドでキャッシュクリアビルドをする

てか感じです。明日4日目はminodiskさんです。