ファインディ株式会社でフロントエンドのリードをしている新福(@puku0x)です。
弊社では Nx を活用してCIを高速化しています。この記事では、最近導入した Nx Agents でフロントエンドのCIをさらに高速化した事例を紹介します。
Nxについては以前の記事で紹介しておりますので、気になる方は是非ご覧ください。
フロントエンドのCIの課題
これまで「キャッシュの活用」や「並列化」「マシンスペックの向上」といった工夫により、フロントエンドのCIを高速化してきました。
しかし、コードベースの増大により時間のかかるタスクにCI時間が引きずられてしまう問題が顕著になってきました。
次の図は、キャッシュヒットしなかった場合のCI時間の一例です。
他のタスクが早く終わっても一番時間のかかるタスクを待つ必要があるため、結果としてCI時間が伸びる傾向にありました。
Nx Agents
タスク単位の並列化では、CI時間のボトルネックを解消するのが困難です。
「DTE(Distribute Task Execution)」はその問題を解決する手法であり、Nx CloudにはDTEのマネージドなサービスである「Nx Agents」が提供されています。
Nx Agentsは今年2月にリリースされました。費用はGitHub Actionsより安価となっています。
Nx Agents | GitHub Actions(Larger runner) | |
---|---|---|
Linux 2コア | $0.0055/min | $0.008/min |
Linux 4コア | $0.011/min | $0.016/min |
参考:
使い方は非常に簡単で、NxワークスペースをNx Cloudに接続した後、CIのタスク実行前に次のようなコマンドを追加するだけです。
npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js"
利用するマシンの数やスペックは、変更の影響範囲に応じて動的に設定できます。
npx nx-cloud start-ci-run --distribute-on=".nx/workflows/dynamic-changesets.yml"
# .nx/workflows/dynamic-changesets.yml distribute-on: small-changeset: 3 linux-medium-js medium-changeset: 6 linux-medium-js large-changeset: 9 linux-large-js
DTE自体はGitHub Actionsの
matrix
の機能を用いて構成することも可能ですが、CIが失敗した場合は 必ずDTEホスト側のマシンをRe-runする必要があります。検証した限りでは誤操作を確実に防ぐ方法が無かったため、Nx Agentsの利用をおすすめします。
Nx Agents導入の結果
Nx Agentsを有効にした場合のCI時間の例を示します。
このワークフローでは、アプリケーションのビルドやテスト、型チェックなどを全てNx Agentsで実行する構成となっています。
npx nx-cloud start-ci-run --distribute-on=".nx/workflows/dynamic-changesets.yml" npx nx affected --target=build,build-storybook,test,lint,stylelint,typecheck --configuration=ci npx nx-cloud complete-ci-run
元の構成では約18分かかっていたものが約11分になりました。
CI本体が約10分、後述する事前処理が追加で約1分かかる計算です。
CI時間は約7分削減されており、およそ 40% ほど高速化できたということがわかりました。
各タスクが複数のエージェントで分散実行されたことで、タスク単位で並列化していた場合よりも高速化できました。
Nx Agents利用上の工夫
Nx Agentsは今年リリースされたばかりのサービスであるため、ドキュメントの不備やノウハウの不足といった課題があることに注意しましょう。ドキュメントに示されているセットアップの例は、最低限のものしかないため最適化の余地があります。
ここでは、弊社で実践している利用上の工夫を紹介します。
プロジェクトを細かく分割する
DTEの性質上、単体のタスクの実行時間が長くなるほどエージェントの利用効率が下がります。
コードを全て apps/**
側に置くとキャッシュヒット率が下がりCI時間も延びるため、まずは libs/**
に分離することから始めると良いと思います。
共有ライブラリについては、コンポーネント系、ユーティリティ系で別のライブラリとして作ると良いでしょう。
弊社の場合、例えばビルド時間が 2分 を超えるようなものを確認した場合は、モジュールの移動や分割を実施しました。
分割が難しい場合は、Nx Agentsの実行対象から外すといった工夫が必要かもしれません。
Node.jsのバージョンを揃える
nx-cloud-workflows の workflow-steps/install-node は nvm に対応しています。nvm以外の管理ツールを利用している場合は自前でセットアップ用のスクリプトを書く必要があります。
例として asdf を用いる場合のスクリプトを示します。
# .tool-versions nodejs 20.18.0
# .nx/workflows/agents.yml launch-templates: custom-linux-medium-js: resource-class: 'docker_linux_amd64/medium' image: 'ubuntu22.04-node20.11-v10' init-steps: - name: Checkout uses: 'nrwl/nx-cloud-workflows/v4/workflow-steps/checkout/main.yaml' - name: Setup Node.js script: | git clone https://github.com/asdf-vm/asdf.git $HOME/.asdf --branch v0.14.1 source $HOME/.asdf/asdf.sh asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git asdf install nodejs
CI時間は5〜10秒ほど延びますが、Node.jsのバージョン不一致によるエラーを回避できます。
キャッシュの活用
workflow-steps/cache を利用した依存ライブラリのキャッシュはほぼ必須と言えるでしょう。node_modules
をキャッシュすることで大幅な時間削減が可能です。
# .nx/workflows/agents.yml launch-templates: custom-linux-medium-js: (中略) - name: Restore NPM Cache uses: 'nrwl/nx-cloud-workflows/v4/workflow-steps/cache/main.yaml' inputs: key: '.tool-versions' paths: ~/.npm base_branch: '<デフォルトブランチ名>' - name: Restore Node Modules Cache uses: 'nrwl/nx-cloud-workflows/v4/workflow-steps/cache/main.yaml' inputs: key: '.tool-versions|package-lock.json|yarn.lock|pnpm-lock.yaml' paths: node_modules base_branch: '<デフォルトブランチ名>' - name: Restore Browser Binary Cache uses: 'nrwl/nx-cloud-workflows/v4/workflow-steps/cache/main.yaml' inputs: key: '.tool-versions|package-lock.json|yarn.lock|pnpm-lock.yaml|"browsers"' paths: | ~/.cache/ms-playwright base_branch: '<デフォルトブランチ名>'
ここで示した例では、以前の記事と同様に ~/.npm
をキャッシュすることで、node_modules
がキャッシュヒットしなかった場合でも可能な限り高速動作するようにしています。
workflow-steps/cache の使い勝手は actions/cache とほぼ同じです。デフォルトブランチ以外にキャッシュを共有する場合は、デフォルトブランチへのpush時にキャッシュを更新すると良いと思います。
# .github/workflows/update-cache.yml on: push: branches: - '<デフォルトブランチ名>' paths: - package-lock.json jobs: cache: runs-on: ubuntu-latest steps: (中略) - uses: actions/cache@v4 id: cache (中略) - run: npx nx-cloud start-ci-run --distribute-on="3 linux-small-js" - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' run: npm ci - run: npx nx-cloud complete-ci-run
設定可能な最小エージェント数は 3
である点に注意しましょう。
安く済ませたいところではありますが...
特定のステップの省略
2024年10月現在、Nx AgentsではGitHub Actionsのような条件分岐の構文はサポートされていません。適宜スクリプトを書いて対応しましょう。
# .nx/workflows/agents.yml launch-templates: custom-linux-medium-js: (中略) - name: Install Node Modules (if needed) script: | if [ ! -d node_modules ]; then npm ci fi
高度なエージェント割り当て
--distribute-on=".nx/workflows/dynamic-changesets.yml"
は記述が簡潔である一方、現状では small
medium
large
の3段階しか設定できません。また、負荷に応じてマシンスペックを上げるといった高度な割り当てもサポートされていません。
Nx Agentsの今後のアップデートに期待しても良いですが、待ちきれないという場合は次のように、メインジョブの前段で nx show projects --affected
を実行し、outputs
経由でオプションを渡すと良いでしょう。
jobs: check: runs-on: ubuntu-latest outputs: distribute_on: ${{ steps.output.outputs.distribute_on }} parallel: ${{ steps.output.outputs.parallel }} steps: (中略) - uses: nrwl/nx-set-shas@v4 with: main-branch-name: ${{ github.base_ref }} - name: Get affected projects id: get_affected_projects run: | length=$(npx nx show projects --affected --json | jq '. | length') echo "length=$length" >> $GITHUB_OUTPUT - name: Output id: output run: | if [ ${{ steps.get_affected_projects.outputs.length }} -gt 20 ]; then echo 'distribute_on="4 custom-linux-large-js"' >> $GITHUB_OUTPUT echo 'parallel=4' >> $GITHUB_OUTPUT elif [ ${{ steps.get_affected_projects.outputs.length }} -gt 10 ]; then echo 'distribute_on="3 custom-linux-medium-plus-js"' >> $GITHUB_OUTPUT echo 'parallel=3' >> $GITHUB_OUTPUT else echo 'distribute_on="3 custom-linux-medium-js"' >> $GITHUB_OUTPUT echo 'parallel=2' >> $GITHUB_OUTPUT fi main: needs: check runs-on: ubuntu-latest steps: (中略) - name: Setup Nx Cloud run: npx nx-cloud start-ci-run --distribute-on=${{ needs.check.outputs.distribute_on }} - run: npx nx affected --target=build,test,lint -parallel=${{ needs.check.outputs.parallel }}
この手法は dynamic-changesets.yml
による判定と nx affected
で検出された影響範囲に乖離がある場合にも有効です。
まとめ
この記事では、Nx Agentsの導入によりフロントエンドのCIを高速化した事例を紹介しました。
Nx Agentsの提供するDTEの仕組みは、タスク単位の並列化を超えた高速化が可能です。
一方で、プロジェクトの細分化が十分でない場合や、単一のタスクが非常に時間のかかる場合では期待する効果が得られないため、適材適所で利用するのが良いでしょう。
国内のNx Agents導入事例はまだ少ないと思われますが、検証中に得られた知見は今後も発信していきますので、是非お役立てください。私たちの取り組みが少しでも皆様の助けとなれば幸いです。
現在、ファインディでは一緒に働くメンバーを募集中です。
ご興味がある方は↓こちらからご応募ください。 herp.careers