freeeの開発情報ポータルサイト

高速でスケーラブルなE2E実行基盤を目指して

こんにちは。SEQ (Software Engineering in Quality)のzakiです。

これまで、freeeのE2Eテストは、Selenium、RSpec、Capybara、およびSitePrismを基盤とするRubyのテストを、Jenkinsを用いて実行していました。この構成にはいくつか課題があったため、現在は PlaywrightをベースにしたTypeScriptのテストを、GitHub Actionsで実行する新構成への移行を進めています。今回はその内、JenkinsからGitHub Actionsへの実行基盤の移行について紹介します。

従来のE2E実行基盤の構成とその課題

現在のE2E実行基盤図 JenkinsでSeleniumのテストを実行
現在のE2E実行基盤図 (Jenkins)

これまでのE2E実行基盤はEC2上にJenkinsを構築し、その中でE2Eテストを実行していました。しかし、この構成には以下の課題がありました。

  • テスト実行の並列数をスケーリングさせづらい
    • 並列数を増やす場合は、EC2インスタンスを追加して色々設定を手動で行う必要がありました。
  • 新規シナリオを作成したら、専用のテストジョブをJenkinsに作る必要がある
  • EC2は常時起動しているため、テストを実行していない間も費用がかかる
  • 別ブランチのテスト実行が簡単にできない
    • EC2上にE2Eレポジトリをcloneして、Jenkinsジョブをビルドしてテストを実行しています。そのため、別ブランチでテスト実行したい場合は、EC2の中身を別ブランチに書き換える必要がありました。そして、別ブランチで実行してる間は、mainブランチでテストを実行できませんでした。

また、定期的にJenkinsをアップグレードする必要がある、歴史的事情でjobの設定をIaC化していないので、設定を変更する場合は、レビューなしでUIから手動で行う必要がある、などの課題もありました。

新しいE2E実行基盤の構成

新基盤では、Jenkinsではなく、GitHub ActionsでE2Eテストを実行します。テスト環境のセキュリティ要件を考慮して、GitHub-Hosted runnersではなく、self-hosted runnersを使用しています。 そして、GitHub Actionsで実行したログやスクショのデータはすべて、ReportPortalという分析基盤に送られています。

新しいE2E実行基盤 GitHub ActionsでPlaywrightのテストを実行
新しいE2E実行基盤 (Playwright)

workflowファイルの中身は基本的に Playwrightの公式documentと一緒です。しかしながら、freeeのE2Eテストは数が多く、特にfreee会計やfreee人事労務のE2Eテスト数は、100を超えます。この巨大なE2EテストをGitHub Actionsで捌き切るために、いくつか工夫した点があるので、何点か紹介します。

E2E用にカスタマイズしたrunnerを作成

大量のE2Eテストを短時間で捌き切るには、並列実行が不可欠です。Playwrightでは、workersオプションを指定することで、テストを並列実行することができます。 社内の既存のself-hosted runnersはCPUのコア数が高めのrunnerは存在せず、大量のE2Eを並列実行するには不安がありました。そこで、SREチームに相談して、E2E専用のCPUコアの、コア数高めのself-hosted runnerを作成しました。(正確にいうと最低8コアなので、本当はもう少しCPUを使えています)

作成した専用runnerでworker数を変えてE2Eを実行してCPU使用率を観察した結果、現在はworker数12で実行しています。下図はworker数12でE2Eを実行した時のCPU使用率の遷移図です。大体80%付近で遷移していて、CPUをフル活用できていることが分かります。

worker数12でE2Eを実行した時のCPU使用率 80%付近で遷移している
worker数12でE2Eを実行した時のCPU使用率

柔軟にrunnerと並列数を切り替える

freee会計やfreee人事労務の大量のE2Eを捌き切るにはCPUコア数高めの専用runnerが必要でしたが、逆にテスト数が少ない小規模サービスのE2Eや、テスト1つだけの動作確認をする場合には、明らかにこのrunnerはオーバースペックです。 高性能な専用runnerは、度重なる起動によりインフラストラクチャのコストも高くなります。そこで、実行するテストやテスト環境によって、動的に実行するrunnerを切り替えられるように実装を工夫しました。

具体的には、E2Eを実行するjobの前に、inputとして受け取った実行するテストの種類や実行環境によって、適切なrunnerや並列数を決定するためのselect-runnerというjobを追加しました。select-runner jobで決定した、適切なrunnerと並列数をE2E実行するjobにoutputとして渡すことで、動的にrunnerと並列数を切り替えることができます。

E2Eを実行するGitHub Actionsのworkflowファイル

