とことんDevOps | 日本仮想化技術が提供するDevOps技術情報メディア

DevOpsに関連する技術情報を幅広く提供していきます。

日本仮想化技術がお届けするとことんDevOpsでは、DevOpsに関する技術情報や、日々のDevOps業務の中での検証結果など、DevOpsのお役立ち情報をお届けします。
主なテーマ: DevOps、CI/CD、アジャイル開発、コンテナ開発など

開催予定の勉強会

各種SNSのフォローもよろしくお願いいたします。

CircleCIのキャッシュを活用してビルド時間を短縮する

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のキャッシュについて、さらに詳しくは依存関係のキャッシュも合わせて参照してください。