STORES 予約 でwebアプリケーションエンジニアをやっております。ykpythemindです。
GitHub Actions、とても便利ですよね。STORES 予約チームでは徐々にCircleCI から GitHub Actionsへの移行を進めていますが、この度歴史あるRailsのリポジトリのCIを移行したので知見を公開します。
概要
- RSpecを実行する
- CIの実行速度のチューニング(CircleCIと同等の速度にしたい)
- node_modulesなどのインストール結果をキャッシュする
- テストを並列実行する
大きな方針として、CircleCI等の他サービスからの乗り換えの場合、同等のCI速度/課金額でないと移行は現実的でないと思いますので、速度面のチューニングも意識しています。
ほぼそのままの設定を貼ります
一部プロジェクト固有のstep等があり注釈コメントをつけています。適宜調整が必要です。
GitHub Action + Rails test example
各ハマりポイントは以下の通りです。
ポイント1. servicesのエントリーポイントを上書きすることができない
servicesとしてMySQLコンテナを立てていますが、ここで任意の引数を渡してコンテナを起動できません。 したがって、step内でsql_modeの調整をしています。
参照: jobs.<job_id>.services.<service_id>.options optionsはdocker createの引数になりそうなので、なんとか渡せそうなのですが、結論としてはできません。
- name: set MySQL sql_mode run: | mysql --ssl-mode=DISABLE --protocol=tcp --host 127.0.0.1 --user=root --password=${DB_PASSWORD} mysql <<SQL SET GLOBAL sql_mode = 'NO_ENGINE_SUBSTITUTION'; SET GLOBAL character_set_server = 'utf8mb4'; SET GLOBAL collation_server = 'utf8mb4_general_ci'; SQL
(2021/05/24追記: volumesを用いて /etc/mysql/conf.d にマウントするという手段もありそうです(未検証) )
ポイント2. needs
を使って実行時間を節約する
backend-test: name: ${{ matrix.target }} ${{ matrix.index }} needs: prepare
needs を設定することで、 backend-test
job は prepare
jobの完了を待つようになります。prepare
job内で assets precompileやnpm installなどを実行しておくことにより、テストを並列実行するjobの実行時間の節約になります。
なお、billable timeはActionsタブから各run結果のページに行き閲覧できます
ポイント3. timeout-minutes
を設定する
こちらは良く記事で見る内容です。jobにタイムアウトを実行することで、jobが暴走しても課金額が安心です。
ポイント4. 並列実行で matrix
を使用する
env: CI_NUMBER_OF_NODES: 8 # NOTE: 並列に実行する数. ここを増やしたらmatrix.indexも増やすこと. strategy: fail-fast: false matrix: target: [backend] index: [0, 1, 2, 3, 4, 5, 6, 7] # NOTE: 要素数は CI_NUMBER_OF_NODES の個数分にすること
このようにしておくと、要素の数だけ複数jobが起動し、ステップで CI_NODE_INDEX: ${{ matrix.index }}
の形でindexを参照できます。fail-fastをfalseにしておくと、どれかのjobがコケても最後まで実行します。
ポイント5. ファイルのsplitterを実装する
CircleCIでは circleci tests split --split-by=timings
*3 を用いることができ、かんたんにテストを各jobに分配できます。また、テストの実行時間をよしなに記録しておいてくれるので、実行時間が特定のコンテナに偏ってしまうことも起きにくいです。
2021/05/20現在GitHub Actionsにはそのような機能は存在しないので、今回はテストファイルを分配するスクリプトを用意して対応しました。
GitHub Action + Rails test example ( splitter )
ファイルの行数が長いファイルはだいたい実行時間が長いと考えて重み付けしてしまっていいだろうという大雑把な仮定のもと、貪欲法で分割しています。 CI_NUMBER_OF_NODESとCI_NODE_INDEXを用いて分配されたファイル群を取り出すことができます。
TEST_FILES="$(ruby spec/spec_splitter.rb --glob='spec/**/*_spec.rb' --node-count=$CI_NUMBER_OF_NODES --node-index=$CI_NODE_INDEX)" bundle exec parallel_rspec -n $PARALLEL_TESTS_CONCURRENCY -- $TEST_FILES
すごく重いテストがあり調整をしていますが、各job間での実行時間差が1分程度に収まっているので一旦は良しとしています。
ベターな方法としてはテスト結果(実行時間のメタデータ)をartifactとして保存しておく方法 ( GitHub Actions でテストを並列に実行して高速化する (parallelism) ) や、knapsack を使う方法がありますが、前者はmainブランチの結果を取得するために少々ハック感があったので今回は見送っています。後々何らかの導入を考えています。
ポイント6. dependabotが作るPull Requestでsecretsが参照できない
2021年3月から以下のようにsecretsが参照できずにCIが落ちるという現象が起きるようになったようです。 GitHub Actionsは非常にセキュアな印象ですが、(買収したはずの)dependabotが不便になってしまったため、改善されることを期待しています。
simple-minds-think-alike.hatenablog.com
(2021/05/24追記: リンク先の記事にあるように pull_request_target
トリガーを使えば解決できますが、セキュリティ上の注意点はありそうです。 )
まとめ
GitHub ActionsはCircleCIとだいたいできることは同じはずなのですが、外部Actionの品質や拡張性、secrets機能などのGitHubとの親和性でかなり楽しく遊べる印象があります。
heyではGitHub Actionsを使い倒すエンジニアを募集中です。よろしくおねがいします。
*1: discourse/tests.yml at main · discourse/discourse · GitHub なお、discourse はすでにGitHub Actionsを使っており参考になります
*2:同僚の指摘で気づいた内容です。テスト自体の実行時間は変わりませんが、課金時間は半分ほどになりました