CircleCIとCodeBuildで爆速CIするためにしたことまとめ

<img width="400" alt="circleci_codebuild.png" src="https://qiita-image-store.s3.amazonaws.com/0/14794/b1c8db4b-2676-d857-019e-1745c372ca88.png">

あるプロジェクトでCIに25〜30分かかっていたのを10分切るぐらいに高速化できたので、やったことを細かく分けて書き出します。

## そもそも

そのプロジェクトではCircleCIで4コンテナ使って40分ぐらいかかっていましたが、AWS CodeBuildで10並列にして20分ぐらいに高速化することができていました。
ただ、db:migrateやbundle install, assets:precompileなどの環境準備の状態によって時間が伸びるケースがあり、10並列以上に増やしても分単位課金のCodeBuild料金が高くなりやすい傾向でした。
そこで、環境準備部分をCircleCI側に寄せ、CodeBuildはrspec実行のみで並列数を増やしやすくすることができるように改修しました。

## やったこと

この節では、そこそこ大きなCircleCIの設定ファイルから、Tipsになりそうなところを抜き出して説明してあります。
利用したサービス構成とすべてが組み合わさった設定ファイル類は、末尾に載せました。

### 1. checkoutにcacheを効かせる

checkoutでCircleCI上にリポジトリをクローン（単純なgit cloneではなくできるだけ転送量減らそうとはしてる）していますが、長く続いているプロジェクトだとローカル環境でgit cloneすると時間かかるなーって事があると思います。それはCircleCIも同じなので`.git`以下のファイルをcacheするとただcheckoutするだけより早くなりました。
`du -chs .git`でサイズを確認してみて大きいようなら導入してみるといいと思います。（そもそもリポジトリをきれいにするという方法もある気がします）

```..circleci/config.yml
commands:
  checkout_with_cache:
    steps:
      - restore_cache:
          keys:
            - git-cache-v1-{{ .Branch }}-{{ .Revision }}
            - git-cache-v1-{{ .Branch }}
            - git-cache-v1-master # <- default branch
      - checkout
      - save_cache:
          key: git-cache-v1-{{ .Branch }}-{{ .Revision }}
          paths:
            - .git
```

### 2. bundle installにcacheを効かせる

これはCircleCIのサンプルにもあります。
ただ、最後にworkflowを作る上で、restore_cacheするだけのときと、そのあとにbundle install, save_cacheをしたい場合で使い分けができるようにparametersで挙動を制御できるようにしました。

```..circleci/config.yml
commands:
  bundler:
    parameters:
      reload:
        type: boolean
        default: true
    steps:
      - restore_cache:
          keys:
            - gem-cache-v1-{{ .Branch }}-{{ .Revision }}
            - gem-cache-v1-{{ .Branch }}
            - gem-cache-v1-master # <- default branch
      - when:
          condition: << parameters.reload >>
          steps:
            - run:
                name: bundle install
                command: |
                  gem update bundler
                  bundle check --path=vendor/bundle || bundle install --path=vendor/bundle -j4
            - save_cache:
                key: gem-cache-v1-{{ .Branch }}-{{ .Revision }}
                paths:
                  - vendor/bundle
```

### 3. yarn installにcacheを効かせる

これはbundle installと同様です。 

```..circleci/config.yml
commands:
  yarn:
    parameters:
      reload:
        type: boolean
        default: true
    steps:
      - restore_cache:
          keys:
            - yarn-cache-v1-{{ .Branch }}-{{ .Revision }}
            - yarn-cache-v1-{{ .Branch }}
            - yarn-cache-v1-master # <- default branch
      - when:
          condition: << parameters.reload >>
          steps:
            - run: yarn install --cache-folder ~/.cache/yarn
            - save_cache:
                key: yarn-cache-v1-{{ .Branch }}-{{ .Revision }}
                paths:
                  - node_modules
                  - ~/.cache/yarn
```

### 4. assets:precompileにcacheを効かせる

これもbundle installと同様です。
あとは、assetsの設定に動的コンパイルOFFと、単一ファイル化ONを追加しました。

