wercker 上で Java / Scala プロジェクトの依存ライブラリをキャッシュする

wercker は今ある CI サービスの中ではもっともイケてるものだと思います。 box と step という仕組みにより、ビルドをおこなう環境とビルド処理を分離し、再利用性を高めています。最近 Docker にも対応したようです。まだ Beta 期間中のため、プライベートリポジトリも無料で利用できます。

最近、 Play Framework を使ったプロジェクトの開発でこの wercker を使用してみたのですが、試しにビルドしてみたら何と2回め以降のビルドでも毎回依存ライブラリをすべてダウンロードしているではないですか!!!

これは困った…どうにかならないもんか…と思ってやってみたのがこの記事です。

僕が今回取り組んだのが Play Framework を使ったプロジェクトだったため、それを題材に書きますが、本質的には Ant だろうと Maven だろうと Gradle だろうと sbt だろうと、もっと言えば Java / Scala に限らずどの言語のプロジェクトでも同じようにできるはずです。ぶっちゃけた話、単に rsync を使って自力でキャッシュを保存/復元しているだけですので。

キャッシュのためのディレクティブがない wercker

メジャーな CI サービスにはキャッシュを制御するディレクティブが用意されているものもあります。
たとえば Travis CI では

.travis.yml
1
cache:
  directories:
    - vendor/bundle

と書いておけば vendor/bundle がキャッシュされます。
CircleCI は至れり尽くせりで、 Java / Scala のプロジェクトの場合には

  • ~/.m2
  • ~/.ivy2
  • ~/.gradle
  • ~/.play

を自動で(暗黙的に)キャッシュしてくれますし、明示的にキャッシュしたいものは

circle.yml
1
dependencies:
  cache_directories:
    - ~/.sbt

のように書いておけば ~/.sbt がキャッシュされます。

一方 wercker はまだ若いサービスのためか、上記のようなキャッシュ機構がきちんと整っておらず、今のところ用意されているのは環境変数 $WERCKER_CACHE_DIR のみのようです (少なくとも僕が探した限りは見つかりませんでした)。

ビルドのたびに依存ライブラリを1からダウンロードされてはたまったもんじゃないので、何とかキャッシュの仕組みを作れないかとやってみました。

参考サイト

以下の wercker の公式ブログの記事を参考にさせていただきました。

上記記事では Go のプロジェクトにおいて、 go get してきた依存ライブラリを rsync を使って $WERCKER_CACHE_DIR に保存したりそこから復元したりして、 ビルド時間を短縮しています。
Java や Scala のプロジェクトでも考え方は同じなので、上記記事を参考にして自分なりのやり方を組み立てました。

要は何をすればよいか

  1. ビルドステップの最初に $WERCKER_CACHE_DIR からキャッシュを復元する
  2. ビルドステップの最後に $WERCKER_CACHE_DIR にキャッシュを保存する

要はこれだけなのですが、

  • キャッシュの保存に rsync を使うために、 rsync をインストールした box を作った
  • キャッシュの保存/復元作業をまとめたシェルスクリプトを書いた

ということをしたので、ちょっと手間暇をかけました。

rsync をインストールした box をつくる

キャッシュの保存/復元は cp コマンドとかでもいいのでしょうが、先の記事でも rsync を使っていましたし、確かに効率を考えると rsync がよさそうです。しかしビルドのたびに rsync をインストールするのも無駄です。そこで、 wercker にはせっかく box という素晴らしい仕組みがあるので、あらかじめ rsync をインストールした box をつくり、それを利用することにしました。

ありがたいことに、 Java 8 と Typesafe Activator をインストールした mitsuse/wercker-box-activator という box がすでにありましたので、 fork させていただき、 rsync をインストールする処理を追記しました。

キャッシュ処理用のシェルスクリプトを書く

簡単な処理であれば wercker.yml に直接書いてしまえばよいのですが、キャッシュ処理が長くなりそうだったので、シェルスクリプトにまとめることにしました。
以下は Play Framework の例です。 Play Framework ではキャッシュディレクトリは ~/.ivy2~/.sbt なので、その2つを指定しています。

wercker_cache.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/bin/bash

#=========================================
# Script for storing and restoring cache
# in Java / Scala project on werkcer
#=========================================

# cache directories
CACHES=( "ivy2" "sbt" )
CMD="$1"

# check if rsync exists
if ! type rsync > /dev/null; then
echo "[ERROR] rsync needs to be installed."
exit 1
fi

# set source and destination directories
case $CMD in
"store")
SRC=$HOME
DST=$WERCKER_CACHE_DIR
;;
"restore")
SRC=$WERCKER_CACHE_DIR
DST=$HOME
;;
esac

# run rsync
for cache in "${CACHES[@]}"; do
if test -d $SRC/.$cache; then
rsync -avz $SRC/.$cache $DST/
fi
done

Gist を貼り付けるとレイアウトが崩れてしまうので、 Gist 版はこちら

単に rsync で $WERCKER_CACHE_DIR へ保存したり、そこから復元したりしているだけです。
このシェルスクリプトをプロジェクトのリポジトリに入れておきます (たとえば sh/wercker_cache.sh など)。

wercker.yml にキャッシュの保存/復元処理を書く

最後に、 wercker.yml にキャッシュの保存/復元処理を書きます。
以下はサンプルで、 sh/wercker_cache.sh に先のスクリプトが入っているとします。

wercker.yml
1
box: skatsuta/activator@0.0.3

build:
  steps:
    - script:
        name: restore cache
        code: |
          sh/wercker_cache.sh restore

    - script:
        name: run activator test
        code: |
          activator test

    - script:
        name: store cache
        code: |
          sh/wercker_cache.sh store

これで wercker 上でビルドしてみると、2回目以降は restore cache というステップで各キャッシュがホームディレクトリに復元され、 store cache というステップで前回のキャッシュディレクトリとの差分が保存されていることが確認できると思います。

ビルド時間の短縮に成功!

僕のプロジェクトではこのおかげで wercker 上でのビルド時間を2分弱短縮することができました。めでたしめでたし。