2019-01-25 追記:CircleCI 2.1を使うとデフォルトで共通化ができるようになっています。 patorash.hatenablog.com
以下より、投稿時の原文です。
私が担当している製品では、RailsのテストをCircleCIで4並列で動かしているのだけれど、これがいちいち各コンテナ毎にコードのチェックアウト、ライブラリのインストールをしているのは無駄じゃないかなー?と常々思っていました。これが、workflowを使ったら解決できそうだとわかったので、やってみることにしました。
目標を定義する
作業前は、以下のようになっていました。
- コードのチェックアウト・・・各コンテナ毎に実行
- bundle install・・・各コンテナ毎に実行
- yarn install・・・各コンテナ毎に実行
- rspec・・・各コンテナ毎に実行
作業後は、このようにしたい。
- コードのチェックアウト・・・1コンテナで実行
- ライブラリのインストール・・・並列実行
- bundle install・・・1コンテナで実行
- yarn install・・・1コンテナで実行
- rspec・・・各コンテナで実行
これならば、ライブラリのインストールを並列で行えるので、速度改善が見込めるんじゃないか?と思いました。
config.ymlを修正する
まずは、定義も何もないところから、workflowだけを書いていきます。
workflows: version: 2 build: jobs: - checkout_code - bundle_dependencies: requires: - checkout_code - yarn_dependencies: requires: - checkout_code - test: requires: - bundle_dependencies - yarn_dependencies
まぁ、こんな感じでしょう。requires
を定義して、ライブラリのインストールが両方終わってからテストが実行されるようにしています。
その後は、以下のconfig.ymlを参考にして作ってみました。
順に書いてみます。
コードのチェックアウト
コードをチェックアウトして、キャッシュに保存しておきます。
jobs: checkout_code: docker: - image: circleci/ruby:2.5.1-node-browsers working_directory: ~/project steps: - checkout - save_cache: key: v1-repo-{{ .Environment.CIRCLE_SHA1 }} paths: - ~/project # 省略
ライブラリのインストール
bundle_dependencies
先ほどキャッシュしたコードと、過去のgemのキャッシュを戻した後、bundle install
を実行し、またキャッシュに保存しておきます。
jobs: # 省略 bundle_dependencies: docker: - image: circleci/ruby:2.5.1-node-browsers working_directory: ~/project steps: - restore_cache: keys: - v1-repo-{{ .Environment.CIRCLE_SHA1 }} - restore_cache: keys: - v1-bundle-{{ checksum "Gemfile.lock" }} - v1-bundle - run: bundle check --path=vendor/bundle || bundle install --path vendor/bundle --jobs 4 --retry 3 - save_cache: key: v1-bundle-{{ checksum "Gemfile.lock" }} paths: - ./vendor/bundle # 省略
yarn_dependencies
こちらもbundle_dependenciesと同様に。yarn
を行ってJSライブラリを入れて、またキャッシュに保存しています。
jobs: # 省略 yarn_dependencies: docker: - image: circleci/ruby:2.5.1-node-browsers working_directory: ~/project steps: - restore_cache: keys: - v1-repo-{{ .Environment.CIRCLE_SHA1 }} - restore_cache: keys: - v1-yarn-{{ checksum "yarn.lock" }} - v1-yarn - run: name: Install dependencies command: yarn - save_cache: key: v1-yarn-{{ checksum "yarn.lock" }} paths: - ./node_modules # 省略
test
テストは、並列実行したいので、parallelism
を指定しています。また、テストにDBが必要なので、DBのdocker imageの指定も行っています。
やっていることは以下の通り。
- 各コンテナで、ここまでのworkflowで行ったキャッシュを取得する
- DBのセットアップ
- RSpecの実行
- 結果の収集
jobs: # 省略 test: docker: - image: circleci/ruby:2.5.1-node-browsers environment: RAILS_ENV: test TZ: "/usr/share/zoneinfo/Asia/Tokyo" - image: circleci/postgres:9.6-alpine-postgis working_directory: ~/project parallelism: 4 steps: - restore_cache: keys: - v1-repo-{{ .Environment.CIRCLE_SHA1 }} - restore_cache: keys: - v1-bundle-{{ checksum "Gemfile.lock" }} - restore_cache: keys: - v1-yarn-{{ checksum "yarn.lock" }} - v1-yarn - run: bundle --path vendor/bundle # Database setup - run: bundle exec rake db:create - run: bundle exec rake db:schema:load # run tests! - run: name: run tests command: | mkdir /tmp/test-results TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)" bundle exec rspec --format progress \ --format RspecJunitFormatter \ --out /tmp/test-results/rspec.xml \ --format progress \ $TEST_FILES # Save artifacts - store_test_results: path: /tmp/test-results - store_artifacts: path: /tmp/test-results destination: test-results # 省略
これでいいはずですが、./circleci/config.yml
の構造が間違ってないか、確認しておいた方がいいでしょう。
circleci config validate -c .circleci/config.yml
コマンドがない場合は、CircleCI local cliを入れておくこと。
CircleCIを実行する
workflowが正しく動いているか確認してみます。
おおー、動きました!!見た目は理想通りです。
config.ymlのリファクタリング
このままだと、重複コードがたくさんあるので、リファクタリングを行います。 こちらのブログを参考にさせてもらいました。
共通部分を以下のように括り出しました。
references: defaults: &defaults working_directory: ~/project ruby_docker_image: &ruby_docker_image image: circleci/ruby:2.5.1-node-browsers environment: RAILS_ENV: test TZ: "/usr/share/zoneinfo/Asia/Tokyo" restore_code_cache: &restore_code_cache restore_cache: keys: - v1-repo-{{ .Environment.CIRCLE_SHA1 }} restore_gem_cache: &restore_gem_cache restore_cache: keys: - v1-bundle-{{ checksum "Gemfile.lock" }} - v1-bundle restore_node_module_cache: &restore_node_module_cache restore_cache: keys: - v1-yarn-{{ checksum "yarn.lock" }} - v1-yarn
これを適用していきます。
適用した完成品がこちら。
version: 2 references: defaults: &defaults working_directory: ~/project ruby_docker_image: &ruby_docker_image image: circleci/ruby:2.5.1-node-browsers environment: RAILS_ENV: test TZ: "/usr/share/zoneinfo/Asia/Tokyo" restore_code_cache: &restore_code_cache restore_cache: keys: - v1-repo-{{ .Environment.CIRCLE_SHA1 }} restore_gem_cache: &restore_gem_cache restore_cache: keys: - v1-bundle-{{ checksum "Gemfile.lock" }} - v1-bundle restore_node_module_cache: &restore_node_module_cache restore_cache: keys: - v1-yarn-{{ checksum "yarn.lock" }} - v1-yarn jobs: checkout_code: <<: *defaults docker: - *ruby_docker_image steps: - checkout - save_cache: key: v1-repo-{{ .Environment.CIRCLE_SHA1 }} paths: - ~/project bundle_dependencies: <<: *defaults docker: - *ruby_docker_image steps: - *restore_code_cache - *restore_gem_cache - run: bundle check --path=vendor/bundle || bundle install --path vendor/bundle --jobs 4 --retry 3 - save_cache: key: v1-bundle-{{ checksum "Gemfile.lock" }} paths: - ./vendor/bundle yarn_dependencies: <<: *defaults docker: - *ruby_docker_image parallelism: 1 steps: - *restore_code_cache - *restore_node_module_cache - run: name: Install dependencies command: yarn - save_cache: key: v1-yarn-{{ checksum "yarn.lock" }} paths: - ./node_modules test: <<: *defaults docker: - *ruby_docker_image - image: circleci/postgres:9.6-alpine-postgis parallelism: 4 steps: - *restore_code_cache - *restore_gem_cache - *restore_node_module_cache - run: bundle --path vendor/bundle # Database setup - run: bundle exec rake db:create - run: bundle exec rake db:schema:load # run tests! - run: name: run tests command: | mkdir /tmp/test-results TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)" bundle exec rspec --format progress \ --format RspecJunitFormatter \ --out /tmp/test-results/rspec.xml \ --format progress \ $TEST_FILES # Save artifacts - store_test_results: path: /tmp/test-results - store_artifacts: path: /tmp/test-results destination: test-results workflows: version: 2 build: jobs: - checkout_code - bundle_dependencies: requires: - checkout_code - yarn_dependencies: requires: - checkout_code - test: requires: - bundle_dependencies - yarn_dependencies
だいぶすっきりとしたコードになりました。
テストは速くなったのか?
重複は排除できたのですが、肝心のテストの速度は速くなるどころか、少々遅くなっています…😢
原因は明白で、jobを細かく分けたため、docker imageのロードに時間がかかっているためです(各ワークフロー毎にだいたい30秒)。元々30秒かかっていたところが、3段階に分かれたので、30*3=90秒かかるようになりました。しかしこれも運次第で、テストが実行されたホストにdocker imageのキャッシュがあれば1〜2秒で終わるのですが、ないと30秒かかるという状態です。完全な条件下では、ライブラリのインストールが並列になるので、多少は速くなるのでは?🤔と思います。
とはいえ、好みとしてはこちらのコードなので、これでしばらく運用してみようかなと考えています。