```..circleci/config.yml
commands:
  assets:
    parameters:
      reload:
        type: boolean
        default: true
    steps:
      - restore_cache:
          keys:
            - assets-cache-v1-{{ .Branch }}-{{ .Revision }}
            - assets-cache-v1-{{ .Branch }}
            - assets-cache-v1-master # <- defautl branch
      - when:
          condition: << parameters.reload >>
          steps:
            - run: RAILS_ENV=test CI=true bundle exec rails assets:clean assets:precompile
            - save_cache:
                key: assets-cache-v1-{{ .Branch }}-{{ .Revision }}
                paths:
                  - public/assets
                  - public/packs-test
                  - tmp/cache/assets
                  - tmp/cache/webpacker
```

```config/environments/test.rb
Rails.application.configure do
  ...

  if ENV["CI"] == "true"
    config.assets.compile  = false
    config.assets.compress = true
  end
end
```

### 5. CircleCI上で準備した環境をS3にアップロードする

rspecの実行以外はCircleCIに寄せたいので、準備した環境をtar.gzに圧縮してS3にアップロードする仕組みです。
`.tarignore`ファイルは`.gitignore`や`.dockerignore`と同じような役割の独自ファイルです。

```..circleci/config.yml
jobs:
  code_build:
    working_directory: ~/app
    docker:
      - image: circleci/ruby:2.5.3-node
    steps:
      - checkout_with_cache # <- checkout
      - bundler             # <- bundle install
      - yarn                # <- yarn install
      - assets              # <- assets:precompile
      - run: sudo apt install pigz # <- マルチコアでtarコマンドを使えるようにするライブラリ
      - run:
          name: Setup CodeBuild sources
          command: |
            mkdir -p ~/cache
            tar -c -I pigz -f ~/cache/src.tar.gz -X .circleci/.tarignore .
            bundle exec rake code_build:setup
```

```..circleci/.tarignore
./.bundle
./.git
./log/*.log
./node_modules
./tmp
```

```lib/tasks/ci/codebuild.rake
namespace :code_build do
  require "aws-sdk-s3"

  desc "Setup CodeBuild"
  task :setup do
    object = Aws::S3::Object.new(bucket_name: "**BUCKET_NAME**", key: "cache/#{ENV["CIRCLE_WORKFLOW_ID"]}.tar.gz")
    object.upload_file "#{Dir.home}/cache/src.tar.gz"
  end
end
```

### 6. specファイルを実績時間で分割できるようにtest metadataを読み込む

