ようやく、CircleCIにてdocker-composeを使ってテストを流して問題なく終了するようになったので、そのためにやったことを書いておきます。
これまでの経緯は過去の記事を参照のこと。
patorash.hatenablog.com
patorash.hatenablog.com
patorash.hatenablog.com
目次
不具合の修正について
できたと思って記事を書いたのですが、実際に運用してみるとちょこちょこ不具合が出てきたので、この記事は随時更新していきます。すみません…。
👍2020-03-13修正済:testジョブでbundle installしろと言われて落ちる
👍2020-03-22修正済:artifactsの保存ができていない
CircleCIでdocker-composeを動かすためにやったこと
開発で使っているdocker-composeと同じものを丸々使ってテストが完走できることを目標としました。こうすることで、開発環境・テスト環境での差異がなくなるので、より安定した開発ができるはずです。
大きな変更点
実行環境をdockerからLinux VM に変更
executorをdockerからmachineに変更しました。machineは、Linux のVM が立ち上がる設定です。
circleci.com
machineを使うことで、Linux でできることは大概できるようになります。これで、VM 内でdocker-composeが起動できます。
executorがdockerの頃は、Dockerイメージの設定をCircleCIの設定ファイルに持たせなければなりませんでしたが、machineにすると全てdocker-compose.ymlで完結するので、そういう点では楽です。
テストの実行をdocker-compose execにて行う
executorがdockerの頃は、.circleci/config.yml
で指定したruby のコンテナでテストを実行していましたが、machineの場合はdocker-composeのruby コンテナで行うことになるので、コマンドの先頭にdocker-compose exec
が付くようになります。
executorをmachineにしてテストすることのメリット・デメリット
先にメリットとデメリットを書いておきます。
メリット
docker-compose.ymlにコンテナ設定が書ける
先ほども書きましたが、CircleCI用にdocker imageを選定しなくてもよくなるのがメリットです。docker-compose.ymlに定義されているimageが丸々使えるからです。
デメリット
デメリットも結構あります。
起動が遅い
executorがdockerの場合は起動が速くて、直ぐに動き始めます(インスタントオン)。が、machineの場合は、若干もたつき ます(30~60秒)。CircleCIの公式ドキュメントにそのように書いてあります。
実行時間が長くなった
テストを実行するための準備に時間がかかります。どういう点かというと、docker imageのダウンロードと、docker imageのビルドです。
ビルドに関しては、Docker Layer Cachingが使えるとありますが、1回につき200クレジットと書いてあり、結構高いです…。resource sizeがmedium 1分10クレジットなので20分に相当します。そのため、現在はまだ使っていません。
毎回docker imageをダウンロード・ビルドするのはいかがなものか?と思うので、Docker Layer Cachingを利用せずに、docker saveコマンドでイメージを出力し、それをsave_cacheとrestore_cacheを利用して使いまわしています(これはいいのか?)
これにより速度改善にはなるのですが、docker loadにかかる時間が1分半くらいかかってます。job毎にこれが発生するので、今のところ1.5分*2=3分くらいはこれで遅くなってるかなと思いますが、まぁ仕方ありません。
また、Mac でのdockerの開発環境と同じにするため、docker volumesを使っています。volumeにgemやnode_modulesなどのライブラリを入れているため、それらをキャッシュするために一旦docker volumeからエクスポートしてからsave_cacheをしているので、そのあたりも遅くなる原因になります。(これは回避できるので後で解説)
クレジット消費量が増える
実行時間が長くなるということは、クレジット消費量が増えるということです。以前は20分以内に収まっていたテストが30分くらいかかるようになったので、遅くなったうえに1.5倍のクレジットがかかるとなると、結構なデメリットです…。
【2020-03-22 追記】
高速化を頑張った結果、最速で25分くらいで終わるようにはなりました。
環境変数 をコンテナに渡すのが大変
executorがdockerの場合はCircleCIの環境変数 を意識せずに使えるのですが、machineの場合はCircleCIの環境変数 が設定されるのはLinux VM のため、docker-composeで起動するコンテナにdocker-compose exec
のときにそれらの環境変数 を明示的に渡さなければなりません。これがいちいち面倒でした。
CircleCIのワークフロー
config.ymlの説明をする前に、使用しているワークフローを説明します。
以下が、CircleCIのワークフローです。
CircleCIのワークフロー図
job: generate_cache
まず、先にキャッシュを作成します。
やっていることを羅列していくと、以下の通りです。
checkout
docker-composeのインストール
docker imageの更新・キャッシュ
必要であれば、docker-composeでtest用コンテナ起動
gemの更新・キャッシュ
node_modulesの更新・キャッシュ
データベース定義の更新・キャッシュ
docker-compose down
次のジョブにワークスペース を渡す
これを並列化せず、1コンテナで行います。たくさんのコンテナで実行しても無駄ですからね。
job: reviewdog
reviewdogはコードチェッカーで、rubocopのルールに違反しているものがあればPull Requestにコメントをしてくれるやつです。
今回は説明はこれくらいで。公開するconfig.ymlからはこの設定を削除しています(余計なので)。
詳しく知りたい人は過去の記事をどうぞ。
patorash.hatenablog.com
job: test
これがテストの本丸ですが、こっちは簡潔で、ただdocker-compose経由でテストを実行するだけです。
こちらは並列化します。(現在はparallelism: 4に設定)
やっていることを羅列していくと、以下の通りです。
job generate_cache から、ワークスペース を引き継ぐ
generate_cacheから引き継ぐため、checkout不要
docker images, gem, node_modulesのキャッシュも引き継ぐのでrestore_cacheが不要
docker-composeのインストール
docker imageのロード
ダウンロードファイル置き場のディレクト リを作成
docker-composeを起動する前に作っておかないとエラーになる
docker-composeでtest用コンテナ起動
docker volumeにgemのキャッシュをロードする
docker volumeにnode_modulesのキャッシュをロードする
docker volumeにbootsnapのキャッシュをロードする
docker volumeにPG_DATAをロードする
テストを実行
bootsnapのキャッシュを保存する
テスト結果をsimplecovでまとめる
docker-compose down
.circleci/config.ymlを公開
では、実際に使っているconfig.ymlから、一部を抜粋しました。(reviewdogのワークフローは今回の本質ではないので削除)
弊社では、parallel_testsとknapsack_proを使っていますので、そこはそのままです。そこは適宜、読み替えてください。
knapsack_proに関しては、過去の記事を読んでください。
patorash.hatenablog.com
では、config.ymlです。長いので必要なときだけ読んでください。
【2020-03-13 追記:不具合修正】
キャッシュするときのkeyの指定にバグがあったので修正しました。
下記のはgemのだけ書いてますが、yarnのほうも同様に直しています。
before: bundle-v{{ .Environment.GEM_CACHE_KEY }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
after: `bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
不具合の解説
以前の記事では、リストアするときにkeyに以下のように指定をしていました。
restore_bundle_cache :
steps :
- restore_cache :
key : gemfile-lock-sha256sum-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
- restore_cache :
keys :
- bundle-v{{ .Environment.GEM_CACHE_KEY }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
- bundle-v{{ .Environment.GEM_CACHE_KEY }}-{{ .Branch }}
- bundle-v{{ .Environment.GEM_CACHE_KEY }}
これだと、ブランチが切り替わるとGemfile.lockのチェックサム で保存してあるキャッシュは無視され、古いgemがリストアされました。その後、bundle installが行われます。
その次のステップで、Gemfile.lockのハッシュ値 が変わっていたら、docker volumeからバックアップを取ってから、save_cacheが行われるのですが、Gemfile.lockを更新するプルリクでテストが実行された時点で新しいGemfile.lockのハッシュ値 が保存されているため、docker volumeからのバックアップが行われずにsave_cacheしてしまい、generate_cacheが終わった後にtestが実行されると、bundle installしろと怒られてテストが落ちました😥
そのため、save_cacheとrestore_cacheのkeyを修正して、Gemfile.lockのチェックサム を基準に見させるようにしました。
restore_bundle_cache :
steps :
- restore_cache :
key : gemfile-lock-sha256sum-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
- restore_cache :
keys :
- bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
- bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}
- bundle-{{ arch }}
yarnのほうも同様の理由で修正しています。
【2020-03-22 追記:不具合修正と高速化】
artifactsの保存に失敗していたのを修正しました。CIRCLECI_TEST_REPORTSを、/tmp/test-resultsにしていたのですが、docker-composeでテストを行うと、dockerコンテナの/tmp/test-resultsに保存されてしまい、ホストOSで取得できない状態でした。そこで、共有ディレクト リであるRails .root/tmp/test-resultsに保存するように修正しました。
また、この修正をしている最中にも新たに高速化できそうなポイントが書かれていた記事を見つけたため、それを参考にさらに高速化しました。参照ページをリンクしておきます。
engineering.later.com
では、設定ファイルです。
version : 2.1
executors :
default :
machine : true
working_directory : ~/project
environment :
RAILS_ENV : test
RACK_ENV : test
TZ : "/usr/share/zoneinfo/Asia/Tokyo"
CIRCLE_TEST_REPORTS : ./tmp/test-results
PARALLEL_TESTS_CONCURRENCY : 4
REVIEWDOG_VERSION : v0.9.17
commands :
install_docker_compose :
steps :
- run :
name : Install Docker Compose
command : |
curl -L https://github.com/docker/compose/releases/download/1.25.4/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
chmod +x ~/docker-compose
sudo mv ~/docker-compose /usr/local/bin/docker-compose
save_docker_images :
steps :
- run :
name : Check cache file, and create docker images cache
command : |
if [ ! -e ~/caches/images.tar ] ; then
docker-compose pull postgres redis elasticsearch memcached chrome
docker-compose build test
mkdir -p ~/caches
docker save $(docker images | awk 'NR>=2 && ! /^<none>/{print $1}' ) -o ~/caches/images.tar
fi
- save_cache :
key : docker-{{ checksum ".dockerignore" }}-{{ checksum "docker-compose.yml" }}-{{ checksum ".dockerdev/Dockerfile" }}-{{ checksum ".dockerdev/entrypoint.sh" }}
paths : ~/caches/images.tar
load_docker_images_and_update_libraries_if_necessary :
steps :
- run :
name : Load Docker images and update libraries if necessary
command : |
set +e
yarn_cache_shasum=`[ -r yarn.lock.sha256sum ] && cat yarn.lock.sha256sum`
yarn_file_shasum=`shasum -a 256 yarn.lock`
gem_cache_shasum=`[ -r Gemfile.lock.sha256sum ] && cat Gemfile.lock.sha256sum`
gem_file_shasum=`shasum -a 256 Gemfile.lock`
if [ "${yarn_cache_shasum}" != "${yarn_file_shasum}" ] || [ "${gem_cache_shasum}" != "${gem_file_shasum}" ] || [ ! -e ~/caches/pg_data.tar ] ; then
docker load -i ~/caches/images.tar
docker-compose up -d test
if [ -e ~/caches/bundle.tar ] ; then
docker run --rm --volumes-from project_test_1 -v ~/caches:/backup busybox tar xvf /backup/bundle.tar
fi
if [ -e ~/caches/node_modules.tar ] ; then
docker run --rm --volumes-from project_test_1 -v ~/caches:/backup busybox tar xvf /backup/node_modules.tar
fi
if [ "${gem_cache_shasum}" != "${gem_file_shasum}" ] ; then
docker-compose exec test sh -c "bundle check || bundle install --clean"
fi
if [ "${yarn_cache_shasum}" != "${yarn_file_shasum}" ] ; then
docker-compose exec test yarn install
fi
if [ ! -e ~/caches/pg_data.tar ] ; then
docker-compose exec \
-e RAILS_MASTER_KEY=$RAILS_MASTER_KEY \
test \
bin/rails db:migrate:reset
docker-compose exec \
-e RAILS_MASTER_KEY=$RAILS_MASTER_KEY \
-e PARALLEL_TESTS_CONCURRENCY=$PARALLEL_TESTS_CONCURRENCY \
test \
bin/rails parallel:load_structure[$PARALLEL_TESTS_CONCURRENCY]
docker-compose stop postgres
pg_container_name=`docker-compose run --rm -d postgres /bin/bash`
docker run --rm --volumes-from $pg_container_name -v ~/caches:/backup busybox tar cvf /backup/pg_data.tar -C / var/lib/postgresql/data
docker kill $pg_container_name
docker-compose start postgres
fi
fi
load_docker_images :
steps :
- run :
name : Load Docker images
command : docker load -i ~/caches/images.tar
restore_docker_images :
steps :
- restore_cache :
key : docker-{{ checksum ".dockerignore" }}-{{ checksum "docker-compose.yml" }}-{{ checksum ".dockerdev/Dockerfile" }}-{{ checksum ".dockerdev/entrypoint.sh" }}
paths : ~/caches/images.tar
docker_compose_up :
steps :
- run :
name : docker-compose up
command : |
docker-compose up -d test
save_node_modules_cache :
steps :
- run :
name : Create node_modules cache
command : |
set +e
cache_shasum=`[ -r yarn.lock.sha256sum ] && cat yarn.lock.sha256sum`
file_shasum=`shasum -a 256 yarn.lock`
if [ "${cache_shasum}" != "${file_shasum}" ] ; then
shasum -a 256 yarn.lock > yarn.lock.sha256sum
docker run --rm --volumes-from project_test_1 -v ~/caches:/backup busybox tar cvf /backup/node_modules.tar -C / app/node_modules
fi
- save_cache :
name : Saving yarn.lock Hash Value
key : yarn-lock-sha256sum-{{ arch }}-v{{ .Environment.YARN_CACHE_KEY }}-{{ checksum "yarn.lock" }}
paths :
- yarn.lock.sha256sum
- save_cache :
name : Saving node_modules Cache
key : yarn-{{ arch }}-v{{ .Environment.YARN_CACHE_KEY }}-{{ checksum "yarn.lock" }}
paths :
- ~/caches/node_modules.tar
load_node_modules_cache :
steps :
- run :
name : Load node_modules caches
command : |
docker run --rm --volumes-from project_test_1 -v ~/caches:/backup busybox tar xvf /backup/node_modules.tar
restore_node_modules_cache :
steps :
- restore_cache :
key : yarn-lock-sha256sum-{{ arch }}-v{{ .Environment.YARN_CACHE_KEY }}-{{ checksum "yarn.lock" }}
- restore_cache :
keys :
- yarn-{{ arch }}-v{{ .Environment.YARN_CACHE_KEY }}-{{ checksum "yarn.lock" }}
- yarn-{{ arch }}-v{{ .Environment.YARN_CACHE_KEY }}
- yarn-{{ arch }}
save_bundle_cache :
steps :
- run :
name : Create bundle cache
command : |
set +e
cache_shasum=`[ -r Gemfile.lock.sha256sum ] && cat Gemfile.lock.sha256sum`
file_shasum=`shasum -a 256 Gemfile.lock`
if [ "${cache_shasum}" != "${file_shasum}" ] ; then
shasum -a 256 Gemfile.lock > Gemfile.lock.sha256sum
docker run --rm --volumes-from project_test_1 -v ~/caches:/backup busybox tar cvf /backup/bundle.tar -C / bundle
fi
- save_cache :
name : Saving Gemfile.lock Hash Value
key : gemfile-lock-sha256sum-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
paths :
- Gemfile.lock.sha256sum
- save_cache :
name : Saving Gem Cache
key : bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
paths :
- ~/caches/bundle.tar
load_bundle_cache :
steps :
- run :
name : Load bundle caches
command : |
docker run --rm --volumes-from project_test_1 -v ~/caches:/backup busybox tar xvf /backup/bundle.tar
restore_bundle_cache :
steps :
- run :
name : Get the Gemfile.lock from the develop branch
command : |
git show develop:Gemfile.lock > DevelopGemfile.lock
- restore_cache :
name : Restore Gemfile.lock Hash Value
key : gemfile-lock-sha256sum-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
- restore_cache :
name : Restore Gem Cache
keys :
- bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
- bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "DevelopGemfile.lock" }}
- bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}
- bundle-{{ arch }}
docker_compose_down :
steps :
- run :
name : docker-compose down
command : docker-compose down
when : always
sync :
steps :
- run : sync
restore_bootsnap_cache :
steps :
- restore_cache :
name : Restore bootsnap cache
keys :
- bootsnap-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}-{{ .Branch }}
- bootsnap-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}-develop
save_bootsnap_cache :
steps :
- run :
name : Create bootsnap cache
command : |
docker run --rm --volumes-from project_test_1 -v ~/caches:/backup busybox tar cvf /backup/bootsnap.tar -C / app/tmp/cache
when : always
- save_cache :
name : Saving bootsnap cache
key : bootsnap-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}-{{ .Branch }}-{{ epoch }}
paths :
- ~/caches/bootsnap.tar
when : always
load_bootsnap_cache :
steps :
- run :
name : Load bootsnap caches
command : |
if [ -e ~/caches/bootsnap.tar ] ; then
docker run --rm --volumes-from project_test_1 -v ~/caches:/backup busybox tar xvf /backup/bootsnap.tar
fi
restore_pg_data_cache :
steps :
- run : cp config/{database_circleci,database}.yml
- run :
name : Database Checksum
command : |
find db -type f -exec md5sum {} \; | sort -k 2 > db_dir_checksums.txt
md5sum config/database.yml >> db_dir_checksums.txt
echo $PARALLEL_TESTS_CONCURRENCY >> db_dir_checksums.txt
md5sum docker-compose.yml >> db_dir_checksums.txt
- run :
name : cat db_dir_checksums.txt
command : |
cat db_dir_checksums.txt
- restore_cache :
name : Restore PG_DATA
key : pgdata-v2-{{ arch }}-{{ checksum "db_dir_checksums.txt" }}
save_pg_data_cache :
steps :
- save_cache :
name : Saving PG_DATA
key : pgdata-v2-{{ arch }}-{{ checksum "db_dir_checksums.txt" }}
paths :
- ~/caches/pg_data.tar
when : always
load_pg_data_cache :
steps :
- run :
name : Load PGDATA cache
command : |
docker-compose stop postgres
pg_container_name=`docker-compose run --rm -d postgres /bin/bash`
docker run --rm --volumes-from $pg_container_name -v ~/caches:/backup busybox tar xvf /backup/pg_data.tar
docker kill $pg_container_name
docker-compose start postgres
jobs :
generate_cache :
executor : default
steps :
- checkout
- install_docker_compose
- restore_docker_images
- save_docker_images
- restore_bundle_cache
- restore_node_modules_cache
- restore_bootsnap_cache
- restore_pg_data_cache
- load_docker_images_and_update_libraries_if_necessary
- save_bundle_cache
- save_node_modules_cache
- save_pg_data_cache
- docker_compose_down
- persist_to_workspace :
root : ../
paths :
- project
- caches
test :
executor : default
parallelism : 4
steps :
- attach_workspace :
at : ~/
- install_docker_compose
- load_docker_images
- run :
name : Make download directory
command : |
mkdir -p tmp/download
chmod 777 tmp/download
- docker_compose_up
- load_bundle_cache
- load_node_modules_cache
- load_bootsnap_cache
- load_pg_data_cache
- sync
- run :
name : Run rspec in parallel
command : |
docker-compose exec \
-e RAILS_MASTER_KEY=$RAILS_MASTER_KEY \
-e CI=$CI \
-e CIRCLE_TEST_REPORTS=$CIRCLE_TEST_REPORTS \
-e KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC=$KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC \
-e KNAPSACK_PRO_CI_NODE_TOTAL=$CIRCLE_NODE_TOTAL \
-e KNAPSACK_PRO_CI_NODE_INDEX=$CIRCLE_NODE_INDEX \
-e KNAPSACK_PRO_BRANCH=$CIRCLE_BRANCH \
-e KNAPSACK_PRO_COMMIT_HASH=$CIRCLE_SHA1 \
-e KNAPSACK_PRO_LOG_LEVEL=warn \
-e PARALLEL_TESTS_CONCURRENCY=$PARALLEL_TESTS_CONCURRENCY \
test \
bin/parallel_test -n $PARALLEL_TESTS_CONCURRENCY -e './bin/parallel_tests'
- save_bootsnap_cache
- deploy :
command : |
docker-compose exec \
-e RAILS_MASTER_KEY=$RAILS_MASTER_KEY \
-e CI=$CI \
-e CIRCLE_PROJECT_USERNAME=$CIRCLE_PROJECT_USERNAME \
-e CIRCLE_PROJECT_REPONAME=$CIRCLE_PROJECT_REPONAME \
-e CIRCLE_BUILD_NUM=$CIRCLE_BUILD_NUM \
-e CIRCLE_TOKEN=$CIRCLE_TOKEN \
-e CIRCLE_TEST_REPORTS=$CIRCLE_TEST_REPORTS \
-e CIRCLE_WORKING_DIRECTORY=$CIRCLE_WORKING_DIRECTORY \
test \
bundle exec ruby circleci_simplecov_parallel.rb
- docker_compose_down
- store_test_results :
path : ./tmp/test-results
- store_artifacts :
path : ./tmp/test-results
destination : test-results
workflows :
version : 2
build :
jobs :
- generate_cache
- test :
requires :
- generate_cache
工夫した点
ライブラリのキャッシュの必要性をハッシュ値 を保存しておく
executorがdockerの場合は、単純にsave_cache, restore_cacheしておけば、同じcacheのkeyがあるときはスキップすればいいだけです。
しかし、docker-composeでvolumeを使っていてそこにライブラリを入れている場合、ホストOSから直接参照できないので、volumeのバックアップを作成してホストOSから参照できるところに置かなくてはなりません。このプロジェクトの場合は、gem置き場のバックアップに2分かかっていました。
2分かかって出力してから、「あ、このcacheのkeyは既にあるからスキップね」となると、この2分が丸々無駄です!😡とはいえ、更新があった場合はsave_cacheのタイミングで新しいファイルが置いてないと困ります。
そこで、こちらでもGemfile.lockとyarn.lockのsha256のハッシュ値 をファイルに保存するようにして、現在のGemfile.lock, yarn.lockと比較させて、違っていたらdocker volumeをバックアップするようにしました。
config.ymlからそこだけ抜粋します(Gemfile.lockのみ)
commands :
save_bundle_cache :
steps :
- run :
name : Create bundle cache
command : |
set +e
cache_shasum=`[ -r Gemfile.lock.sha256sum ] && cat Gemfile.lock.sha256sum`
file_shasum=`shasum -a 256 Gemfile.lock`
if [ "${cache_shasum}" != "${file_shasum}" ] ; then
shasum -a 256 Gemfile.lock > Gemfile.lock.sha256sum
docker run --rm --volumes-from project_test_1 -v ~/caches:/backup busybox tar cvf /backup/bundle.tar -C / bundle
fi
- save_cache :
name : Saving Gemfile.lock Hash Value
key : gemfile-lock-sha256sum-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
paths :
- Gemfile.lock.sha256sum
- save_cache :
name : Saving Gem Cache
key : bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
paths :
- ~/caches/bundle.tar
これで、2分短縮できました🥳(ライブラリの更新がない場合に限る)
dockerに環境変数 を渡す
ついうっかり忘れがちでしたので、気をつけましょう。CircleCIのコンテナには、環境変数 が設定されていますが、コンテナが起動するdockerのコンテナにはCircleCIが自動で設定してくれる環境変数 は反映されていません!必要な環境変数 は適宜、渡します!
- run :
name : Run rspec in parallel
command : |
mkdir /tmp/test-results
docker-compose exec \
-e RAILS_MASTER_KEY=$RAILS_MASTER_KEY \
-e CI=$CI \
-e CIRCLE_TEST_REPORTS=$CIRCLE_TEST_REPORTS \
-e KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC=$KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC \
-e KNAPSACK_PRO_CI_NODE_TOTAL=$CIRCLE_NODE_TOTAL \
-e KNAPSACK_PRO_CI_NODE_INDEX=$CIRCLE_NODE_INDEX \
-e KNAPSACK_PRO_BRANCH=$CIRCLE_BRANCH \
-e KNAPSACK_PRO_COMMIT_HASH=$CIRCLE_SHA1 \
-e KNAPSACK_PRO_LOG_LEVEL=warn \
-e PARALLEL_TESTS_CONCURRENCY=$PARALLEL_TESTS_CONCURRENCY \
test \
bin/parallel_test -n $PARALLEL_TESTS_CONCURRENCY -e './bin/parallel_tests'
これで、テストもちゃんと通るはず…!!👍
2020-03-22 追記分
追加でやったことを一覧にしておきます。
PARALLEL_TESTS_CONCURRENCYの指定を4に変更した
gemのキャッシュを参照にする際に、基本ブランチを2番目に参照にするよう修正した
基本ブランチはキャッシュがヒットする可能性が高いから
bootsnapのキャッシュを保存するように修正した
PG_DATAのコールドバックアップを再利用するよう修正した
1つずつ説明していきます。
PARALLEL_TESTS_CONCURRENCYを2から4に変更
これは、先ほど紹介した記事に、machine sizeがmediumなら並列化は4が最適と書かれていたから、実際にしてみたところ、2の時よりも短くなったためです(30秒~1分程度)。mediumだとCPU数がそもそも2なので、劇的な変化はありませんが、恐らくI/O待ちが発生している間に実行できるテスト数が増えることで多少速くなるのかな?と思います(あくまで推測ですが…)
ただし、4並列にすると、Chrome がメモリ不足でエラーを起こすようになるので、以下の設定が必須です。
patorash.hatenablog.com
基本ブランチを2番目に参照するよう修正
これも先ほど紹介された記事にありました。確かに現在のGemfile.lockの値を参照するのが一番で、二番目には基本ブランチのGemfile.lockを参照したほうがキャッシュ効率がよさそうでした。
うちではdevelopブランチが基本ブランチなので、そこからgit showコマンドを使ってDevelopGemfile.lockを作り、restore_cacheする際に使うように修正しました。
commands :
restore_bundle_cache :
steps :
- run :
name : Get the Gemfile.lock from the develop branch
command : |
git show develop:Gemfile.lock > DevelopGemfile.lock
- restore_cache :
name : Restore Gemfile.lock Hash Value
key : gemfile-lock-sha256sum-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
- restore_cache :
name : Restore Gem Cache
keys :
- bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "Gemfile.lock" }}
- bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}-{{ checksum "DevelopGemfile.lock" }}
- bundle-{{ arch }}-v{{ .Environment.GEM_CACHE_KEY }}
- bundle-{{ arch }}
bootsnapのキャッシュを保存するように修正
bootsnapはRails の起動するまでの処理をキャッシュすることで高速化するためのgemで、Rails5.2から標準になっています。これがない場合はRails を起動するためのキャッシュ生成で結構時間がとられるので、これを保存して再利用すると、Rails が直ぐ起動できて速くなります。なるはずです。しかし、そこまで効果が高いかと言われるとうちのプロジェクトでは微妙でした…。bootsnapのキャッシュの保存にも時間がかかるので、効果があるプロジェクトだったらやるべきかなとは思います。
PG_DATAのコールドバックアップを再利用するよう修正
これは先ほど紹介した記事から着想を覚えて行った、オリジナルの高速化です。Executorがdockerであれば、先ほどの記事で紹介された手法がいいでしょう。
軽く説明しておくと、parallel_testを使うとテストが並列で行えるので速くなる一方、その分データベースを作らなければならず、そこに結構時間を取られます。各コンテナでDB作成を行わせていたのですが、4つのデータベースを作るのに大体40秒~1分くらいかかっていました。
これを、先ほどの記事では、データベースに関わる変更があるかどうかわかるものと、pg_dumpall.sql というファイルをキャッシュしておいて、変更がなければpg_dumpall.sql からデータベースを作るというものでした。この方法だと、わざわざRails を介さずにデータベースを作成することができるため、高速化が期待できます。
しかし、docker-composeならば、更にPGDATA(PostgreSQL のデータが保存されている場所)を丸々コールドバックアップしてロードすれば、sql を通してデータベースを作成する手間も省けるのでは?🤔と考えました。最終的にはそれは思った通りで成功し、40秒~1分かかっていた処理が6~10秒になったので効果ありましたが、そこにたどり着くまでに結構ハマりました。
まず、コールドバックアップを取らなければならないので、PostgreSQL を停止しなければなりません。そこで、docker-compose exec postgres /bin/bash
でpostgresのコンテナの中に入り、pg_ctl stop
を実行したところ、コンテナごと停止しました。まぁ確かに、コンテナが実行していたプロセスを止めたので、そうなるか…と思いましたが、じゃあどうすればコールドバックアップを取れるのか?
結論としては、docker-compose run -d postgres /bin/bash
で、一時的にpostgresのコンテナを立ち上げるのですが、PostgreSQL を立ち上げずにbash を立ち上げさせておきます。その際に、一時的なコンテナ名を取得します。
そして、そのコンテナに対してバックアップを行わせました。
pg_container_name=`docker-compose run --rm -d postgres /bin/bash`
docker run --rm --volumes-from $pg_container_name -v ~/caches:/backup busybox tar cvf /backup/pg_data.tar -C / var/lib/postgresql/data
docker kill $pg_container_name
あとは、~/caches/pg_data.tarを各コンテナでロードします。ロードするときも、postgresコンテナは停止させて、runで一時的なコンテナを立ち上げています。終わったら即docker killしています。
docker-compose stop postgres
pg_container_name=`docker-compose run --rm -d postgres /bin/bash`
docker run --rm --volumes-from $pg_container_name -v ~/caches:/backup busybox tar xvf /backup/pg_data.tar
docker kill $pg_container_name
docker-compose start postgres
データベースのセットアップは結構時間のかかる処理ですが、これでDBに変更がない限りは数秒で終わるようになりました👍
感想
ここにたどり着くまでに3週間くらいかかったと思うので、かなり大変でしたが、なんとかなりました。
開発環境とCI環境が同じコンテナを使うので、安心感はありますね。
ただ、テストがやや遅くなるのが…😢これからも高速化できそうなポイントがあったら模索していきます😀
2020-03-22 追記
ここまでの修正で、キャッシュがフル稼働するときの時間ではありますが、30~33分→25~28分くらいにはなりました。
極力docker loadをしない
最もパフォーマンスのよい並列数にする(parallel_testにて)
コンパイル キャッシュを再利用する
ライブラリの更新がなければ何もしない
データベースの更新がなければ何もしない
という方向性で、30分が25分になったので、約16%の高速化 です。
多分これ以上となると、テスト自体のリファクタリング をするか、parallelismを上げるしかないと思います!