アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

CI Test環境を作り直した話

CI Test環境を作り直した話

morishitaです。

いこーよは Rails アプリケーションです。
ユニットテストには Rspec を利用しています。

それなりに大きなアプリケーションなので全スペックを実行するにはそれなりに時間がかかります。
ローカルPCで全スペックを実行したことはないのですが、試みて終わるのを待てなくて途中で中断する程度には時間がかかります。

実装中のクラスを中心に部分的に Rspec を実行しながら実装しますが、思わぬところに影響していたりすることもまれにあるので、コードレビューに出す前には全部通しておきたいところです。

以前は Docker コンテナを動的に作ってその中で実行する自作の Web アプリケーションがあって、社内に立てたサーバで動かしていました。

一方で他のプロダクトではすでに CodeBuild で RSpec を実行していました。

tech.actindi.net

ではどうして今までいこーよだけはやってなかったかというと、他のプロダクトよりテスト数が多く、実行時間が 20〜30 分程度かかっていたためです。
社内サーバの環境は結構パワフルなマシンを使っていたのもあり、早い場合だと 10 分強で実行できていました。ただ、社内サーバ環境も混んでるとリソース不足で起動できないエラーが頻発したり、実行時間が長くなったりしていました。
あんまり快適に使える状況ではなくなってきたし、基本リモート勤務に移行したことで、この社内サーバにトラブルが発生してもすぐには対応できないという状況にもなりました。これらをどうにか改善する手段として、他のブロダクトで実績のある CodeBuild での CI Test をいこーよにもついに導入しました。

今回はそれについて書きます。

作ったもの

他のプロダクトで使っているものをそのまま使うこともできました。しかしもともとスキマ時間を利用して JavaScript から TypeScript にリファクタ中だったものが中途半端に放置していたのでこの機会に CDK で作り直しました。

作ったものの概要はこんな感じです。

  1. テスト実行環境である CodeBuild
  2. 上記 CodeBuild と連携する Lambda 関数
  3. 上記 2 つを cdk deploy で構築できる CDK コード

おんなじものを作り直しても面白くないので、ちょっとだけ改善しました。
今回、加えた改善点は次の2点です。

改善点1 - CDKによるオールインワンなインフラ構築

以前作ったものは実行環境である CodeBuild 自体は別途作成しておく必要がありました。今回は通知用の Lambda 関数に加え CodeBuild も構築するようにしました。ワンデプロイで必要なもの全部構築できるようにしました。

改善点2 - CodeBuild のレポートによる結果確認の改善

Lambda で Slack に通知の仕組みは以前と変わりません。
が、そのままというのも芸がないので CodeBuild のレポートを機能を利用して結果を確認しやすく改善しました。

CDK で構築する CodeBuild と Lambda についてもう少し説明します。

CodeBuild でやっていること

CodeBuild は RSpec を実行する実行環境です。RSpec は直接動かしているのではなく cocker-compose を使ってコンテナ内で実行します。
この docekr-compose にはテストを実行するために必要なミドルウェアも含んでおり、次のコンテナで構成しています。

  • RSpec コンテナ
  • Redis コンテナ
  • MySQL コンテナ
  • Solr コンテナ

docker-compose up --abort-on-container-exit でこれらのコンテナを起動し、RSpec を実行します。--abort-on-container-exit はテスト実行完了後、RSpec コンテナが終了したら他のミドルウェアコンテナも止めるためにつけています。

Lambda でやっていること

Lambda 関数は 2 つありで、それぞれの役割は次のとおりです。

  • トリガー関数:Github の Webhook を受けて CodeBuild でのテストを実行する
  • 結果通知関数:CodeBuild のイベントを受けて Slack に通知する

トリガー関数

API Gateway 経由して Github のプルリクエストへの操作の Webhook を受信して実行されます。
受信した Webhook には PullRequest のデータが含まれています。Lambda 関数はまず「自動テスト対象」というラベルの有無をチェックし、無ければそこで終了。あれば CodeBuild を実行します。
実行するためにはテスト対象のプルリクエストのタイトルやブランチ、コミット ID 等を取り出します。そしてそれらを環境変数として設定しつつ CodeBuild を実行します。
と同時に Github のプルリクエストの状態を更新してテスト中であることがわかるようにします。

結果通知関数

CodeBuild は CloudWatch イベントにイベントを通知します。
CodeBuild Build State Changeイベントを受けて実行が終了したことを検知し、それをトリガーに結果通知用の Lambda 関数が実行されます。
これはテスト結果を Slack と Github に通知します。

CodeBuild Build State Changeイベントに含まれる情報だけでは通知したい情報が足りません。なので、AWS の API を叩いて CodeBuild の実行結果やレポート情報を取得してデータをかき集めます。中でもアーティファクトには RSpec や SimpleCov の結果が入っているのでそれも S3 から取得してテストの成否やコードカバレッジを得ます。
集めた情報を整理して後述する通知を Slack に送ります。 と同時に Github のプルリクエストの状態にもテスト結果を反映します。