jobs:
  select-runner:
    runs-on: ubuntu-latest
    outputs:
      worker: ${{ steps.run-select-runner.outputs.worker }}
      runner: ${{ steps.run-select-runner.outputs.runner }}
      matrix: ${{ steps.run-select-runner.outputs.matrix }}
    steps:
    - name: Run select_runner script
      id: run-select-runner
      env:
        ENVIRONMENT: ${{ github.event.inputs.environment }} # テストを実行する環境のこと。freeeにはリリース前に動作確認を行うためのテスト環境や、機能開発を行うためのテスト環境など、様々な環境が存在します
      run: |
        output=$(node select_runner.js ${{ github.event.inputs.spec_file }})
        echo "worker=$(echo $output | jq -r '.worker')" >> $GITHUB_OUTPUT
        echo "runner=$(echo $output | jq -r '.runner')" >> $GITHUB_OUTPUT
        sharding=$(echo $output | jq -r '.sharding')
        matrix=()
        for ((i=1; i<=sharding; i++)); do
          matrix+=($i)
        done
        # 配列をカンマ区切りで表示
        echo "matrix=[$(IFS=,; echo "${matrix[*]}")]" >> $GITHUB_OUTPUT


  run-e2e:
    needs: select-runner
    runs-on: ${{ needs.select-runner.outputs.runner }}
    strategy:
      fail-fast: false
      matrix:
        shard: ${{ fromJSON(needs.select-runner.outputs.matrix) }}

 .. 以下E2Eを実行するstep

そして、適切なrunnerと並列数を決定するために、各サービスごとに、シナリオ数を考慮してworker数とsharding数、実行するrunner名を記載したファイルを用意します。例えば、freee会計のE2Eテストを一斉実行する場合は、worker数12 × sharding数4 = 48並列で、E2E専用runnerで実行されるような設定にしました。

ci.config.json

[
  {
    "service": "freee会計",
    "worker": 12,
    "sharding": 4,
    "runner": "e2e用の専用runner名"
  },
  {
    "service": "freee人事労務",
    "worker": 12,
    "sharding": 3,
    "runner": "e2e用の専用runner名"
  },
  {
    "service": "小規模マイクロサービスA",
    "worker": 6,
    "sharding": 1,
    "runner": "低スペックのrunner名"
  }
]

最後に実行するテストや実行環境を元に、実行するrunnerと並列数を決定するためのスクリプトを配置します。

select_runner.js

function selectRunner() {
  // 以下のロジックで、どのrunnerでテストを実行するかを決定する
  // 1. 1テストのみの実行の場合、低スペックrunnerを選択する
  // 2. リソースが潤沢でないテスト環境の場合、並列数を絞って実行する
  // 3. サービスのテスト一斉実行の場合、ci.config.jsonに設定されたサービスに対応するrunnerと並列数を選択する
  // 4. 機能毎のテストを実行する場合、対象のserviceを基にshardingを計算する
}

最後の機能毎のテストについても、軽く説明します。freee会計の中には、取引や経費精算、ファイルボックスなど様々な機能が存在します。そして影響範囲が明らかな場合は、取引に関係するテストのみを実行したい、という需要が存在しました。そのような場合は npx playwright test tests/会計/取引の様にテストを実行しています。その場合のsharding数は、(会計のsharding数 = 4) * (取引のテストファイル数) / (会計のテストファイル数)と決定しました。

E2Eレポジトリのディレクトリ構造

tests
├─ 会計
    ├── 取引
    ├── ファイルボックス
    └── 経費精算

このような工夫で、動的に実行環境を切り替えることができます。特に、E2E利用者から見ると、特に設定しなくても勝手に適切な実行環境を選んでくれるのが、「売り」です。

新基盤に移行した成果

GitHub Actionsの新基盤に移行した結果、旧基盤の課題をいくつか解決することができました。特にスケールが簡単になったこと、別ブランチのテスト実行が簡単になったことの恩恵を感じています。

  • テスト実行の並列数をスケーリングさせづらい -> スケールが非常に簡単になった
    • テストの並列数を上げたい場合は、ci.config.jsonの数字を増やしてmergeすれば、後は勝手にGitHub Actionsが必要な分のインフラリソースを立ち上げてくれるようになりました
  • 新規シナリオを作成したら、専用のテストジョブをJenkinsに作る必要がある -> E2Eレポジトリにmergeすれば勝手にGitHub Actionsで実行してくれるようになりました
  • テストが動いていない間もお金がかかっている -> 必要な時に必要な分だけインフラリソースが立ち上がる様になり、お金が掛からなくなりました
  • 別ブランチのテスト実行が簡単にできない -> 簡単にできるようになった

おわりに

SEQチームでは、現在新しいテスト基盤への移行をガンガン進めています。移行に関するブログは他にもあるので、ご興味ある方はぜひ覗いてみてください。

それでは、よい品質を〜