wercker v2 で Rails + RSpec のビルドを高速化する

TL; DR

  • 予め作っておいた Docker イメージを box として指定する
  • rake assets:precompile で生成されたファイルをキャッシュする
  • test-queueRSpec の実行を並列化する
  • Database Cleaner の代わりに DatabaseRewinder を使う

概要

業務では,WAF として Ruby on Rails,テストフレームワークとしては RSpec,CI として wercker を使用しています.今のところ,社内のエンジニアは1人しかおらず,開発規模もそれほど大きくないので,特に CI ツールに課金をすることはしていないのですが,溜まっている pull request を同時に develop branch に merge した場合や,他の人が push した場合に,build queue が溜まってしまうことがしばしばありました.wercker の無料プランでは2つの build を並列で実行することしかできず,特に急いでデプロイをしたい場合などに,作業時間のボトルネックになることも少なからずあり,待ち時間が幾分煩雑に感じられていました.

そこで,開発効率の向上を図るため,ビルド時間の短縮を目指してみることにしました.

以前から wercker を使っていたので(かつ1プロジェクトでしか使っていなかったので)知らなかったのですが,昨年の4月から wercker v2 (codename: Ewok) と銘打ち,Docker-based のビルドを行えるようになっていたようです.そこで今回は,予めテスト環境用の Docker イメージを作成しておき,また,テスト環境の環境構築時に生成されるファイルをキャッシュしておくことにより,高速化を行う方針にしました.

実際の作業

Docker ベースのシステムに移行する

旧来の wercker から Docker-based なシステムに移行するためには,プロジェクトの設定画面から,"Infrastructure stack" を変更する必要があります.

ここを "Classic" から "Docker (default)" に変更してください.

Docker のイメージを作成する

Docker イメージの詳細な作成方法はここでは説明しません. いい感じのコンテナを継承元として指定し,必要なセットアップを行うといいと思います. 業務では,フロントエンドライブラリの管理ツールとして,Bower を使用している(そろそろ捨てたい)のと,ブラウザの自動操作による統合テストを行っているため,Ruby のコンテナをベースに仮想ディスプレイ環境を整えたコンテナを継承元として指定し,その上で Node.js と Bower をインストールしたコンテナを Docker Hub においています. このコンテナを wercker.yml 内で box の値に指定してやれば,その上でビルドが走るようになります. 詳しくは 公式のチュートリアルを参照してください.

以前は wercker が提供する Ruby イメージの上に毎回 Bower をインストールしていたのですが,予め作成しておいたイメージを使用することにより,環境構築に要する時間の短縮に成功しました.

アセットファイルのキャッシュによる高速化

統合テストのための環境整備と,デプロイ時にコマンドの実行が通ることを保証するために,wercker 上で rake assets:precompile を実行していますが,CI 上では毎回新しいコンテナが立てられてしまうので,差分のみではなく,毎回スクラッチでプリコンパイルが走ってしまいます. そこで,wercker が提供するキャッシュディレクトリである $WERCKER_CACHE_DIR に,生成されたファイルをコピーしておくことで,次回以降のプリコンパイルにかかる速度を大幅に減少させることができます.

具体的には,rake assets:precompile の前後に,以下のような step を実行させています.また,bower-rails が Bower で管理されているアセットをインストールするために,rake bower:install を実行しますが,その際にインストールされるファイルも同様にキャッシュしておくことにより,高速化を図っています.

- script:
    name: restore assets cache
    code: |
        [ -e $WERCKER_CACHE_DIR/public/assets ] && cp -fr $WERCKER_CACHE_DIR/public/assets $WERCKER_SOURCE_DIR/public || true
        mkdir -p $WERCKER_SOURCE_DIR/tmp/cache
        [ -e $WERCKER_CACHE_DIR/tmp/cache/assets ] && cp -fr $WERCKER_CACHE_DIR/tmp/cache/assets $WERCKER_SOURCE_DIR/tmp/cache || true
        [ -e $WERCKER_CACHE_DIR/vendor/assets/bower_components ] && cp -fr $WERCKER_CACHE_DIR/vendor/assets/bower_components $WERCKER_SOURCE_DIR/vendor/assets || true
- script:
    name: store assets cache
    code: |
        mkdir -p $WERCKER_CACHE_DIR/public/assets
        cp -fr $WERCKER_SOURCE_DIR/public/assets $WERCKER_CACHE_DIR/public
        mkdir -p $WERCKER_CACHE_DIR/tmp/cache/assets
        cp -fr $WERCKER_SOURCE_DIR/tmp/cache/assets $WERCKER_CACHE_DIR/tmp/cache
        mkdir -p $WERCKER_CACHE_DIR/vendor/assets/bower_components
        cp -fr $WERCKER_SOURCE_DIR/vendor/assets/bower_components $WERCKER_CACHE_DIR/vendor/assets

2016年12月31日追記 : Docker イメージに前もって rsync などを入れておくと便利です.

test-queue によるテストケースの並列実行

RSpec のテストケースの並列実行には test-queue を使用しています.RSpec の他にも Test::Unit や MiniTest にも対応しています. 導入にあたって難しい点は特になく,README に従って executable を生成し,wercker 上でそれを実行する step を記述すれば動くはずなので,詳細は割愛します.

DatabaseRewinder によるデータベースのリセット

各テストケース毎のデータベースのリセットには,これまで database_cleaner を使用していましたが,後発の database_rewinder は,これと凡その互換性を保ちつつ,高速に動作するように開発されている Gem になっています.database_rewinder では,テストケースの実行中に,各テーブルに対して発行された INSERT 文を記憶しておき,その後それに対応する DELETE 文を発行することで,高速化を図っているようです.

こちらも導入は極めて簡単なので,詳細は割愛します.

効果

以前は1回のビルドを実行するのに,平均で11-12分程度かかっていたのですが,これらの改善を経た結果,5−6分にまで短縮することができました.特に "setup environment" でのタイムアウトがなくなったのが嬉しいです.この step は,うまく wercker 側に Docker のイメージがキャッシュされていれば数秒で終わるのですが,なければイメージを pull してくるのに数分かかることもある,といった状況です.

まとめ

CI 上でのビルド環境は,毎回新たに作成されるため,高速化のためには,毎回実行される処理をあらかじめ行った環境を最初から用意しておくということが重要です.そうした環境を用意するため,wercker は Docker イメージ上でのビルドや,キャッシュ用のディレクトリを提供しており,今回はこれらを使用しました. (どうも CircleCI だと circle.yml にキャッシュするディレクトリを書けば済むようですが,wercker は無料でもそこそこ使える反面,あまりその辺りは気が効いていません.) また,テストケースの並列実行と,各テストケース間に行われる処理の高速化により,ビルドに要する時間を大幅に短縮することができました.