SmartHR Tech Blog

SmartHR 開発者ブログ

Rails のテスト実行時間を60分から6分に短縮するまで

こんにちは。SmartHR のエンジニアの @meganemura です。

SmartHR はひとつの Rails アプリのリポジトリで開発が進められており、GitHub への Pull Request 作成などを契機に CircleCI でテストの実行や静的解析によるコード品質のチェックを継続的に実施しています。 しかし、プロダクトの成長と共に CI の実行時間が増え、またエンジニアの増加につれ CI のキュー待ちの時間も増え、実行完了までの時間が日々増え続けています。 その状況に対して、 Buildkite という CI サービスを利用して CI 環境の速度を改善した取り組みについて紹介します。

背景

以前にこのテックブログの CircleCI 2.0 の利用 の記事を公開した時点で全体のテスト実行が 40 分弱程度になっていたのですが、現在 50 分弱から 60 分程度にまで増加しています。ビルドのプロセスに大きな変更はないため、これはプロダクトの機能追加・改善に伴ったテストケースの増加によるものと考えています。参考までに RSpec のテストケースは記事の執筆時点で約 12500 です。

SmartHR ではテストが通ったコードのみをマージすることにしています。テストが完了するまでの待ち時間が増えた結果、コードを push して次の作業をしていたらテストが失敗したので、前の作業に戻る、そのたびに頭の切り替えが発生して作業効率が悪くなる、ということが起こっています。 プロダクトの品質とスピードとを両立したプロダクト開発のためにも CI でのビルド時間短縮の必要性を感じて速度改善に取り組み始めました。

テストの中で最も時間が必要となっていたのは Rails アプリのテスト (RSpec) で CircleCI でコンテナ 5並列 で実行して 40 分ほどの実行時間となっていました。並列数を上げたところ多少は改善したのですが、今度は実行までのキュー待ちが長くなってしまう状況になっていました。 今後もテストの実行時間は増加し、開発者も増えることが予定されているため、テストの並列数をテストファイル数に近づけてとにかく早く実行完了できるような CI サービスを調べていき、 Shopify の事例 を知り、AWS アカウントさえあれば試せるということもあり CI サービスの Buildkite の試用を開始しました。

Buildkite はどんなもの?

Buildkite は一般的な CI サービスと異なり、このサービス自体はビルドの実行は担わず、主としてビルド全体のパイプライン管理を行うことに特化しています。 ビルドの実行のためには Buildkite の Agent を実行したマシンを用意します。Agent の数はユーザが任意の数立ち上げることが可能になっています。

ビルド全体に対して、テストランナーや Linter の実行をジョブという単位で分けたとき、それらの各ジョブの実行状況を Buildkite が管理します。実行待ちのジョブを Agent が取得・実行して結果を返すことを繰り返してビルドが完了します。

自前で実行環境を用意するということはその環境構築や維持の手間が増えてしまうという欠点がありますが、Buildkite では CloudFormation のテンプレートが配布 されているため AWS のアカウントさえあれば構築にはほとんど時間はかかりません。また、この CloudFormation の設定でインスタンスタイプやオートスケールのインスタンス上限数などが設定可能です。実行環境のインフラを自前で用意するため、1ビルドの並列数や同時実行ビルドの数を好きなタイミングで変更することができます。オートスケールの条件は Buildkite の未完了ジョブの有無による設定となっています。これは CloudFormation によって定義された Lambda function が Buildkite から取得した未完了ジョブ数を CloudWatch のメトリクスとして書き出し、それを利用する仕組みになっています。

一般的な CI サービスと同じく Web 上からのビルド結果の確認やリトライ、GitHub の Webhook を受け取ってビルドのキューに追加する、Slack に結果を通知する、といった機能は備えているので、自分たちで作業が必要なのはあくまで実行環境の準備のみとなります。

Buildkite を利用するまで

SmartHR の Rails アプリのテストを Buildkite で実行するために次のような準備を行いました。

  • 上記の CloudFormation を使うための準備 (インスタンス数の制限緩和の依頼も)
  • それぞれの Agent ごとに必要なミドルウェアを用意してテストを実行するための docker-compose.yml の作成
  • docker-compose run で実行するそれぞれのジョブに対応したスクリプトの作成 (RSpec, RuboCop, karma, eslint, ...)
  • RSpec の実行対象テストファイルをノード数と実行時間に合わせて分割して実行するための knapsack gem の設定
  • Buildkite のパイプライン定義ファイルの作成

結果として、次のようなパイプラインで処理が行われています。

(各ジョブ名に :emoji: が使える のが結構うれしい)

