概要
もう随分と前に TravisCI から CircleCI へ乗り換えたのですが、いかんせん、便利な CircleCI をもってしても Android のプロジェクトのビルド時間は長くなり続け、ついに 1 回のビルドに 20 分を費やすほどにまで成長してしまいました。いくつか無駄を省いたり、キャッシュをしてみたりと言った策を講じたものの、目立った改善が得られませんでした。そこで CircleCI を脱却してみることにしました。現在、CircleCI を脱却し Wercker を利用することで 1 回のビルドが 5 分ほどで終わるようになりました。この記事には、何がどのようにして短時間で済むようになったかを書き記してあります。
問題の根源
そもそも CircleCI で時間がかかっている部分はどこかというところから見ていきます。現在のプロジェクトで使用している分には、以下に上げる部分でかなりの時間をかけていることがわかっています。
環境のセットアップ
CircleCI には Andorid のサポートがありますが、デフォルトでインストールされているものが最新に追いついていない場合があります。その場合は別途セットアップの段階で SDK のアップデートをする必要がありますが、ご存知の通りこれには数分から場合によっては十数分を要します。
メモリ制限によるパフォーマンス向上の限界
CircleCI では、メモリ使用量の上限が設定されています。そしてその上限は4GBであるため、近頃の Android のビルドシステム(特に dex)のような、メモリをジャブジャブ使う前提のものは必然的に厳しい状況に追い込まれます。
最良の CI サービスを探して
理想的には、サブスクリプション型でお金のパワーでメモリをジャブジャブ使える CI サービスがあれば最高なのですが、なかなかそのような CI サービスはありません。一方で、Android のようにプラットフォームの進化がそこそこ速いと、CI サービス側で予め用意された設定はすぐに腐ってしまいます。CI サービス側の対応を待つと言っても、いつになるか分からないものを待つのはしんどいものです。
Travis も CircleCI も、ある程度自分たちでカスタマイズできる余地はあります。Android の SDK のセットアップは自分で好きなものに変更することも出来ますし、アップデートもできます。ただ、ビルドを走らせるたびに Android SDK をセットアップ・アップデートしなければい点、無駄が多くなります。両者ともキャッシュとしてセットアップ済み Android SDK を永続化することができますが、キャッシュへ保存したりキャッシュから読みだしたりする時間もバカにはなりません(S3 に丸投げしたりと別のところに保存するので、転送に時間がかかります)。
どうせなら、Docker かなにかで環境をまるごとコンテナにぶち込んで、その上で CI してくれれば。。。
救世主 Wercker
まさにドンピシャで、Docker コンテナを DockerHub などから引っ張ってきて動かし、その上で CI が出来るサービスが有りました。そう、Werckerです。そしてさらに今ならなんと、private リポジトリでもタダでビルドさせてくれます。
Docker コンテナをつくろう
やることは簡単、Dockerfile を用意して、必要な物を入れて、ビルドして、DockerHub に push するだけです。
ただしこの「必要な物」というのが、プロジェクトの構成によって少しずつ変わってきます。たとえば、retrolambda を使っているなら、Java8 が必要だったり。あるいは、64bit 版 ubuntu をベースにセットアップするならば、いくつかの 32bit 版のパッケージが必要だったり。
そういうわけで、いまのところ自分たちのところで最低限必要となりそうなものをまとめたコンテナを作ってみました。
ベースは Ubuntu ですので、Ubuntu で Android SDK のセットアップやビルドに必要なパッケージ、NDK、Gradle、retrolambda 用の Java8 と Java7 の設定、その他周辺ツールを導入するためのpipがコンテナに入っています。
DockerHub: https://hub.docker.com/r/keithyokoma/wercker-container-android/
Dockerfile: https://github.com/KeithYokoma/wercker-container-android
DockerHub から引いてくるには、keithyokoma/wercker-container-android
と指定するだけです。
wercker.yml を設定
ビルドを走らせるだけなら以下で十分です。この辺りは好みのものを他の CI 同様に設定しましょう。
どのコンテナを使うかは最初の行に書いておきます。
box
に対応する値は DockerHub のリポジトリ名で指定します。
box: keithyokoma/wercker-container-android
build:
steps:
- script:
name: run gradle assembleDebug
code: |
./gradlew --project-cache-dir=$WERCKER_CACHE_DIR assemble testDebugUnitTest -PdisablePreDex
結果
いかんせんコンテナが2GBもありますので、初回こそビルドに時間を要しますが、2回目以降はさくさくとビルドが進みます。CircleCI で 20 分要したビルドがいまでは 4〜6分程度で終わります。速いですね!!
Docker コンテナはローカルキャッシュに積まれることから、環境のセットアップはわずか3秒です。
もちろん、環境のアップデートをするたびにコンテナの再ビルドとセットアップが必要になりますが、その時間を考慮に入れても毎回5分で済むようになったことは大きな利益です。
この他、Wercker ではデプロイとビルドは別のプロセスとなりますが、ビルドの成果物は別途保存されるため、デプロイ時にはデプロイに必要なスクリプトのみを用意すれば良いことになります。このため、より一層ビルドの待ち行列を効率的にさばけるようになっています。ただし、ビルド時に使った環境変数はデプロイのプロセスでは再設定する必要があることに注意が必要です。
余談1
よく知られている話ではありますが、boot2docker は VirtualBox の中で動くので、ディスクアクセス(特に書き込み)がかなり低速です。上記の Dockerfile は内容自体は大したことはありませんが、その実大量にファイルをダウンロードすることになるため、boot2docker 上で docker build すると湯水のように時間を吸い取られます(Macbook Air Core i7 1.7GHz 8GB メモリのモデルで40分くらい)。
作っては壊してまた作りなおすというイテレーションを回すにはしんどい環境ですので、できればパワーの有り余った Linux マシン上でビルドすることをおすすめします(迫真)。
余談2
ところで、CI サービスを使ってビルドをした時に、ビルドの失敗理由が何であるかということは少ない手順で把握したいものです。チャットサービスと連携しているなら尚更、ビルドの成否だけではなく、失敗時にはその理由も合わせてチャットに流れてこないと、結局 CI サービスのウェブインタフェースを眺めるはめになります。そして往々にして、Android のビルドログは長いため、サービスによってはログが途中で切り落とされて大事なところが見えなくなってしまうため(CircleCI も Wercker もこの仕様はある)、結局手元で失敗を試すはめになります。
もちろん、コードをプッシュする前に失敗しないかどうかを試しておくべきということではありますが、それでも失敗するときは失敗してしまいます。
その時に便利なのがこの Gradle プラグインです。WebHook の URL を与えれば、ビルドの失敗時にその理由を Slack に投げつけられます。これで、CI 失敗時にはチャットを見ればその理由までバッチリ把握できて便利ですね!
FAQ
Docker コンテナを CircleCI で動かすのではだめなの?
CircleCI は最近 Docker ベースのビルドをサポートし始めました。
CircleCI の Docker サポートでは、CircleCI がビルドごとに動かしているコンテナの上に Docker コンテナを乗せてその中でビルドを動かすことになります。これはつまり、単純に Docker ベースの CI をしようとすると、ビルドのたびにコンテナを DockerHub 等から引っ張ってくることになります。キャッシュの仕組みは自分でビルドスクリプトに記述しておく必要があります。その他もまだ自分でビルドスクリプトに必要な処理を書かないと Docker コンテナ上での CI ができない状況です。
詳しい記事はCircleCIのDocker上でJavaを使ってビルドしようとして諦めた話として別のブログ記事にまとめられています。