カミナシのソフトウェアエンジニアisanaです。 カミナシレポートの開発に携わっています。
私たちのチームでは、Webアプリケーションの品質担保のため、Playwrightを用いたブラウザテストを実装し、GitHub Actionsで実行しています。しかし、このCIプロセスにおいていくつかの課題がありました。
他方、ソフトウェア開発においては日々寄せられるVoCに対応したり、新機能の開発を行うなかで、負債や課題を上手くハンドリングしていく必要があります。
本稿では、CIプロセスにおける課題をコスパよく解決するための改善策と、その過程で遭遇した「ハマったポイント」について、具体的な設定例を交えながらご紹介します。
PlaywrightやGitHub Actionsを利用している開発者の方々にとって、少しでも参考になれば幸いです。
前提となる環境
本稿で紹介する事例は、以下の環境を前提としています。
- テストフレームワーク: Playwright v1.52.0
- CI/CDプラットフォーム: GitHub Actions
- パッケージマネージャー: pnpm
直面していた課題とその影響
冒頭でも触れましたが、私たちのチームが抱えていたCIの課題は主に以下の2点です。
タイムアウトするまで実行しつづけているjobがあった
何らかの不具合によってPlaywrightのテストがハングしたとき、CIのjobがfailせずにタイムアウトしていました。
GitHub Actionsのデフォルトタイムアウトは360分となっており、jobがハングしていることに気づかないとCIの待ち時間が発生するだけでなく、GitHub Actionsのリソースも無駄に消費してしまい、不要なコストを支払っていました。
CIの実行時間が長い(約15分)
テストが全て正常に完了する場合でも、全体の実行に15分程度かかっていました。
これは、プルリクエストを出してからフィードバックを得るまでの待ち時間としては長く、開発体験を損ねる一因となっていました。
改善にあたり重視したポイント
これらの課題を解決するにあたり、以下の2点を特に重視しました。
1. CIにおけるテスト実行時間の目標設定
やみくもに高速化を目指すのではなく、明確な目標を持つことが重要だと考えました。
そこで参考にしたのが、DevOps Research and Assessment (DORA) が提唱する指標です。
DORAによると、「自動テストの待ち時間は10分以内が望ましい」とされています。
The tests should not take more than a few minutes to run, with an upper limit of about 10 minutes according to DORA’s research . 出典: DORA | Capabilities: Continuous Integration
この「10分以内」という具体的な基準があったことで、速度改善のゴールが明確になり、取り組みやすくなりました。
2. クリティカルパスの改善
CIパイプラインは複数のjobで構成されており、並列実行も組み合わせて構築されていることが一般的です。
全体の待ち時間を短縮するためには、全てのjobを均等に少しずつ高速化するよりも、最も時間のかかっているjob(クリティカルパス)を特定し、集中的に改善する方が効果的です。
今回は並列実行しているブラウザテストの中で、最も実行に時間がかかっているクリティカパスに対して改善を行いました。
具体的に取り組んだ改善策
上記の課題と重視したポイントを踏まえ、以下の改善策を実施しました。
1. jobのタイムアウト設定
まず、テストがハングアップした際にCIリソースを無駄に消費し続けないように、jobに適切なタイムアウト値を設定しました。GitHub Actionsでは、ワークフローファイル内で timeout-minutes
を指定することで、jobの最大実行時間を制御できます。
# .github/workflows/ci.yml jobs: playwright-tests: runs-on: ubuntu-latest timeout-minutes: 20 # 例えば20分でタイムアウトさせる steps: # ... テスト実行ステップ ...
これにより、万が一テストがハングしても、設定した時間で自動的にjobが停止し、早期に問題を検知できるようになりました。
2. キャッシュの活用
一般論として、CI/CDの最適化にあたりまず検討するべきポイントはキャッシュの利用です。
a. node_modules
のキャッシュ
node.jsのセットアップに利用する定番アクションであるsetup-node
はnode_modulesのcacheを隠蔽してくれるのでそれを利用します。
下記のようにwith.cache
にpnpm
を指定するだけでOKです。
# .github/workflows/ci.yml (一部抜粋) jobs: playwright-test: runs-on: ubuntu-latest timeout-minutes: 60 steps: - uses: actions/checkout@v4.2.2 - name: Setup pnpm uses: pnpm/action-setup@v4.1.0 with: version: 10 - uses: actions/setup-node@v4.4.0 with: node-version-file: 'package.json' cache: 'pnpm'
pnpm install
の実行時間は、全体の実行時間に対して微々たるものですが、上述の通り極めて簡単に利用できるため、設定しておいて損はないです。
b. Playwrightが依存しているブラウザのキャッシュ
Playwrightはテスト実行時に対応するブラウザ(Chromium, Firefox, WebKitなど)をダウンロード・インストールします。これもキャッシュすることでjobの実行時間を短縮できます。
ここで、キャッシュの話に入る前に、Playwrightの依存関係についておさらいします。
Playwrightには、Playwright自身の実行に必要な依存ライブラリ
とテストの実行に利用する依存ブラウザ
があります。
依存ライブラリはパッケージマネージャを利用して所定のパスにインストールされます。
一方、依存ブラウザは~/.cache/ms-playwright
にインストールされます。
Managing browser binaries | Playwright
このディレクトリをキャッシュすることで、依存ブラウザをインストールするstepをスキップすることができます。
依存ブラウザをキャッシュしつつ、依存ライブラリをインストールするアプローチとして、本稿では以下2つのコマンドを組み合せるアプローチをご紹介します。
playwright install --with-deps
:Playwrightの依存ライブラリと依存ブラウザをインストールするplaywright install install-deps
: Playwrightの依存ライブラリのみインストールする
このアプローチでは下記のように、依存ブラウザがキャッシュヒットしたときは依存ライブラリのみをインストールし、依存ブラウザがcacheヒットしなかったときは依存ライブラリと依存ブラウザの両方をインストールするようになっています。
jobs: playwright-test: runs-on: ubuntu-latest timeout-minutes: 20 steps: # ....node, pnpm, node_modulesのsetup.... - name: Get installed Playwright version # playwrightのバージョンを控える id: playwright-version run: echo "version=$(pnpm exec playwright --version | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+')" >> $GITHUB_OUTPUT - name: Cache Playwright Browsers # Playwrightのバージョンをkeyに依存ブラウザをcache uses: actions/cache@v4.2.3 id: playwright-cache with: path: ~/.cache/ms-playwright key: '${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}' restore-keys: ${{ runner.os }}-playwright- - name: Install Playwright Browsers # cacheがない時は依存ブラウザと依存ライブラリを両方インストール if: steps.playwright-cache.outputs.cache-hit != 'true' run: pnpm exec playwright install --with-deps - name: Install Playwright system dependencies # cacheがある時は依存ライブラリのみインストール if: steps.playwright-cache.outputs.cache-hit == 'true' run: pnpm exec playwright install-deps # ....テスト実行....
4. クリティカルパスにのみlarge runnerを利用
今回改善を行ったプロジェクトでは、Desktop Chrome、Mobile Chrome (Android)、Mobile Safari (iOS) の3つのブラウザ環境に対するテストを並列実行していました。
この中でも特にMobile Safari でのテストが最も時間がかかっており、全体のクリティカルパスとなっていました。
キャッシュを活用しただけではMobile Safariでのテストにおいて、目標の10分を切ることが出来なかったため、更なる改善が必要でした。
ここで、Reactのレンダリングを最適化することでブラウザテストを高速化するアプローチを検討しました。
しかし、今回の対応のスコープ内でレンダリングのパフォーマンス改善を実施するのは難易度が高いと判断し諦めることにしました。
そこで、Mobile Safariのテストのみ、標準のランナーよりも高性能な GitHub-hosted large runner (Ubuntu 8-core) を利用することにしました。
About larger runners - GitHub Docs
# .github/workflows/ci.yml (一部抜粋) jobs: test-mobile-safari: runs-on: ubuntu-8-core # mobile safariのテストのみlarge runnerを指定 timeout-minutes: 20 steps: # ... WebKit用のテスト実行ステップ ... test-chrome: runs-on: ubuntu-latest # Chromeは標準ランナー timeout-minutes: 20 steps: # ... Chrome用のテスト実行ステップ ...
他のブラウザテストは標準ランナーのままとし、クリティカルパスとなっている部分に限定して高性能なリソースを割り当てることで、コストを抑えつつ全体の実行時間短縮を図りました。
尚、GitHub-hosted large runnerを利用する際は、上記のようなjobの設定に加えて、所属するorganizationへの登録が必要になります。
Managing larger runners - GitHub Docs
ハマったポイントとその解決策
large runnerが起動せず、jobがスタートしない
large runnerをorganizationに追加し、上記のように設定したにもかかわらず、該当のjob (test-mobile-safari
) がキューに入ったまま実行されない状態に陥りました。
Requested labels: ubuntu-8-core Job defined at: xxxxxxxx Waiting for a runner to pick up this job...
原因を調査したところ、organizationに登録したGitHub-hosted runnerの設定項目Maximum Job Concurrencyの値が「1」に設定されていたためでした。
これは、organizationに登録したrunnerを同時に実行できるjob数の設定です。
あるブランチのjobで登録したrunnerを実行しているとき、別のブランチで同じrunnerを指定したjobは全て待機状態になっていました。
Maximum Job Concurrency
の値を、最大値である1000に変更したことでこの問題を解決することができました。
この設定項目については、GitHubの以下のドキュメントで解説されています。
Managing larger runners - Configuring autoscaling for larger runners (公式ドキュメント)
もしlarge runnerの利用で同様の問題に直面した場合は、この設定を確認してみてください。
まとめ
本稿では、PlaywrightとGitHub Actionsを用いたブラウザテストのCIにおいて、サクッとできる改修で待ち時間を10分に収めるようにした事例をご紹介しました。
- jobのタイムアウト設定をすることでjobのタイムアウトによる無駄なリソース消費を抑えることが出来た
node_modules
とPlaywrightブラウザ
のキャッシュを利用し、large runnerをクリティカルパスに投入するすることでブラウザテストの待ち時間を約15分から7~9分以内に短縮できた
Playwrightによるブラウザテストを根本的に早くするための作業時間を確保するのは難しいですが、タイムアウト、キャッシュ、スケールアップをクリティカルパスに対してうまく活用することでコスパ良く改善することができます。
ちょっとした改善でも開発体験を良くすることができるので、こういった積み重ねを継続していきたいです。
PlaywrightやGitHub Actionsを利用してCIの課題に直面している方にとって、本稿が少しでもお役に立てれば幸いです。
最後に宣伝です📣
カミナシではソフトウェアエンジニアを募集しています。
プロダクト開発の傍ら、生産性の改善や負債の返済にも妥協なく取り組みたい方、是非一緒に働きましょう!