エンジニアのビリーです。
今年はCI改善PJに取り組んできましたが、その結果、CI実行時間を50分から13分に削減することができました。そのやり方を紹介していきたいと思います。
課題
CIの実行時間は2024年5月には50分以上かかっていました。長いCI時間はエンジニアの生産性に悪影響を与えるので改善することになりました。
以前のテストアーキテクチャでは、単一のECSタスクが複数プロセスを使ってテストを並列実行していました。すべてのテストは、アプリケーションと同じデータベースを使用していました。
※DBにはMySQL、Elasticsearchが含まれる
この設計には以下のような問題がありました。
- アプリケーションとテスト環境のデータベースが分離されていない
- 多くのテストがアプリケーションのデータベース内の既存データに依存しており、データが変更されるとテストが失敗しやすくなり、flaky(不安定)になる
- 並列実行中に複数のプロセスが同時にデータベースへアクセスすることで、デッドロックが発生する
- 水平スケーリングを行うと、デッドロックがさらに頻発する
解決策: 並列実行
これらの問題(データベースのデッドロック、データ依存によるテストの不安定性、水平スケーリングの困難さ)を解消するため、CIアーキテクチャを以下のように改善しました。
- ECSタスク内にテスト専用のローカルホストのデータベースを作成
- 既存データベースに依存するテストに対して、フィクスチャを導入
- テスト実行をECSタスク単位で並列化する
この設計により、テスト実行に以下の改善が見られました。
- 複数のECSタスクを使った並列CI実行により、容易に水平スケーリングが可能
- テスト数が増えても、ECSタスク数さえ増やせばCI実行時間を短縮することができる
- データベースでのデッドロックが発生しない
- テストが既存のデータベースデータに依存しなくなる
結果
CI改善は2つのフェーズで行われました。
- フェーズ1MySQLのローカルホスト移行(6月リリース)
- フェーズ2Elasticsearchのローカルホスト移行と並列化(10月リリース)
結果は、CI実行時間を50分から13分に削減することができました。
テストの分割
各ECSタスクには、環境変数TEST_CONTAINER_ID
とTOTAL_CI_CONTAINERS
が設定されています。これらの変数を使ってテストを分割します。
例えば、ECSタスクが3つあり、テストファイルが10件あるとします。
TEST_FILES=test1 test2 test3 test4 test5 test6 test7 test8 test9 test10 TOTAL_CI_CONTAINERS=3
テストは以下のように分割されます。
TEST_CONTAINER_ID=1 split_tests $TEST_FILES $TEST_CONTAINER_ID $TOTAL_CI_CONTAINERS # test1 test2 test3 test4
TEST_CONTAINER_ID=2 split_tests $TEST_FILES $TEST_CONTAINER_ID $TOTAL_CI_CONTAINERS # test5 test6 test7 test8
TEST_CONTAINER_ID=3 split_tests $TEST_FILES $TEST_CONTAINER_ID $TOTAL_CI_CONTAINERS # test9 test10
このように分割する際のデメリットは、実行時間が長いテストが同じECSタスクに割り当てられてしまう可能性があることです。このリスクを軽減するために、まず長時間実行されるE2Eテストを分割し、その後その他のテストを2段階で分割します。今後の改善点として、過去の実行時間に基づいてテストを効率的に分割する方法を検討しています。
テスト結果の保存とマージ
- 各コンテナがテスト実行を終了した後、結果はS3にJUnit XMLとして保存されます。
- すべてのECSタスクのテスト実行が完了したかどうかを確認するため、S3に保存されたテスト結果の数をカウントする
- テスト結果の数がコンテナ数と一致したら、CIの結果をマージする
- マージされたJUnit XMLファイルはDatadogに送信され、CI実行の分析やレビューが可能になる
終わりに
今回のCI改善のおかげで、CIの実行時間を50分から13分に短縮することができて、エンジニアの生産性が向上し、より早いフィードバックサイクルを実現することができました。今後も開発体験の改善を続けていきます。
オープンワークでは今後も開発体験の改善を続けていくので、興味のある方は採用サイトを覗いてみてください。