CircleCIのDocker上でJavaを使ってビルドしようとして諦めた話

前回のエントリではCircleCI上でDockerを使ってビルドする方法については敢えて触れませんでした。

今回は、CircleCI上で任意のバージョンのJVMを使ってビルドする為にDockerコンテナを使ってみます。

試行錯誤した結果のcircle.ymlはこちらになります。参考にしたい方はどうぞ。

machine:
  timezone: Asia/Tokyo
  environment:
    GRADLE_OPTS: -Xmx4G -Dorg.gradle.daemon=true -XX:+HeapDumpOnOutOfMemoryError
  services:
    - docker
  post:
    - sudo service mysql stop
    - sudo service postgresql stop

dependencies:
  cache_directories:
    - ~/docker
  override:
    - if [[ -e ~/docker/image.tar ]]; then docker load -i ~/docker/image.tar; else docker pull java:openjdk-8; fi
    - if [[ ! -e ~/docker/image.tar ]]; then mkdir -p ~/docker; docker save java:openjdk-8 > ~/docker/image.tar; fi
    - docker run --name gige -v ~/.gradle:/root/.gradle -v `pwd`:/build -dit java:openjdk-8 bash
    - chmod +x ./gradlew
    - sudo lxc-attach --keep-env -n "$(docker inspect --format '{{.Id}}' gige)" -- bash -c "cd /build && ./gradlew -v"
    - sudo lxc-attach --keep-env -n "$(docker inspect --format '{{.Id}}' gige)" -- bash -c "cd /build && ./gradlew testClasses"

test:
  override:
    - sudo lxc-attach --keep-env -n "$(docker inspect --format '{{.Id}}' gige)" -- bash -c "cd /build && ./gradlew --full-stacktrace check"
  post:
    - docker stop gige

何故Dockerを使うのか。

CircleCIのマニュアルによるとDockerは現在ベータサポート中です。つまり、本格的に利用出来るわけでは無いのです。

前回のエントリではapt-getコマンドを使って毎回Java8をインストールしていました。それは、CircleCIが/var/cache/aptをキャッシュとして保持してくれないからです。

しかし、aptのローカルキャッシュは兎に角サイズがデカい。CircleCIのキャッシュレストア処理はキャッシュが大きければ大きいだけ時間がかかる仕様なので、黙ってても凄く大きいディレクトリをキャッシュするのは望ましくないのです。

ファイルサイズをやり方次第である程度コントロール可能なDockerコンテナのイメージならあるいは現実的なのではないかと考えたわけです。

もう一つ、aptのインストールプロセスがcircle.ymlの中で一際異彩を放っており、何か大げさな感じがするのが嫌なのです。Dockerを使えばもっと簡素な表現になるんじゃないかと思っていた時期が僕にもありました。

CircleCIはDockerコンテナ及びそのイメージをキャッシュしてくれない

いや、ちゃんとマニュアル読めって話なんですけども。

Docker images aren't cached automatically. At the moment, we have no good method to cache them although we are trying to find a technical solution.

という訳で、circle.ymlの中にシェルスクリプトでキャッシュ処理を書くことになります。

dependencies:
  cache_directories:
    - ~/docker
  override:
    - if [[ -e ~/docker/image.tar ]]; then docker load -i ~/docker/image.tar; else docker pull java:openjdk-8; fi
    - if [[ ! -e ~/docker/image.tar ]]; then mkdir -p ~/docker; docker save java:openjdk-8 > ~/docker/image.tar; fi

cache_directoriesでDockerのコンテナイメージを保存する適当なディレクトリを指定します。

最初のif文では、イメージが存在している時にはdocker loadでコンテナイメージを読み出しつつ、イメージが存在していない時には、docker pullでコンテナイメージをDockerHubから取ってきています。

標準でサポートされているJavaのコンテナイメージはこれです。

このリポジトリには何か色々な理由でOracleのJavaは入っていません。

二番目のif文では、イメージが存在していない時だけdocker saveでコンテナイメージを所定のディレクトリ内に格納しています。

依存ライブラリのキャッシュはMount volumeオプションで指定する

ビルドする際にGradleがダウンロードしてくるライブラリやGradleのランタイムはCircleCIが自動的にキャッシュしてくれるディレクトリに配置しましょう。また、GitHubからチェックアウトしてきたソースコードもDockerコンテナ内から見える位置に配置します。

dependencies:
  override:
    - docker run --name gige -v ~/.gradle:/root/.gradle -v `pwd`:/build -dit java:openjdk-8 bash

ここでは、-v ~/.gradle:/root/.gradleによって、ホスト側の~/.gradleディレクトリをコンテナ内の/root/.gradleにマウントしています。:(コロン)より左側は~(チルダ)を使っても余り問題ないのですが、:(コロン)より右側は絶対パスにしましょう。

次に、-v `pwd`:/buildで、ホスト側のカレントディレクトリ、つまりソースコードリポジトリのルートディレクトリをコンテナ内の/buildにマウントしています。

CircleCIはdocker execをサポートしていない

これもマニュアル通りですね、はい。なので、回避措置としてlxc-attachを使います。

dependencies:
  override:
    - docker run --name gige -v ~/.gradle:/root/.gradle -v `pwd`:/build -dit java:openjdk-8 bash
    - chmod +x ./gradlew
    - sudo lxc-attach --keep-env -n "$(docker inspect --format '{{.Id}}' gige)" -- bash -c "cd /build && ./gradlew -v"
    - sudo lxc-attach --keep-env -n "$(docker inspect --format '{{.Id}}' gige)" -- bash -c "cd /build && ./gradlew testClasses"

lxc-attachを使うには、まずコンテナを名前付きで起動する必要があります。ここでは、docker run --name gigeというふうに--nameを使って名前付けしています。

これによって、docker inspect –format ‘{{.Id}}’ gigeというふうにコンテナの名前からコンテナのIDを得られるからです。

--keep-envオプションは、ホスト側の環境変数をコンテナ内で利用するために指定しています。

少々細かい話ですけども、bashコマンドの-cオプションより後ろの部分は”(ダブルクォート)でくくらないと期待通りの動作になりません。

Gradle Daemonがなぜか動かない

環境変数はちゃんと渡されているのですけども、Gradle Daemonが動きません。この辺まできて、凄く辛みがあるのでこれ以上時間をかけて調査をする気が失せてきました。

ええ、もう毎回apt-getしますよ。うるさいなら、その部分だけシェルスクリプトとして切り出せば良いんだし…うう…