パイプラインの流れは、はじめに1台で依存パッケージを含んだイメージをビルドして、AWS ECR に push したのち、次に複数台(100台程度で実行)でイメージを pull して各種テストを実行するようになっています。 各種テストは互いに依存しないような単位でジョブを分割して実行しています。最も時間のかかっていた RSpec は 95 個のジョブに分割しており、RSpec の実行部分は約1〜3分で完了するようになりました。

また、 Buidlkite ではジョブの種類ごとに実行する Agent を指定できるため Docker のビルドに利用するインスタンスだけをスケールダウンしにくい設定の別の CloudFormation Stack として用意しています。

CloudFormation のテンプレートは一般的などの AWS リージョンでも利用できるようになっているため、料金の安い Oregon (us-west-2) のリージョンで起動させています。

試してみた結果どうだったか

いいところ

テスト全体の実行は最も速い場合で 6 分で完了するようになりました。現在は Agent を最大200台動かし、1ビルドあたり最大100のジョブに分割して実行しています。 docker compose で実行するための環境の準備は CircleCI 2.0 への移行の際に Dockerfile が整備済みのため、大きく時間のかかる作業は必要ありませんでした。

また AWS のアカウントさえあれば、(もちろん AWS の費用はかかりますが) サービスの試用期間の間でも100台での並列ビルド環境がお手軽に試すことができるというのはとても良い点でした。

つらいところ

Buildkite のサービスによる問題ではないのですが、各種要因でテスト全体の実行時間がまだ安定していないのが現状です。 タイトル詐欺に近くて申し訳ないのですが、 6 分でビルドできる状態というのは次の前提が揃った時となっています。その前提は

  • ビルド用のインスタンスが全台起動済み
  • どのインスタンスも Docker のイメージについて最後のアプリケーションコードの COPY 以外のレイヤーについて push/pull ができている

というものです。

インスタンス起動時間待ち

ビルドする対象が多い場合には上記の条件が揃った状態になり、素早くビルドが完了しますが、インスタンスが立ち上がっていない状態だとここにインスタンス起動時間待ちの時間と Docker の build/push/pull の時間とが載りビルドの時間が増えていきます。

インスタンス起動時間待ちについては、たとえば営業時間中は起動している状態になるよう起動時間を長めにしてスケールダウンしにくくすることで解消することは可能です。先日 Amazon Web Service より EC2 および EBS について使用した秒単位の時間で請求が行われることが発表されましたので、これを活用するために EC2 のオートスケールのインスタンスを細かく上げ下げしようとすると、AWS 側での制限なのか 30 秒に 10 台までしかインスタンスが起動・終了できないことに気づきました。つまり 0 台の状態からだと 100 台のインスタンスが立ち上がり切るまでにそれだけで 5 分かかってしまいます。これは 100 台から 0 台に減らすときも同様でした。 オートスケールのインスタンス数増減が落ち着くのを待ってから次の増減が行われるため、インスタンス数を減らしている間にジョブの登録が行われると、完了までの時間が2倍以上かかってしまうということがありました。これが原因となりビルドに 30 分必要になる場合もときどき発生しています。

AWS ECR の API Rate Limit に注意

また、その他にも 150 の Agent を並列で実行してみたところ ECR の API Rate limit に引っかかるということがありました。 そのため、 ECR を使ったまま並列数をこれ以上に増やそうとすると、成功するまでリトライする・サポートに連絡する (EC2 の制限解除)・別リージョンで動かす などの方法での回避が必要そうです。

ジョブのタイムアウトを設定しておく

Buildkite のパイプライン定義ファイル作成はサービス特有なものなので、それなりに学習コストが必要でした。 気をつける点としては、ジョブの実行中に入力待ちになるなどの原因で処理が止まった場合に、永遠にジョブが完了しない状態が発生することです。たまたま気づいたので止められたものの、そのまま放置していたら EC2 の料金でたくさんのお金が飛んでいたことでしょう :money_with_wings: これはジョブごとのタイムアウト時間の設定することで回避可能です。

まとめ

この記事では弊社の CI 環境を Buidlkite という CI サービスを利用して速度改善をしたという話について書きました。

一般的な CI サービスで依存パッケージをキャッシュする処理は yaml などビルド用設定ファイルへの数行の追加で可能ですが、Buildkite では自分で書く必要があります。 その一方で、Buildkite を使った場合は、 AWS の S3 や EFS などのリソースをプロジェクトに適した形で使うことができ、実行環境以外のテストを分散処理するための面倒な部分を全ておまかせできるため、プロジェクトに合わせた CI 環境を構築していけることは他にない面白いところだと思いました。

現在は検証期間として CircleCI と並行して動かして片方だけ失敗することがないかを確認しています。安定次第移行するつもりです。 また、まだ高速化とビルド時間の安定化の余地が見えているので、安定して 5 分でビルドできるようになったらまた続報を書きたいと思います!!