メインコンテンツまでスキップ

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

Sato Taichi

前回のエントリでは 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 は現在ベータサポート中です。つまり、本格的に利用出来るわけでは無いのです。

CircleCI currently offers beta support for running Docker within build containers.

前回のエントリでは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しますよ。うるさいなら、その部分だけシェルスクリプトとして切り出せば良いんだし…うう…