ファインディ株式会社でフロントエンドのリードをしております 新福(@puku0x)です。
弊社では、数年前に社内のCI環境をすべてGitHub Actionsに移行しました。
この記事では、弊社のGitHub Actions活用事例の内、CI高速化についてご紹介します。
なぜCI高速化に力を入れるのか
当ブログをはじめ弊社では、たびたびCI高速化の大切さについて言及しています。
- Findyの爆速開発を支えるテクニック - Findy Tech Blog
- RailsのCIのテスト実行時間を 10分から5分に高速化した話 - Findy Tech Blog
- Findy転職フロントエンドの開発生産性を向上させるためにやったこと - Findy Tech Blog
これはなぜでしょうか?
開発が進むにつれて、コードベースが肥大化し、CIの待ち時間が増えていくのは皆さんにも経験があると思います。
CIの待ち時間が長いとついレビューを放置してしまいがちです。 レビューが遅いとブランチの生存期間が伸び、コンフリクトの発生確率が上がります。 コンフリクトを解決しても、CIが遅い状態ではまた同じことの繰り返しとなるでしょう。
GitHubの調査では、開発者は多くの時間をCI待ちに費やしていると報告されています。
見方を変えると、CI高速化はコーディングの効率化と同程度のインパクトがあると言えます。
チームの開発生産性を支える基盤として、弊社はCI高速化に力を入れているのです。
CI高速化
キャッシュの活用
弊社では、actions/cache を使って、依存関係のインストールを省く工夫を取り入れています。
ここでは例として、フロントエンド系のリポジトリのワークフローを紹介します。
- uses: actions/setup-node@v4 id: setup_node with: node-version: 20 - uses: actions/cache@v4 id: cache with: path: node_modules key: ${{ runner.arch }}-${{ runner.os }}-node-${{ steps.setup_node.outputs.node-version }}-npm-${{ hashFiles('**/package-lock.json') }} - if: steps.cache.outputs.cache-hit != 'true' run: npm ci
フロントエンド周りのCIを組んだことのある方はピンと来たかと思います。
このワークフローでは、node_modules
ディレクトリをキャッシュしています。
npm公式では非推奨とされていますよね?
なぜこのような書き方でも大丈夫なのでしょうか?その秘密はキャッシュのキーにあります。
key: ${{ runner.arch }}-${{ runner.os }}-node-${{ steps.setup_node.outputs.node-version }}-npm-${{ hashFiles('**/package-lock.json') }}
キャッシュキーとして、OSやNode.jsのバージョン、パッケージマネージャーの種別などが細かく設定されています。
node_modules
ディレクトリのキャッシュが非推奨とされる理由は、異なる環境で実行されることによるファイルの不整合を防ぐためです。これに気を付けていればキャッシュしても良いのです。
※最近のGitHub ActionsではArmランナーが利用できるため、キャッシュキーにCPUアーキテクチャを追加するとより堅牢になるでしょう。
node_modules
がキャッシュヒットしなかった場合を考慮して、.npm
ディレクトリのキャッシュも含めると次のようになります。
- uses: actions/setup-node@v4 id: setup_node with: node-version: 20 - uses: actions/cache@v4 id: cache with: path: node_modules key: ${{ runner.arch }}-${{ runner.os }}-node-${{ steps.setup_node.outputs.node-version }}-npm-${{ hashFiles('**/package-lock.json') }} - uses: actions/cache@v4 if: steps.cache.outputs.cache-hit != 'true' with: path: | ~/.npm key: ${{ runner.arch }}-${{ runner.os }}-node-${{ steps.setup_node.outputs.node-version }}-npm-${{ hashFiles('**/package-lock.json') }} restore-keys: ${{ runner.arch }}-${{ runner.os }}-node-${{ steps.setup_node.outputs.node-version }}-npm- - if: steps.cache.outputs.cache-hit != 'true' run: npm ci
弊社の場合では、これでおよそ20秒〜30秒ほど高速化できました。
ジョブの並列化
バックエンドのテストを例に挙げます。このワークフローでは、matrix
機能を用いてテストを10並列で動作させるようにしました。
strategy: matrix: ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] steps: - name: Test env: CI_NODE_TOTAL: ${{ strategy.job-total }} CI_NODE_INDEX: ${{ matrix.ci_node_index }} run: | # get spec files order by filesize TEMP_FILE_PATH=$(mktemp) git ls-tree -r -t -l --full-name HEAD | grep '_spec.rb' | sort -n -k 4 | awk '{ print $5 }' | ./scripts/ci/rspec_split_files.sh > $TEMP_FILE_PATH # echo outputs echo "====SPEC FILES COUNT====" cat $TEMP_FILE_PATH | tr ' ' '\n' | wc -l echo "====SPEC FILES====" cat $TEMP_FILE_PATH | tr ' ' '\n' # run rspec bundle exec parallel_rspec -- --format progress -- $(cat $TEMP_FILE_PATH)
#!/bin/bash i=0 ret=() while read -r line do if [ $[i % $[CI_NODE_TOTAL]] = $[CI_NODE_INDEX] ] ; then ret+=($line) fi let i++ done echo ${ret[@]}
単純にテストを均等配分するのではなく、ファイルサイズでソートするという工夫が施されています。これは、テスト実行時間がファイルサイズに比例するという仮定に基づいています。
これらの工夫により、実行時間が従来の約半分になるまで高速化できました 🚀
詳細は↓こちらの記事をご覧ください。 tech.findy.co.jp
Larger Runners
基本的には並列化でCI高速化を目指しますが、難しい場合はLarger Runnersを使うのも手です。
(並列化の可否の判定は、テスト対象ファイルの分割を外部から制御可能かどうかで決めています)
Larger Runnersは、契約しているプランが「GitHub Teamプラン」または「GitHub Enterprise Cloudプラン」の場合に利用可能です。 最大で64コアまでスペックアップできるらしいです。いつか使ってみたいですね!
Armランナーについては先日GAとなったこともあり、積極的に移行を検討するようになりました。
3割程度のコスト削減ができることから、既に社内のいくつかのプロジェクトではLinux Armランナーに完全移行しています。
2024年9月現在では、ruby/setup-ruby などサードパーティの対応が進んでいないものもあります。移行の際は動作検証を十分にしておくと良いでしょう。
参考までに、弊社のフロントエンドでの高速化の例を示します。
Build | Test | |
---|---|---|
2コア | 15m8s | 12m7s |
4コア | 8m38s | 6m56s |
スペックアップするとコストは増えますが、その分CIの待ち時間の削減が期待できます。実質的な負担に大きな変化が無い場合は強気でスペックアップしていきましょう 💪
まとめ
いかがでしたでしょうか?
この記事では、弊社のGitHub Actions活用事例の内、CI高速化についてご紹介しました。
CIの待ち時間については、「継続的デリバリー」の書籍を参考に 10分以内 を目指すと良いでしょう。
実際に弊社では、これらの取り組みを行う前は1PRあたり15分〜20分ほどかかっていたCIが、10分以内の完了を目指して取り組んできた結果、平均5分程度まで高速化できた例もあります。
弊社にはCIの整備に関心の高いメンバーが多く在籍しております。勉強会等でお会いする機会がありましたらぜひお声がけください。
こちらの発表については、connpassのイベントページにアーカイブ動画へのリンクを載せております。 findy.connpass.com
次回へ続きます!👋
ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。