CircleCIからGithub Actionsへの移行
挨拶
ポートのSREを担当している @s.yanada です。
最近、弊社にてCI環境を CircleCI から Github Actions に移行したので、移行時の振り返りをしていきたいと思います。
移行理由
CircleCI から Github Actions へ移行した主な理由は以下の通りです。
- 価格
- Github Enterpriseの導入により Github Actions の利用枠が拡大
- セキュリティ
- Github のIP制限機能の有効化に、CircleCI の setup_remote_docker が対応していない
移行方法
当初移行方法には、最近 GA になった Github Actions Importer の利用を想定していました。
しかし、実際に活用してみるとGithub Actions Importer によるインポート後のワークフローの修正が多く発生したため1から自分で作り直すことにしました。
移行時のポイント
Github Actions へ移行の際に工夫した点などを紹介したいと思います。
CI実行環境のコンテナ化
CircleCI では CI に必要なサービスをコンテナ化していたため、Github Actionsでも jobs.<job_id>.services を利用して DB などのコンテナを作成する想定でした。
しかし、上記の手段で実行したコンテナに対して entrypoint に対する引数が指定できないという問題に遭遇し、actions の中で docker-compose によりコンテナを起動する手段を取りました。
通常、docker-compose で起動したコンテナは、 xxx_default
という名前のネットワークに属していますが、 jobs.<job_id>.conatiner
で起動されたコンテナは別のネットワークに存在するため疎通できません。
(以下のログからわかります)
/usr/bin/docker create --name 4d3ab51f1d02438b84c3e09bbc0ea56e_***pj_ci --label 6c0442 --workdir /__w/pj/pj --network github_network_9140afb921b34afda2ab7df33afab164 --network-alias pj_ci_network -v "/var/run/docker.sock":"/var/run/docker.sock" -v "/home/runner/work":"/__w" -v "/home/runner/runners/2.303.0/externals":"/__e":ro -v "/home/runner/work/_temp":"/__w/_temp" -v "/home/runner/work/_actions":"/__w/_actions" -v "/opt/hostedtoolcache":"/__t" -v "/home/runner/work/_temp/_github_home":"/github/home" -v "/home/runner/work/_temp/_github_workflow":"/github/workflow" --entrypoint "tail" ***/pj_ci:ruby2.7.4_chromedriver "-f" "/dev/null"
そこで、 docker-compose.yml
に network の設定を記述し、実際のネットワーク名は actions の中で都度書き換えてからコンテナ起動するようにしました。
version: "3"
services:
mysql:
image: mysql:8.0
container_name: "mysql"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --sql-mode=NO_ENGINE_SUBSTITUTION
networks:
default:
name: replace_me_with_github_actions_network_name # ここを yq で書き換える
external: true
steps:
- name: setup docker
run: |
export NETWORK_NAME=$(docker network ls --filter "name=^github_network_" --format "{{.Name}}")
yq -i '.networks.default.name = env(NETWORK_NAME)' .github/docker/docker-compose.yml
docker compose -f .github/docker/docker-compose.yml up -d
これで、command
指定と、コンテナ間通信の問題が解決されました。
余談ですが、 docker
コマンドは、jobs.<job_id>.conatiner
内で行われているため、一見 docker in docker になっていそうです。しかし、コンテナ起動時のログを見ると -v "/var/run/docker.sock":"/var/run/docker.sock"
のようにホスト側のソケットファイルがコンテナ側にマウントされているため、コンテナ内からホスト側のコンテナ操作が可能になっています。
以下の記事が参考になりました。
dind(docker-in-docker)とdood(docker-outside-of-docker)でコンテナを料理する
並列処理
CI ではテストの実行を行っているのですが、これを単体で行うとテストの規模によってはとてつもない時間がかかってしまいます。
そこで、github actions で用意されている jobs.<job_id>.strategy.matrix を基に並列処理をさせたいのですが、github actions には、CircleCI の circleci tests のようなテスト分割機能が組み込まれていません。
そこで、各ジョブで実行対象となるテストファイルの算出は独自で作成する必要がありました。
これに関してはスクリプトを掲載している記事がいくつか存在しそれを参考に作成しました。
-
https://rubyyagi.com/how-to-run-tests-in-parallel-in-github-actions/
- commit id を基にランダムにテストを分割
-
https://tech.buysell-technologies.com/entry/adventcalendar2021-12-05
- 重いテストを専用のジョブに切り出すことで、各ジョブのテスト実行時間をより均等化
また、デフォルトでは jobs.<job_id>.strategy.matrix
で実行されるジョブのうち1つでも失敗した場合に、他のジョブらが全てキャンセルされてしまうのですが、テストの場合は全て最後まで実行されて欲しいので、jobs.<job_id>.strategy.fail-fast を false
にしています。
高速化
並列処理で大分速くなるのですが、それでも CircleCI 時代と比べると遅いです。
高速化できるポイントはいくつかあると思いますが、まず思いつくのがキャッシュを利用したものです。
rails を例に上げると、Gemfile は他のファイルより変更頻度が低いためほとんどの CI では毎回同じようなパッケージをインスールすることになります。
またテストを並列実行させている際、各ジョブ全てでパッケージのインストールやビルド実行するのは無駄になります。
actions/cacheを利用しながら改善していきます。
例
steps:
- uses: actions/checkout@v3
# 依存パッケージのキャッシュ
- name: Cache bundle gem
uses: actions/cache@v3
with:
path: vendor/bundle
key: ${{ env.bundle-cache-name }}-${{ hashFiles('Gemfile.lock') }}
restore-keys: |
${{ env.bundle-cache-name }}-
- run: bundle install --path vendor/bundle
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ env.node-cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ inputs.node-cache-name }}-
- run: npm ci
# ビルド結果のキャッシュ
- name: Cache build
uses: actions/cache@v3
with:
key: build-${{ github.sha }}
path: public/packs
- run: npm run build
flaky test
github actions に移行した際、画面に関連したテスト(rails でいう feature spec)でたまに落ちるテストがかなり増えました。
落ちるテストが多いかつ原因の特定が難しかったため、一度失敗したテストは以下の方法で再実行させることでいい感じに解消されました。
steps:
# ...
- name: Exec Rspec
run: |
bin/rspec_file_splitter -p ${{ matrix.parallelism }} -i ${{ matrix.id }} | \
xargs bundle exec rspec --failure-exit-code 0
- name: Retry Rspec Only Failures
run: bundle exec rspec --only-failures
rspec の各オプションについてはこちらを参照。
可読性
CI では lint、test、build ... など色々実行するケースがあると思いますが、これらを全て1ファイル(1ワークフロー)に詰め込むと行数がかなり多くなります。基本は lint、test、build などをそれぞれのワークフローとして分割しつつ、それでも1ワークフローでの処理内容が多い場合は、github actions の以下の機能を利用することである程度分割可能になります。
-
on.workflow_call
- CI実行前の準備段階(ビルドなど)をこちらで記述
-
on.workflow_run
- あるワークフロー成功後に実行したい処理を記述
例
name: Prepare CI
on:
workflow_call:
jobs:
prepare:
runs-on: ubuntu-latest
steps:
# CI環境の準備
# パッケージのインストール・ビルドなど
# ...
name: CI
jobs:
prepare:
uses: ./.github/workflows/prepare.yml
main:
needs: prepare
runs-on: ubuntu-latest
steps:
# prepare workflow からパッケージやビルド結果のキャッシュを基にメインの処理を実行
# ...
on:
workflow_run:
workflows: [CI]
types:
- completed
jobs:
post-ci:
steps:
# ...
まとめ
いくつか Github Actions に移行時のポイントを記載しましたが、特に高速化の部分ではまだまだ工夫の余地があると思います。
何かしら参考になれば幸いです。
Discussion