🤖

GitHub Actions + Docker + JUnitでCI基盤構築

に公開

これは何?

CI基盤(自動テストの実行基盤)をGitHub Actions、Docker(Docker Compose)、JUnitを使って作成したので、その中での工夫点や今後についてを記載します。

※以前の記事で予告していた内容
https://zenn.dev/omiai_techblog/articles/d8e360a1ac8a17#💬-今後の記事で扱う予定の内容

なぜやったか

早く、より良いソフトウェアを開発をしていくためには、安定したCI/CDの基盤は必須です。
Omiaiでは、テストコードを書く文化自体は以前からありましたが、それをCIとして実行するような基盤がありませんでした。そのため、レビュー前やリリース前のデグレに気付きづらいという課題がありました。

その課題を解消するために、安定している、かつ高速なCI基盤を目指して対応を進めています。

技術選定について

当初の構想ではAWS上に自動テスト用のリソースを用意しインフラ構成を再現した状態でAPIテストを行うつもりでしたが、管理工数やコストの懸念から断念しました。

チーム開発でGitHubを利用しているため、GitHub ActionsをCIプラットフォームとして採用し、ローカル環境のために整備したエミュレータなども利用して軽量ですぐに壊せるDockerコンテナを使ってインフラ構成を擬似的に再現しています。
元々JUnitでテストコードが実装されていたため、そのままJUnitを採用しています。

何をやったか

自動テストを実行するワークフローをGitHub Actionsを使って作りました。今回作ったワークフローではAPIテスト(HTTP通信を伴う結合テスト)のみを実行します。

ワークフローの流れは以下です。

処理内容は各ジョブ名の通りですが、簡単に説明します。

  1. setup-source:
    • 複数のリポジトリからソースコード収集
    • ライブラリインストール
    • CI用に設定を上書き
  2. build-and-upload-images:
    • ソースを元にコンテナイメージをビルド
    • ビルドしたイメージをArtifactとしてアップロード
  3. setup-and-run-api-test:
    • Artifact(ソース、コンテナイメージ)の収集
    • Docker Composeでプロジェクト起動
    • JUnit実行
    • テスト結果のレポート作成
  4. delete-artifacts:
    • 各ステップで作成したArtifactを削除
  5. notify-slack-on-failure:
    • ワークフローが失敗した場合、Slack通知

このように特に変わったことはしてないので、この実行基盤を用意するにあたって工夫した点について記載していこうと思います。

工夫点

setup-resourceジョブ

処理内容の概要で少し触れましたが、この処理内ではモノレポ構成を採用してないので複数リポジトリからソースコードを収集します。
そのため、CI実行をトリガーしているリポジトリ以外のソースコードも取得してくる必要があります。

一般的なCIの構成では、単体テストなどを実行することが多いため1つのリポジトリで完結します。その場合は、ワークフローのコンテキスト(github.token)のトークンを利用すれば事足ります。

ただ、別リポジトリをいじる必要がある場合は適切な権限が割り振られたトークンが必要になります。よく記事に出てくるのはPATで対応するケースです。
個人開発ではそれで問題ないですが、チーム開発において個人用のアクセストークンを共有利用するのは管理上リスクがあります。

そのため今回はGitHub Appを作成し、そのGitHub Appで生成された一時トークンを利用する方法を取りました。

実際に設定する際には以下の記事を参考にさせてもらいました。
https://techblog.ap-com.co.jp/entry/2024/12/15/180000

build-and-upload-imagesジョブ

ジョブの全体像

OmiaiのAPIテスト用のDocker環境では、APIサーバー、DB、キャッシュサーバーなど一般的な構成から、外部サービスのエミュレータなど多くのコンテナを利用するため、ワークフロー内で直列実行していると時間がかかりすぎます。

そこでいい感じにコンテナイメージのビルドを並列実行するために、matrixComposite Actionを利用しました。詳細はそれぞれ以下のリンクを参照してください。
https://docs.github.com/ja/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow
https://docs.github.com/ja/actions/tutorials/creating-a-composite-action

愚直に並列実行の処理を書くなら単純に並列したジョブを定義すれば良いだけですが、それだとワークフローの記述が長くなり、全体が把握しづらくなります。ただ列挙するのではなく、Composite Actionで再利用可能なアクションを定義しつつ、matrixの記法を利用して記述量を少なく、けどやれることは多くという状態をつくりました。

Composite Actionについては以下の記事を参考にさせていただきました。
https://developer.mamezou-tech.com/blogs/2023/03/13/github-actions-using-composite-action-in-repo/

DockerコンテナのビルドTips

Dockerコンテナのビルド処理に関しては以下のアクションを組み合わせて利用しています。

ワークフローを高速化するためにキャッシュ周りの設定はこだわる必要があります。docker/build-push-actionでは、cache-fromcache-toにGitHub Actionsを指定する(type=gha)するとよしなにキャッシュを保存、書き出しを行ってくれます。

...
    - uses: docker/build-push-action@v6
      with:
        cache-from: type=gha,scope=hoge
        cache-to: type=gha,mode=max,scope=hoge
...

setup-and-run-api-testジョブ

このジョブではJUnitを実行するだけではありますが、何点か工夫しました。

