ようやく、CircleCIにてdocker-composeを使ってテストを流して問題なく終了するようになったので、そのためにやったことを書いておきます。 これまでの経緯は過去の記事を参照のこと。
目次
- 目次
- 不具合の修正について
- CircleCIでdocker-composeを動かすためにやったこと
- executorをmachineにしてテストすることのメリット・デメリット
- CircleCIのワークフロー
- .circleci/config.ymlを公開
- 工夫した点
- 感想
不具合の修正について
できたと思って記事を書いたのですが、実際に運用してみるとちょこちょこ不具合が出てきたので、この記事は随時更新していきます。すみません…。
- 👍2020-03-13修正済:testジョブでbundle installしろと言われて落ちる
- 👍2020-03-22修正済:artifactsの保存ができていない
CircleCIでdocker-composeを動かすためにやったこと
開発で使っているdocker-composeと同じものを丸々使ってテストが完走できることを目標としました。こうすることで、開発環境・テスト環境での差異がなくなるので、より安定した開発ができるはずです。
大きな変更点
実行環境をdockerからLinux VMに変更
executorをdockerからmachineに変更しました。machineは、LinuxのVMが立ち上がる設定です。
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のワークフローです。
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からはこの設定を削除しています(余計なので)。 詳しく知りたい人は過去の記事をどうぞ。
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に関しては、過去の記事を読んでください。
では、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に保存するように修正しました。
また、この修正をしている最中にも新たに高速化できそうなポイントが書かれていた記事を見つけたため、それを参考にさらに高速化しました。参照ページをリンクしておきます。
では、設定ファイルです。
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` # ライブラリの更新が必要な場合、docker-composeを起動してbundle install, yarn installを行う 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 # Load libraries 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 # bundle install if [ "${gem_cache_shasum}" != "${gem_file_shasum}" ]; then docker-compose exec test sh -c "bundle check || bundle install --clean" fi # yarn install if [ "${yarn_cache_shasum}" != "${yarn_file_shasum}" ]; then docker-compose exec test yarn install fi # Database Setup & Backup $PGDATA 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 # postgresのコンテナから$PGDATAをコールドバックアップする 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: | # テスト実行後なのでSimpleCovの結果をまとめる 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 # Save artifacts - 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がメモリ不足でエラーを起こすようになるので、以下の設定が必須です。
基本ブランチを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を立ち上げさせておきます。その際に、一時的なコンテナ名を取得します。
そして、そのコンテナに対してバックアップを行わせました。
# postgresのコンテナから$PGDATAをコールドバックアップする
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を上げるしかないと思います!