ファインディ株式会社でフロントエンドのリードをしている新福(@puku0x)です。
皆さん、GitHub ActionsのLarger runnerはご存知でしょうか?
高性能なマシンを使ってCIを実行できる一方、変更の少ない場合や計算負荷の低いCIではコストパフォーマンスが悪くなってしまいがちですよね?🤷♂️
この記事では、Nxの機能を利用してLarger runnerを動的に切り替える方法をご紹介します。
Nxについては以前の記事で紹介しておりますので、気になる方は是非ご覧ください。
Larger runner(より大きなランナー)
Larger runnerは、「GitHub Teamプラン」または「GitHub Enterprise Cloudプラン」の場合に利用可能です。
プライベートリポジトリ用の通常のランナー(GitHub-hosted runner)は、Linuxマシンの場合、CPUは2コアとなりますが、Larger runnerでは4コアや8コア、16コアなどより高いスペックのマシンを選択できます。
最近はArmベースのCPUも利用できるようになり、x64ベースのCPUを使う場合よりもコストを抑えられるようになりました。積極的に使っていきたいですね。
課題
Larger runnerは強力ですが、その分コストがかかります。
コードの変更が少ない場合や、CI全体が数分で終わってしまうような場合では、せっかくの高いスペックも宝の持ち腐れとなってしまうでしょう。
負荷が高い時だけLarger runnerのスペックを上げるにはどうすれば良いか? というのが今回の課題となります。
現在のGitHub Actionsには、変更の規模や負荷に応じて動的にランナーを切り替える機能が標準で備わっていないため、自分で組まなくてはいけません。
解決策
単純に変更されたファイル数や行数をカウントしても実際の影響範囲とは乖離があるため、より高度な制御が必要となります。
✨そこで、Nxの登場です。✨
Nxはモノレポ内のプロジェクトの依存関係を Project Graph として保持しており、コードの変更から影響範囲を割り出すことが可能です。
次のコマンドを実行すると、影響されるプロジェクトをJSON形式で取得できます。
npx nx show projects --affected --json
あとは実行結果をパースしてLarger runner切り替えの条件を組めば実現できそうです。
ワークフローの例を示します。
on: pull_request: jobs: check: runs-on: ubuntu-latest timeout-minutes: 5 outputs: runs_on: ${{ steps.output.outputs.runs_on }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: 20 - uses: nrwl/nx-set-shas@v4 with: main-branch-name: ${{ github.base_ref }} - run: npm ci - name: Get affected projects id: get_affected_projects # 1. 影響されるプロジェクト数を算出 run: | length=$(npx nx show projects --affected --json | jq '. | length') echo "length=$length" >> "$GITHUB_OUTPUT" - name: Output id: output # 2. 影響されるプロジェクト数に応じたLarger runner名をセット run: | if [ ${{ steps.get_affected_projects.outputs.length }} -gt 20 ]; then echo "runs_on=arm64-4-core-ubuntu-22.04" >> $GITHUB_OUTPUT else echo "runs_on=arm64-2-core-ubuntu-22.04" >> $GITHUB_OUTPUT fi build: needs: check # 3. 指定されたLarger runnerで実行 runs-on: ${{ needs.check.outputs.runs_on }} timeout-minutes: 30 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: 20 - uses: nrwl/nx-set-shas@v4 with: main-branch-name: ${{ github.base_ref }} - run: npm ci - run: npx nx affected --target=build
※actions/checkout
や actions/cache
は適宜最適化しましょう(1分ほど速くなる余地あり)
ワークフローを実行すると、まず影響されるプロジェクト数が算出されます。
後続のジョブでは、その結果を元に runs-on: ${{ needs.check.outputs.runs_on }}
で利用するLarger runnerを設定します。
やりたかったことが実現できていますね!🎉
負荷の高い時は高スペックのLarger runnerが動くため、CI時間の短縮が見込めます。負荷が低い時は、Larger runnerのスペックを落としてコストを節約できます。
結果
直近100回のCI結果を元に、4コアマシン固定の場合と2〜4コア可変の場合でコストを計算してみました。
Before(4コア固定) | After(2〜4コア可変) |
---|---|
$6.53 | $4.79 |
※ArmベースのCPUを使用する想定でコストを算出しています
※直近100回中、約1割が高負荷なCI(約12分)、残りが低負荷なCI(約5分)でした
Larger runnerを動的に切り替える方法を採用することにより、コストを3割ほど削減できました。
以前の状態と比較してコストパフォーマンスが改善されたと思います。
まとめ
この記事では、Nxの機能を活用してLarger runnerを動的に設定することで、コストパフォーマンスを改善する方法を紹介しました。
今回は runs-on
の切り替えのみ紹介しましたが、他にもNx CLIの --parallel
オプションの動的設定など応用は様々です。
皆さんの参考となりましたら幸いです。
ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。