gradleのキャッシュ活用

gradle/actions/setup-gradleアクションを利用しました。
基本的に特に設定なしでもある程度キャッシュを管理してくれますが、こだわりたい場合はREADMEを読みつつ設定が必要。Gradle Wrapperを利用する場合、gradle-versionの指定不要。

JUnitの結果をレポート、PRコメントに反映

JUnitの実行結果がPR上にまとまっていた方が何が失敗しているかが把握しやすくなるため、レポートは必要です。

レポートにはdorny/test-reporterを利用しました。
以下のような感じで何が成功した、何が失敗した、というのを分かりやすくまとめてくれます。

またActions Summaryへの直接の導線がPR上にはないため、辿りやすいようにPRコメントにも結果の概要と詳細レポートへのリンクを書くようにしました。(内容はテスト時のものです)

delete-artifactsジョブ

このステップはArtifactのストレージ上限を超過しないために、ワークフロー実行の最後に必ず実行するようにしているジョブです。また、万が一このジョブが失敗しても良いように、Artifactの保持期間(retention-days)は1日にしています。

まずスコープですが、他のジョブでアップロードしたArtifactを消してしまうとまずいので、今実行しているジョブで生成されたArtifactだけを対象にします。

このジョブではactions/github-scriptを利用していて、処理の流れは以下です。

  1. ジョブ実行IDに紐づくArtifact一覧取得APIを叩く
  2. Artifact一覧情報からArtifact IDを取得して、Artifact削除APIを叩く

notify-slack-on-failureジョブ

ワークフローが失敗した際にSlack通知を行うためのジョブです。

GitHubとSlackを連携していないため、自前でGitHubユーザーとSlackユーザー(Slack Member ID)を一致させる必要がありました。ただ愚直に列挙する形で実装するとチームメンバーの増減でワークフローの管理が煩雑になることは目に見えていました。

そこで、GitHubのユーザー名とSlack IDとのマッピングをyamlファイルで定義し、ワークフロー内でyqを使って動的にSlackメンション対象を調整できるようにしました。
yamlを採用した理由は、コメントが書ける、ワークフフローの定義と同じ構文で定義できるという2点で運用が楽だと判断したからです。

Slack通知処理は以下のSlack公式アクションを使いました。
https://github.com/slackapi/slack-github-action

ワークフロー全体での工夫

属人化の防止

チーム開発において、「この実装、この設定は〇〇さんしか分からない」という状況は避けるべきです。それはワークフローの定義においても言えます。例えば、main関数に全てを記述するように1つのジョブ内で全ての処理を記述したら、失敗の原因が追いづらくなったり、処理の追加、変更がしづらいです。

そのため、適切な粒度でジョブ、ステップを分けてあげる必要があります。基本的には、ソースコードと同じく単一責任の原則に従うと良いと考えています。

あるジョブ内で複数の処理をする必要がある場合は、目的の数分でステップに分割します。そうすることで読みやすく、保守のしやすいワークフロー定義を作ることができます。
また、意図があって敢えて特殊な処理をしている場合はその意図をコメントしました。

どれも当たり前のことですが、ワークフロー全体を通して上記のことを意識して実装しました。

今後について

以下については引き続き対応をしていく予定です。

  • 高速化
  • コスト最適化

そのため、適切なサイズのLarger Runner、Self-Hosted Runnerの利用やテストの並列実行なども今後進めていく予定です。

まとめ

  • CI/CDは安定していて、高速であることが求められる
    • CIを高速にするためにキャッシュや並列実行を活用できないかを模索する
    • CIを安定させるために、色々こだわって定義をする
  • チーム開発の共有リソースにおける属人化は避けるべき
  • 今後もCI/CD基盤の整備、改善に取り組んでいきます

おまけ

ubuntu-latestランナーではdocker-composeコマンド(v1)は使えない

docker composeコマンドを使おう。
https://github.com/actions/runner-images/issues/9557

ワークフローでスクリプトを実行したい場合、実行権限付与が必要

chmod +x 対象ファイル...の実行が必要。
https://docs.github.com/ja/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/adding-scripts-to-your-workflow

GitHub Actionsのデフォルトランナーのストレージには14GBの上限がある

https://docs.github.com/ja/actions/reference/github-hosted-runners-reference#プライベート-リポジトリの標準の-github-でホストされたランナー

当たり前だが、ストレージ使い切るとワークフローが落ちる。
https://github.com/actions/runner-images/issues/9494

GitHub Actionsでのジョブ間でDockerイメージの受け渡し方法

Artifactとして処理して、ダウンロードしたtarファイルをdocker loadする。
https://docs.docker.com/build/ci/github-actions/share-image-jobs/

actions/upload-artifactアクションではArtifactを1つにまとめてアップロードできる

結局使わず。
https://github.com/actions/upload-artifact/tree/main/merge

APIを使ってGitHubのコメントを最小化する

GitHub v4 APIでのみコメント非表示機能が提供されてるので、それを使う。
https://github.com/isaacs/github/issues/1480#issuecomment-523154721

使いづらい場合は、github-commentというCLIツールを使うと良いかも。
https://github.com/suzuki-shunsuke/github-comment

Omiai Tech Blog

Discussion