チームが抱えていた CI テストに関する課題
最近、GitHub Actions で実行しているテスト、とりわけ Scala のプロダクトコードのテストに関連する様々な問題が原因で、チームの生産性が下がっているほか、Scala のコードを改修することへ抵抗感を覚えてしまっているのでは、と思うようになった。ここで敢えて Scala というワードを出したが、Scala の CI 周りに関するエコシステムに一定の不便さを感じるものの、それ自体が原因というよりは自分たちのソースコードの現状の運用に起因するところが多い。
具体的には以下のような問題をチームで認識していた:
- コンパイルやテストの実行が遅い
- どのテストケースで落ちたか分かりづらい
- lint で落ちたのか単体テストで落ちたのかも分かりづらい
- テストの workflow が Docker のマルチステージビルドを濫用して作られている(⭐︎)
- compile warning が大量に出ており、自分たちが新たに warning を増やしても気付きづらい
⭐︎ について、一般的な課題ではないと思うので補足する。
テストが Docker コンテナ上で実行されていたのだが、docker run
時ではなく docker build
時に行われるような仕組みになっていた。具体的には、Dockerfile にテスト実行用のためのステージがあり、build 時にテストレポートが生成される stage をターゲットに指定すると、その stage を build するための依存が処理される過程でテストが実行されるという仕掛けだった。この方式には
- Dockerfile に大量の stage が記述されており、build 時の流れを追いづらい
- ログが docker の出力に wrap されているので、テストの実行結果などのログが読み辛い
- 途中で失敗した時にコンテナに入って debug することも不可能
- GitHub Actions のエコシステムとの食い合わせが良くない
などの問題があった。そもそも、単体テストの粒度ではコンテナ上で動かす理由もなく、基本的には GitHub Actions のランナー上で直接 sbt を立ち上げれば良いはずだ。
これらの問題が改善されない状態が長らく続いていたのだが、転機となる事件が起こった。
きっかけ: Temurin のインストールに確率的に失敗するように
前述の docker build でテストを動かす仕組みの中で、JDK distribution の Temurin を apt-get install
していた。最近(この記事の執筆 2023-10-25 当時)、install 時に Temurin のリポジトリがしばしば 403 を返してきてテストの実行に失敗するようになってしまった。
この結果、CI が通らずにマージできない PR が溜まっていくようになった。また、SRE によるデプロイフローの改善によりほぼ毎日リリースすることができていたところが、テストの失敗によりその後のワークフローが動かずスムーズにリリースできない状態になってしまった。
この状態はマズいと思ったので、CI に一家言があり、これまでも workflow 設計などでチームに技術的な提案をしてきた自分が直してみることにした。
GitHub Actions Runner に元から入っている java にパスを通して使う(i.e. actions/setup-java
を利用する)ようにすれば、毎回 apt リポジトリからダウンロードする必要がなく、結果としてテストの実行が安定すると考えた。これを実現するためには docker build を用いてテストしている仕組みから、GitHub Actions 上で直接 sbt を実行する仕組みに移行しなければならない。
Dockerfile を食わせると stage 間の依存関係が描画されるツール dockerfilegraph を用いて現状のワークフローを把握し、テストに関連する stage を徐々に剥がして GitHub Actions のワークフローに載せていった。
そうした結果、docker build でテストを行う仕組みは無事移行された。GitHub Actions のエコシステムの恩恵にあずかることができ、Temurin の apt-get install のエラーによるテストの不安定さが解消された。
その後の改善
Dockerfile のしがらみから解放されたので、他の改善もずいぶんやりやすくなった。これまでに以下の改善を行うことができた:
- lint や format のチェックを行う workflow と、テストを実行する workflow を分け、何が原因で CI が通らないか分かりやすいようにする
- 落ちたテストケースの一覧を大量のログから漁らなくていいように Summary に出す
- Scala 2.13 の
@nowarn
アノテーションを使い、どうしても対応しようがない warning を無視する- 具体的には、次のバージョンで deprecated が撤回される予定のライブラリメソッドを利用している箇所で無視したかった
- compile 時の warning を、GItHub Actions のアノテーションとしてソースコードに紐づけて表示する
この辺りの細かい実践については、また別の機会に紹介できればと思う。
テストやコンパイルの実行に時間がかかる問題や、JUnit Report からアノテーションを作る時になぜか .class ファイルのパスになってしまい diff に表示されない問題などは残っているが、今後も空き時間に継続して取り組んでいく。
まとめ
安定して動き、何が原因で落ちたかが分かりやすい自動テストの実現により、無駄なことに時間を取られず、アプリケーションコードの変更を怖がらない環境に一歩近づけたのではないかと思う。
デリバリーのサイクルの中の課題を見つけ、エンジニアリングで解決まで持っていき、最終的にプロダクトの成長に貢献することができたら、それはエンジニアとして本望だなあと実感した。