テストの実行

構築したこのテスト環境を利用するには Github のプルリクエストに「自動テスト対象」というラベルをつけるだけです。
ラベルを付けたとき、あるいはラベルの付いたプルリクエストに Push するとテストが実行されます1

テストが終了すると Github のプルリクエスト上で次のように表示されます。

f:id:HeRo:20201217000706p:plain
Githubプルリクエストでの表示

Slack にも次のように通知します。

f:id:HeRo:20201217000824p:plain
RSpec OK

通知には次の項目を含みます。

  • プルリクエストのタイトル(プルリクエストへのリンクにもなっています)
  • テストログを含むアーティファクトのダウンロードリンク
  • CodeBuild のレポートへのリンク
  • テスト結果
  • テストカバレッジ
  • 所要時間(と概算の CodeBuild の実行コスト)

上記はテストが成功したときですが、失敗したときには次のように通知されます。

f:id:HeRo:20201217000904p:plain
RSpec NG

テストは 8 スレッドで並行実行しています。成功したときには total だけを表示していますが、失敗したときにはどのスレッドに失敗したテストがあるのかわかるようにしています。というのも「詳細情報」からダウンロードできるアーティファクトにスレッドごとのテストログが含まれているからです。どのスレッドで fail したのかわかれば、どのログファイルを見ればいいのかがすぐわかる仕組みになっています。

実行時間の短縮

このエントリの最初に書いたように、移行できなかった理由は実行時間がかかりすぎるということでした。
せめて平均すると同程度の時間でテストが実行できるようにはしたいと思い、チューニングを試みました。

インスタンスタイプはコストとのバランスで general1.large (8vCPU, メモリ 15GB, $0.02/min) を利用しています2
この中で前述の通り docker-compose で必要なミドルウェアのコンテナと RSpec を実行するコンテナを起動してテストを行います。
Rspec はそのまま実行するのではなく test_queue を使って 8 スレッド並行で実行しています。それでも 20 〜 30 分かかっていました。

実行時間を短縮しようと最初に試したのは test_queue の並行実行数を増やすことでした。8vCPU のインスタンスということで 8 スレッドにしていましたが。これを増やしたり逆に減らしたりしてみました。結果的には効果ありませんでした。

他に、複数の CodeBuild に分散させるとかも検討はしましたが、あまり複雑になってしまうのも避けたかったので採用しませんでした。

結局効果があったのは、MySQL, Solr のコンテナを増やしたことでした。
test_queue を使うには互いに干渉しないように読み書きするリソースについては独立したものを用意する必要があります。つまり、8 スレッドで動かすなら MySQL や Solr、Redis を 8 個づつ用意する必要があります。
最初は次の図のような構成でした。

f:id:HeRo:20201217000941p:plain
改善前の構成

テスト実行中は各スレッドがそれぞれのテストデータを書き込んではロールバックして、また書き込むという状況になります。
内部のリソース消費状況を調べると RSpec のコンテナは思ったほど CPU もメモリも使っていませんでした。ということは RSpec コンテナではなくミドルウェアコンテナにボトルネックがあるのでは? と思い、MySQL, Solr のコンテナを増やしてみました。
コンテナを増やして処理を分散させてやると実行時間を 15 分程度に短縮できました。

コンテナが 1 つづつではコンテナ間の通信 I/O が詰まってしまうのかコンテナに割り当てられるリソースでは能力が不足するのか詳細は調べませんでしたが、速くなったので良しとしました。
最適数を求めてコンテナの数をいくつか試してみて、最終的に次のような構成になりました。

f:id:HeRo:20201217001036p:plain
改善後の更生

こうして、元々の社内サーバのベストタイムには及ばないものの平均すると同程度となり、まあ許容できるかなという時間に収まるようになりました。

CodeBuild のレポート機能

機能自体は以前から知っており、使う機会を伺っていたところでした。
丁度いい機会なので、導入してみました。
詳細は次のエントリで紹介しています。

tech.actindi.net

ここでは結果だけ述べると RSpec の結果を確認できるようにしたのですが導入してよかったです。

移行してみて

旧社内サーバの環境と並行稼動している期間があったのですが、みんな早々に旧環境は使わなくなり新しい CodeBuild 環境を使い始めました。なので作業性、実行時間、結果の確認しやすさを合わせた総合的な使い勝手は向上できたのではないかと思います。

最後に

アクトインディでは開発環境を改善しながらサービスを開発していけるエンジニアを募集しています。 actindi.net


  1. 旧環境では、Webの管理画面でテストするブランチを指定してコンテナを作り、初期化してテストを実行するという手動操作が必要でした。

  2. このサイズの上には general1.2xlarge があります。72vCPU, 144GiB というパワフルスペックなのですが、$0.25/min と実装中に頻繁に使うには高価すぎて使えません。