この記事は CircleCI Advent Calendar 2018 の12月18日の記事です。
やりたいこと
- Gradleマルチプロジェクトのテストを複数コンテナで並列実行する
前提
- CircleCI2.0
- 下記のようなGradleマルチプロジェクト(ここではSpring Boot x Groovy)
.
├── build.gradle
├── gradle
├── gradlew
├── gradlew.bat
├── project-one
├── project-two
├── project-three
├── project-four
├── project-five
├── project-six
└── settings.gradle
- docker-composeを動かしてその上でテスト実行
- コンテナを3つ利用
docker-composeを動かす
docker executer
だとvolumeマウントできないのでmachine executer
にする必要があります
refs.
CircleCI 2.0 で docker-compose を動かすなら、Machine Executor にしないとハマる
Docker コンテナにデータボリュームをマウントする方法
テスト分割と割り当て
並列実行するためにテストクラスをコンテナの数だけ分割してあげる必要があります
最初にイメージしていたのは
- 1.ルートプロジェクト配下の全テストクラスをスクリプトで全取得
- 2.1を
circleci tests split
に渡して自動分割 - 3.2をGradleのtestタスクの引数に渡して一括実行
のような流れですが、Gradleには絶対パスで指定したテストクラスをプロジェクト横断で一括実行する
という機能がないため詰まりました(あったら教えてください..)
下記のようにプロジェクト指定でのテストクラス指定実行は可能です
./gradlew :project-one:test --tests "com.example.service.SampleServiceTest"
結果どうしたかというと、苦肉の策として
CIRCLE_NODE_INDEX
という環境変数に実行コンテナのindexが格納されているので、どのコンテナにどのプロジェクトのテストクラスを割り当てるか手動で指定することにしました
下記のコードで
- コンテナ1
- project-one
- project-two
- コンテナ2
- project-three
- project-four
- コンテナ3
- project-five
- project-six
という割り当てでテスト実行が可能になります
#!/bin/bash
set -e
EXECUTE_MODULE=()
if [[ ${CIRCLE_NODE_INDEX} -eq 0 ]]; then
EXECUTE_MODULE+=(one two)
elif [[ ${CIRCLE_NODE_INDEX} -eq 1 ]]; then
EXECUTE_MODULE+=(three four)
elif [[ ${CIRCLE_NODE_INDEX} -eq 2 ]]; then
EXECUTE_MODULE+=(five six)
fi
#コマンドの生成
COMMAND="./gradlew --parallel --max-workers=3 "
for item in ${EXECUTE_MODULE[@]}; do
COMMAND=${COMMAND}":project-$item:clean :project-$item:test "
done
#実行
eval ${COMMAND}
refs.
Using Environment Variables to Split Tests
Running Tests in Parallel
キャッシュの設定
CircleCIのビルドは揮発性なのでキャッシュの設定をなにも行わないと都度ライブラリやパッケージのインストール等が行われてしまいます
今回はgradleのキャッシュ設定を行いました
プロジェクト配下のbuild.gradle
/settings.gradle
/gradle-wrapper.properties
に変更があったときは新しくキャッシュkeyを生成し直します
#!/bin/bash
#Gradleのキャッシュを使うかどうか判定するためのキー生成
#refs.https://medium.com/@chrisbanes/circleci-cache-key-over-many-files-c9e07f4d471a
RESULT_FILE=$1
if [ -f ${RESULT_FILE} ]; then
rm ${RESULT_FILE}
fi
touch ${RESULT_FILE}
checksum_file() {
echo `openssl md5 $1 | awk '{print $2}'`
}
# Gradleのビルドファイルを取得する
FILES=()
while read -r -d ''; do
FILES+=("$REPLY")
done < <(find . \( -name 'build.gradle' -o -name 'settings.gradle' -o -name 'gradle-wrapper.properties' \) -type f -print0)
for FILE in ${FILES[@]}; do
echo `checksum_file ${FILE}` >> ${RESULT_FILE}
done
# ソートする
sort ${RESULT_FILE} -o ${RESULT_FILE}
refs. Caching Dependencies
ちなみにこのキャッシュKeyもコンテナごとに一意のものにしてあげないと、利用する依存ライブラリに差異が出てしまって毎回差分をfetchする形になるので指定してあげます
- restore_cache:
keys:
- v1-gradle-dependencies-{{ .Environment.CIRCLE_NODE_INDEX }}-{{ checksum "/tmp/checksum.txt" }}
- save_cache:
#キャッシュは一度設定すると消せないのでgradleファイルに変更はないが消したい場合にはバージョンをインクリメントする
#res:https://circleci.com/docs/2.0/caching/#clearing-cache
key: v1-gradle-dependencies-{{ .Environment.CIRCLE_NODE_INDEX }}-{{ checksum "/tmp/checksum.txt" }}
ターミナルの指定
なんでもいいのでターミナルを指定しないとgradlewrapper実行時に * What went wrong: Could not open terminal for stdout: $TERM not set
というエラーになるので指定します
export TERM=dumb
テスト結果/成果物の保存
テスト結果や実行時間を綺麗なUIでみれることってめっちゃ大事ですよね
store_test_results
を使うことで簡単に実現できます
- store_test_results:
path: ~/junit
- store_artifacts:
path: ~/junit
全体像
version: 2
jobs:
build:
working_directory: ~/project
machine:
image: circleci/classic:201808-01
environment:
COMPOSE_FILE: ./docker-compose.yml
SPLIT_TEST_PATH: ./circleci
parallelism: 3
steps:
- checkout
- run:
name: Generate Cache Key
command: bash ${SPLIT_TEST_PATH}/create_cache_key.sh /tmp/checksum.txt
- run:
name: Lancuh docker containers
command: docker-compose up
- restore_cache:
keys:
- v1-gradle-dependencies-{{ .Environment.CIRCLE_NODE_INDEX }}-{{ checksum "/tmp/checksum.txt" }}
- run:
name: Run Unit Test
#ターミナルを指定しないと下記のようなエラーがでる
#refs.https://stackoverflow.com/questions/49163104/how-to-resolve-term-not-set-on-gradlew-assemblerelease-on-circleci
command: |
export TERM=dumb
bash ${SPLIT_TEST_PATH}/split_test.sh
- save_cache:
#キャッシュは一度設定すると消せないのでgradleファイルに変更はないが消したい場合にはバージョンをインクリメントする
#res:https://circleci.com/docs/2.0/caching/#clearing-cache
key: v1-gradle-dependencies-{{ .Environment.CIRCLE_NODE_INDEX }}-{{ checksum "/tmp/checksum.txt" }}
paths:
~/.gradle
when: always
- run:
name: Save Test Results
command: |
mkdir -p ~/junit/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/junit/ \;
when: always
- store_test_results:
path: ~/junit
- store_artifacts:
path: ~/junit
今後やってみたいこと
- 2.1への書き換え
-
Docker Layer Cachingの利用
- 毎回dockerイメージを取り直してるしてるのでdockerのコンテナを立ち上げるだけで毎回ビルドが1~2mかかっています
- これを利用するとdocker imageをキャッシュしてくれるのでビルドの高速につながるようです。ただ有料なのでおかねに余裕ができたらつかってみたいです
最後に
CircleCIは公式のドキュメントが非常に充実していてわかりやすく、初心者の自分でもあまり困ることはありませんでした
今後も機会があったら活用したいと思います
以上、最後までみていただきありがとうございました