CircleCIには並列でテスト実行できるように、テストファイル群を並列数で分割するコマンドが組み込まれています。（[Running Tests in Parallel](https://circleci.com/docs/2.0/parallelism-faster-jobs)）
分割モードには、以前のテスト結果の時間データを元に分割するモードがあるのでそれが使えるように以前のテスト結果を読み込む仕組みを作りました。
このコマンドはかなりトリッキーな仕組みになっていて、CircleCIのジョブがsuccessで完了したあとに取得する必要があるため、workflowを駆使しています。

```..circleci/config.yml

workflows:
  ci_flow:
    jobs:
      - codebuild
      - save_test_metadata:
          requires:
            - code_build

jobs:
  code_build:
    ... # <- 環境準備

    - test_metadata: # <- test_metadataのcache読み込み
        restore: true
    - run: ... # <- CodeBuildの実行（後述(*1)）

    ... # <- テスト結果の登録など

    - run:
        name: Cache build number
        command: echo $CIRCLE_BUILD_NUM > .build_number # <- test metadata取得用のビルドID
    - persist_to_workspace: # <- workflowの後続ジョブにデータ引き継ぎ
        root: ~/app
        paths:
          - .build_number

  save_test_metadata:
    working_directory: ~/app
    docker:
      - image: circleci/ruby:2.5.3-node
    steps:
      - attach_workspace: # <- code_buildジョブ(先発ジョブ)のデータ引き継ぎ
          at: ~/app
      - test_metadata: # <- test metadataのcache化
          save: true

commands:
  test_metadata:
    parameters:
      restore:
        type: boolean
        default: false
      save:
        type: boolean
        default: false
    steps:
      - when:
          condition: << parameters.restore >>
          steps:
            - restore_cache:
                keys:
                  - metadata-cache-v1-{{ .Branch }}-{{ .Revision }}
                  - metadata-cache-v1-{{ .Branch }}
                  - metadata-cache-v1-master # <- default branch
                  - metadata-cache-v1
            - run:
                name: Download test metadata # <- CircleCi導入直後などcacheが存在しないときは強制的に取り込めるようにする
                command: |
                  if [ ! -e circle-test-results ]; then
                    mkdir circle-test-results
                    curl "https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/**ビルドID（ハードコーディング）**/tests?circle-token=$CIRCLE_API_TOKEN" > circle-test-results/results.json
                  fi
      - when:
          condition: << parameters.save >>
          steps:
            - run:
                name: Get test metadata
                command: |
                  mkdir circle-test-results
                  curl "https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/$(cat .build_number)/tests?circle-token=$CIRCLE_API_TOKEN" > circle-test-results/results.json
            - save_cache:
                key: metadata-cache-v1-{{ .Branch }}-{{ .Revision }}
                paths:
                  - circle-test-results
```

### 7. CodeBuildの実行を並列にリクエストする(*1)

6の説明で省略していたCodeBuildの実行指示部分です。
CodeBuildへの実行指示は、ビルドごとに流してほしいspecファイルを環境変数TESTFILESに設定してリクエストを出すようにしました。
並列数が増えるとリクエストするだけでもそこそこ時間がかかるのでrakeタスクの実行自体も並列でリクエストできるようにしてあります。

```..circleci/config.yml
jobs:
  code_build:
    ... # <- 環境準備、test_metadataのcache読み込み

    - run:
        name: Start CodeBuild(1~10) with async
        environment:
          REPO_URL: **ECR_IMAGE_NAME**:2.5.3 # <- CodeBuildで使うdocker image名
          CIRCLE_INTERNAL_TASK_DATA: "." # <- test metadataの場所
          NODE_START: 1
          NODE_END: 10
        command: bundle exec rake code_build:start # <- 20並列中の1~10の実行指示
        background: true # <- このrunコマンドを非同期に実行する（完了を待たずに次のrunコマンドを実行する）
    - run:
        name: Start CodeBuild(11~20) with async
        environment:
          REPO_URL: **ECR_IMAGE_NAME**:2.5.3
          CIRCLE_INTERNAL_TASK_DATA: "."
          NODE_START: 11
          NODE_END: 20
        command: bundle exec rake code_build:start # <- 20並列中の11~20の実行指示
        background: true
```

```lib/tasks/ci/codebuild.rake
namespace :code_build do
  require "aws-sdk-codebuild"

  desc "Start CodeBuild"
  task :start do
    options = {
      project_name:                "**CODEBUILD_PROJECT_NAME**",
      source_version:              ENV["CIRCLE_WORKFLOW_ID"],
      buildspec_override:          File.read(".codebuild/buildspec.yml"),
      image_override:              "#{ENV["AWS_ECR_ACCOUNT_URL"]}/#{ENV["REPO_URL"]}",
      compute_type_override:       "BUILD_GENERAL1_SMALL",
      timeout_in_minutes_override: 10,
      artifacts_override: {
        type:                      "S3",
        location:                  "**BUCKET_NAME**",
        path:                      "result/#{ENV["CIRCLE_WORKFLOW_ID"]}",
        namespace_type:            "NONE",
        name:                      "results.zip",
        packaging:                 "ZIP",
        override_artifact_name:    true,
        encryption_disabled:       true,
      },
    }
    client = Aws::CodeBuild::Client.new

    ENV["NODE_START"].upto(ENV["NODE_END"]) do |i|
      files = `circleci tests glob "spec/**/*_spec.rb" | circleci tests split --total=#{ENV["CODEBUILD_NODE_TOTAL"]} --index=#{i} --split-by=timings --timings-type=filename`.gsub("\n", " ")

      options[:environment_variables_override] = [
        { name: "TESTFILES",          value: files,                     type: "PLAINTEXT" },
        { name: "CI",                 value: "true",                    type: "PLAINTEXT" },
        { name: "RAILS_ENV",          value: "test",                    type: "PLAINTEXT" },
        { name: "CIRCLE_API_TOKEN",   value: ENV["CIRCLE_API_TOKEN"],   type: "PLAINTEXT" },
        { name: "CIRCLE_BUILD_NUM",   value: ENV["CIRCLE_BUILD_NUM"],   type: "PLAINTEXT" },
        { name: "CIRCLE_WORKFLOW_ID", value: ENV["CIRCLE_WORKFLOW_ID"], type: "PLAINTEXT" },
      ]
      client.start_build(options)
    end
  end
end
```

## 利用したサービス構成

<img width="1000" alt="サービス構成.png" src="https://qiita-image-store.s3.amazonaws.com/0/14794/9fcfc9fb-905c-e40c-c9e9-c1177ff0fc24.png">

GitHubとCircleCI部分はおなじみの構成です。
CircleCIはまず最初に、CodeBuild上で実行できる環境の準備（git cloneやassets:precompileなど）を実行してできあがったものをS3にアップロードします。
次にCodeBuildに分割したテストファイルリストをリクエストし、CodeBuildがECRからdocker imageのpull、S３からソースコード類（構築済の環境）をダウンロードして、渡されたテストファイルリストにしたがってrspecを実行します。
（ECRに登録してあるdocker imageも別のタイミングでCircleCIから登録したCI用のベースイメージです。この部分の紹介は別途予定しています。）
CodeBuildで実行したrspecの結果はS3に保存され、CircleCIはその保存された結果ファイルが並列数分たまるのを待って、揃ったらダウンロードを始めるようになっています。
最後にテスト結果をCircleCI側で解析してCI完了という流れです。

## 結果

改修前は10並列なのでrspecの実行時間自体長いのですが、bundle installやassets:precompileなどの事前準備部分でも10分程度時間がかかっていました。
改修後は30並列にして1ビルドあたりのrspec実行時間自体が短くなっただけではなく、事前準備部分の時間も3分未満になってrspec以外の時間を削減できました。
CodeBuildの料金観点でも、CodeBuildの利用時間が減っているためそのまま効いてきています。

provisioningにかかっている時間がECRからdocker imageをpullしている部分で、ここを短くする方法があれば教えてほしいというのが残課題です。

| | before(10並列) | after(30並列) |
|---|---|---|
| ビルド一覧 | <img width="1000" alt="ビルド一覧.png" src="https://qiita-image-store.s3.amazonaws.com/0/14794/d1bfd1ad-3120-72c0-9e9b-d6d6d20278ed.png"> | <img width="1000" alt="ビルド一覧.png" src="https://qiita-image-store.s3.amazonaws.com/0/14794/2f67ff7b-9356-c856-5138-aab540516a46.png"> |
| ビルド詳細 | <img width="1000" alt="ビルド詳細.png" src="https://qiita-image-store.s3.amazonaws.com/0/14794/da209114-1fd0-fadb-2d99-6fc88856772a.png"> | <img width="1000" alt="ビルド詳細.png" src="https://qiita-image-store.s3.amazonaws.com/0/14794/787ad121-6c2e-d92b-7bd1-359b491193f5.png"> |

## さいごに

CircleCIで事前準備して、CodeBuildの仕事を単純化することで並列数を上げやすくして時間短縮と料金削減することができました。
新しいCI方式で運用を始めて間もないのでrakeタスクなどガッツリ書いている状態なので、徐々にgem化やorb化していくのが次の目標です。

## 付録

設定ファイル類を載せました。
`.circleci/config.yml`はとても長いです m(_ _)m

### .circleci/config.yml

```..circleci/config.yml
version: 2.1

ruby_version: &ruby_version
  2.5.3

workflows:
  ci_flow:
    jobs:
      - before_setup
      - code_build:
          repo: **ECR_IMAGE_NAME**
          requires:
            - before_setup
      - save_test_metadata:
          requires:
            - code_build

jobs:
  before_setup:
    executor: ruby
    steps:
      - restore_workspace

  code_build:
    executor: ruby
    parameters:
      version:
        type: string
        default: *ruby_version
      repo:
        type: string
    steps:
      - run: sudo apt install pigz
      - restore_workspace:
          reload: false
      - run:
          name: Setup CodeBuild sources
          command: |
            mkdir -p ~/cache
            tar -c -I pigz -f ~/cache/src.tar.gz -X .circleci/.tarignore .
            bundle exec rake code_build:setup
      - test_metadata:
          restore: true
      - run:
          name: Start CodeBuild(1~10) with async
          environment:
            REPO_URL: << parameters.repo >>:<< parameters.version >>
            CIRCLE_INTERNAL_TASK_DATA: "."
            NODE_START: 1
            NODE_END: 10
          command: bundle exec rake code_build:start
          background: true
      - run:
          name: Start CodeBuild(11~20) with async
          environment:
            REPO_URL: << parameters.repo >>:<< parameters.version >>
            CIRCLE_INTERNAL_TASK_DATA: "."
            NODE_START: 11
            NODE_END: 20
          command: bundle exec rake code_build:start
          background: true
      - run:
          name: Wait CodeBuild jobs
          command: bundle exec rake code_build:wait
          no_output_timeout: 15m
      - run:
          name: Check CodeBuild artifacts
          command: |
            mkdir -p ~/tmp/test-results/rspec ~/log
            unzip -q -d ~/tmp/test-results/rspec "tmp/$CIRCLE_WORKFLOW_ID/*.zip"
            mv ~/tmp/test-results/rspec/*.log ~/log > /dev/null 2>&1 || true
            # logファイルが存在したらrspecでfailureがあったと判断する
            if [ -n "$(ls ~/log)" ]; then exit 1; fi
          when: always
      - store_test_results:
          path: ~/tmp/test-results
      - store_artifacts:
          path: ~/log
          destination: log
      - run:
          name: Cache build number
          command: echo $CIRCLE_BUILD_NUM > .build_number
      - persist_to_workspace:
          root: ~/app
          paths:
            - .build_number

  save_test_metadata:
    executor: ruby
    steps:
      - attach_workspace:
          at: ~/app
      - test_metadata:
          save: true

commands:
  restore_workspace:
    parameters:
      reload:
        type: boolean
        default: true
      bundle_path:
        type: boolean
        default: true
    steps:
      - checkout_with_cache
      - bundler:
          reload: << parameters.reload >>
      - when:
          condition: << parameters.bundle_path >>
          steps:
            - run:
                name: Set bundle path
                command: |
                  gem update bundler
                  bundle config --local path vendor/bundle
      - yarn:
          reload: << parameters.reload >>
      - assets:
          reload: << parameters.reload >>

  checkout_with_cache:
    steps:
      - restore_cache:
          keys:
            - git-cache-v1-{{ .Branch }}-{{ .Revision }}
            - git-cache-v1-{{ .Branch }}
            - git-cache-v1-master
      - checkout
      - save_cache:
          key: git-cache-v1-{{ .Branch }}-{{ .Revision }}
          paths:
            - .git

  bundler:
    parameters:
      reload:
        type: boolean
        default: true
    steps:
      - restore_cache:
          keys:
            - gem-cache-v1-{{ .Branch }}-{{ .Revision }}
            - gem-cache-v1-{{ .Branch }}
            - gem-cache-v1-master
      - when:
          condition: << parameters.reload >>
          steps:
            - run: |
                gem update bundler
                bundle check --path=vendor/bundle || bundle install --path=vendor/bundle -j4
            - save_cache:
                key: gem-cache-v1-{{ .Branch }}-{{ .Revision }}
                paths:
                  - vendor/bundle

  yarn:
    parameters:
      reload:
        type: boolean
        default: true
    steps:
      - restore_cache:
          keys:
            - yarn-cache-v1-{{ .Branch }}-{{ .Revision }}
            - yarn-cache-v1-{{ .Branch }}
            - yarn-cache-v1-master
      - when:
          condition: << parameters.reload >>
          steps:
            - run: yarn install --cache-folder ~/.cache/yarn
            - save_cache:
                key: yarn-cache-v1-{{ .Branch }}-{{ .Revision }}
                paths:
                  - node_modules
                  - ~/.cache/yarn

  assets:
    parameters:
      reload:
        type: boolean
        default: true
    steps:
      - restore_cache:
          keys:
            - assets-cache-v1-{{ .Branch }}-{{ .Revision }}
            - assets-cache-v1-{{ .Branch }}
            - assets-cache-v1-master
      - when:
          condition: << parameters.reload >>
          steps:
            - run: RAILS_ENV=test CI=true bundle exec rails assets:clean assets:precompile
            - save_cache:
                key: assets-cache-v1-{{ .Branch }}-{{ .Revision }}
                paths:
                  - public/assets
                  - public/packs-test
                  - tmp/cache/assets
                  - tmp/cache/webpacker

  test_metadata:
    parameters:
      restore:
        type: boolean
        default: false
      save:
        type: boolean
        default: false
    steps:
      - when:
          condition: << parameters.restore >>
          steps:
            - restore_cache:
                keys:
                  - metadata-cache-v1-{{ .Branch }}-{{ .Revision }}
                  - metadata-cache-v1-{{ .Branch }}
                  - metadata-cache-v1-master
                  - metadata-cache-v1
            - run:
                name: Download test metadata
                command: |
                  if [ ! -e circle-test-results ]; then
                    mkdir circle-test-results
                    curl "https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/**ビルドID（ハードコーディング）**/tests?circle-token=$CIRCLE_API_TOKEN" > circle-test-results/results.json
                  fi
      - when:
          condition: << parameters.save >>
          steps:
            - run:
                name: Get test metadata
                command: |
                  mkdir circle-test-results
                  curl "https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/$(cat .build_number)/tests?circle-token=$CIRCLE_API_TOKEN" > circle-test-results/results.json
            - save_cache:
                key: metadata-cache-v1-{{ .Branch }}-{{ .Revision }}
                paths:
                  - circle-test-results

executors:
  ruby:
    working_directory: ~/app
    parameters:
      version:
        type: string
        default: *ruby_version
    docker:
      - image: circleci/ruby:<< parameters.version >>-node
```

### .circleci/.tarignore

```..circleci/.tarignore
./.bundle
./.git
./log/*.log
./node_modules
./tmp
```

### .codebuild/buildspec.yml

```..codebuild/buildspec.yml
version: 0.2

run-as: circleci

phases:
  install:
    commands:
      - mkdir -p $CODEBUILD_SRC_DIR/cache
      - |
        ruby -r aws-sdk-s3 \
             -e 'object = Aws::S3::Object.new(bucket_name:"**BUCKET_NAME**", key:"cache/#{ENV["CIRCLE_WORKFLOW_ID"]}.tar.gz")' \
             -e 'object.download_file("#{ENV["CODEBUILD_SRC_DIR"]}/cache/src.tar.gz")'
      - tar -x -I pigz -f $CODEBUILD_SRC_DIR/cache/src.tar.gz -C ~/app
    finally:
      - mkdir -p $CODEBUILD_SRC_DIR/tmp/test-results/rspec
      - cd ~/app
  pre_build:
    commands:
      - sudo service mysql start
      - bundle config --local path vendor/bundle
      - bundle exec rake circleci:watch &
      - RAILS_ENV=test bundle exec rails db:migrate
  build:
    commands:
      - |
        bundle exec rspec $TESTFILES -f progress -f RspecJunitFormatter \
          -o $CODEBUILD_SRC_DIR/tmp/test-results/rspec/${CODEBUILD_BUILD_ID#*:}.xml || \
          cp log/test.log $CODEBUILD_SRC_DIR/tmp/test-results/rspec/${CODEBUILD_BUILD_ID#*:}.log
    finally:
      - touch $CODEBUILD_SRC_DIR/tmp/test-results/rspec/${CODEBUILD_BUILD_ID#*:}.xml

artifacts:
  name: ${CODEBUILD_BUILD_ID#*:}.zip
  discard-paths: yes
  base-directory: tmp/test-results
  files:
    - "**/*"
```

### codebuild.rake

```lib/tasks/ci/codebuild.rake
namespace :code_build do
  require "aws-sdk-codebuild"
  require "aws-sdk-s3"
  require "fileutils"

  desc "Setup CodeBuild"
  task :setup do
    object = Aws::S3::Object.new(bucket_name: "**BUCKET_NAME**", key: "cache/#{ENV["CIRCLE_WORKFLOW_ID"]}.tar.gz")
    object.upload_file "#{Dir.home}/cache/src.tar.gz"
  end

  desc "Start CodeBuild"
  task :start do
    options = {
      project_name:                "**CODEBUILD_PROJECT_NAME**",
      source_version:              ENV["CIRCLE_WORKFLOW_ID"],
      buildspec_override:          File.read(".codebuild/buildspec.yml"),
      image_override:              "#{ENV["AWS_ECR_ACCOUNT_URL"]}/#{ENV["REPO_URL"]}",
      compute_type_override:       "BUILD_GENERAL1_SMALL",
      timeout_in_minutes_override: 10,
      artifacts_override: {
        type:                      "S3",
        location:                  "**BUCKET_NAME**",
        path:                      "result/#{ENV["CIRCLE_WORKFLOW_ID"]}",
        namespace_type:            "NONE",
        name:                      "results.zip",
        packaging:                 "ZIP",
        override_artifact_name:    true,
        encryption_disabled:       true,
      },
    }
    client = Aws::CodeBuild::Client.new

    ENV["NODE_START"].upto(ENV["NODE_END"]) do |i|
      files = `circleci tests glob "spec/**/*_spec.rb" | circleci tests split --total=#{ENV["CODEBUILD_NODE_TOTAL"]} --index=#{i} --split-by=timings --timings-type=filename`.gsub("\n", " ")

      options[:environment_variables_override] = [
        { name: "TESTFILES",          value: files,                     type: "PLAINTEXT" },
        { name: "CI",                 value: "true",                    type: "PLAINTEXT" },
        { name: "RAILS_ENV",          value: "test",                    type: "PLAINTEXT" },
        { name: "CIRCLE_API_TOKEN",   value: ENV["CIRCLE_API_TOKEN"],   type: "PLAINTEXT" },
        { name: "CIRCLE_BUILD_NUM",   value: ENV["CIRCLE_BUILD_NUM"],   type: "PLAINTEXT" },
        { name: "CIRCLE_WORKFLOW_ID", value: ENV["CIRCLE_WORKFLOW_ID"], type: "PLAINTEXT" },
      ]
      client.start_build(options)
    end
  end

  desc "Wait CodeBuild"
  task :wait do
    bucket = Aws::S3::Bucket.new(name: "**BUCKET_NAME**")
    until bucket.objects(prefix: "result/#{ENV["CIRCLE_WORKFLOW_ID"]}").count == ENV["CODEBUILD_NODE_TOTAL"].to_i
      sleep 10 # seconds
    end

    FileUtils.mkdir_p "tmp/#{ENV["CIRCLE_WORKFLOW_ID"]}"
    bucket.objects(prefix: "result/#{ENV["CIRCLE_WORKFLOW_ID"]}").each do |object|
      next if object.size == 0
      object.download_file "tmp/#{ENV["CIRCLE_WORKFLOW_ID"]}/#{object.key.split("/").last}"
    end
  end
end

```

### circleci.rake

```lib/tasks/ci/circleci.rake
namespace :circleci do
  desc "Watch CircleCI status"
  task :watch do
    require "aws-sdk-codebuild"
    require "circleci"

    CircleCi.configure {|config| config.token = ENV["CIRCLE_API_TOKEN"] }
    job = CircleCi::Build.new("**GITHUB_USERNAME**", "**GITHUB_REPOSITORYNAME**", "github", ENV["CIRCLE_BUILD_NUM"])

    loop do
      case job.get.body["status"]
      when "success"
        return exit 0
      when "canceled", "timedout"
        Aws::CodeBuild::Client.new.stop_build(id: ENV["CODEBUILD_BUILD_ID"])
        return exit 1
      when "failed", nil
        return exit 1
      end

      sleep 10 # seconds
    end
  end
end
```