Z Lab Advent Calendar 2023 4日目の記事になります。
Z Lab では今まで「コミュニティメンテナンス版」とも言われるレガシーオートスケーリングモードのGitHub Actions Runner Controller (ARC) を利用していましたが、GitHub 公式版のARCに乗り換えることにしました。
このコミュニティメンテナンス版のARCは元Z Labに所属していた @summerwind さんが開発を始め、これまた元Z Lab所属の @mumoshu さんがメンテナンスをしてきたという、弊社からするととても思い入れのあるプロダクトだったりするのですが、これもまた世の流れ、諸行無常、色即是空。
乗り換えにあたって問題となったこと
さて、GitHub公式版のインストール方法やら、使ってみた、公式版ARCを利用することのメリットなどは他の記事に任せるとして、Z Labでこの公式版に移行しようと思った際にその差異の中で問題となった部分について語ろうと思います。
- Runner のラベルを複数指定する方法が現状ない
- Job実行前後のHookがない
- DinD構成だとdockerdとRunnerで別コンテナになってしまう
- クラスターのローリングアップデート時にJobを中断したくない
ちなみに、検証時のバージョンは以下のとおりです。
- gha-runner-scale-set: v0.6.0
- ghcr.io/actions/actions-runner: 2.311.0
Runner のラベルを複数指定する方法が現状ない
一つ目は、「Runner のラベルを複数指定する方法が現状ない」です。
これはどういうことかというと、例えば、Ubuntu 22.04 のコンテナイメージで動作する Runner を指定するラベルについて、ubuntu-22.04
と ubuntu-jammy
と複数のラベルを付与することができない、ということです。
jobs:
validate:
name: Validate
runs-on: ubuntu-22.04 # <- これ
公式としては今のところこれをサポートする予定はないようで、同じ spec
の AutoScalingRunnerSet
を複数デプロイするしかないです。
- Deploy runner with multiple label with gha-runner-scale-set? #2921
- Deploying runner scale sets with Actions Runner Controller / About runner scale sets
正直ちょっとやりづらいところがあるので、どうにかしてほしいところです。
Job実行前後のHookがない
二つ目は「Job実行前後のHookがない」ことでした。
コミュニティメンテナンス版のARCでは、Runnerのコンテナイメージとしてsummerwind/actions-runner
が利用されていました。
これが意外と痒い所に手が届くようなコンテナイメージで、公式版のコンテナイメージである ghcr.io/actions/actions-runner
にはない機能がいくつかあったりします。
そのうちの一つが、Jobの実行前後のフックの提供です。
以下の二つのディレクトリにスクリプトを配置しておくと、適宜Jobの実行時、終了時に実行されます。
/etc/arc/hooks/job-started.d/
/etc/arc/hooks/job-completed.d/
Z LabではこのHookを利用して、どのPodがどのレポジトリのどのワークフローを実行しているのかについてトラックしていました。
具体的には以下のようなスクリプトを実行することで実行中のPodにアノテーションとしてJobの情報を付与し、その値をPrometheusのメトリクスとして取得していました。
jq -n \
--arg workflow_repository "${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}" \
--arg workflow_name "${GITHUB_WORKFLOW:-}" \
--arg workflow_run_id "${GITHUB_RUN_ID:-}" \
--arg workflow_run_number "${GITHUB_RUN_NUMBER:-}" \
--arg workflow_job "${GITHUB_JOB:-}" \
--arg workflow_action "${GITHUB_ACTION:-}" '
.metadata.annotations["repo.name"] = $workflow_repository
| .metadata.annotations["workflow.name"] = $workflow_name
| .metadata.annotations["workflow.run_id"] = $workflow_run_id
| .metadata.annotations["workflow.run_number"] = $workflow_run_number
| .metadata.annotations["workflow.job"] = $workflow_job
| .metadata.annotations["workflow.action"] = $workflow_action
' | curl \
--cacert ${serviceaccount}/ca.crt \
--data @- \
--noproxy '*' \
--header "Content-Type: application/merge-patch+json" \
--header "Authorization: Bearer ${token}" \
--show-error \
--silent \
--request PATCH \
"${apiserver}/api/v1/namespaces/${namespace}/pods/${HOSTNAME}" \
1>/dev/null
これに関しては、そもそも大元の Actions Runner 自体に Hook の機能があったので、それを直接利用することで解決できました。
Runnerに以下の二つの環境変数を設定することにより、Jobの実行時と終了時に任意のスクリプトを実行することができるようになるようです。
export ACTIONS_RUNNER_HOOK_JOB_STARTED=/etc/arc/hooks/job-started.sh
export ACTIONS_RUNNER_HOOK_JOB_COMPLETED=/etc/arc/hooks/job-completed.sh
DinD構成だとdockerdとRunnerで別コンテナになってしまう
Jobの中でコンテナを利用する場合、Dockerデーモンが必要になってきます。
公式版のRunnerでは、dind
と kubernetes
という二つのモードが用意されているのですが、dind
のモードを使った場合、RunnerのPodの中で、Runnerのコンテナとdockerdのコンテナが二つ起動してしまいます。
さて、Z Lab ではRunnerを large, middle, small という三つのフレーバーで提供しており、利用者はそれぞれの用途に合わせたフレーバーを選択することで、リソースの使用量をケチって節約しています。
-
large
: 8 CPU, Memory 16Gi -
middle
: 4 CPU , Memory 8Gi -
small
: 2 CPU, Memory 2Gi
さて、勘の良い人はもうすでに気付いたと思いますが、このリソース使用量の制限は Pod のコンテナに対する resources.requests
/ resources.limits
で実現しています。
この制限は Pod レベルではなく、コンテナレベルでの制限のためコンテナ二つのPodに対して上記のリソースを上手に割り当てることは難しいです。
8CPUを割り当てたいRunnerがいた際に、Runnerコンテナに8CPUを割り当てたとしてもDockerdコンテナにはCPUは割り当たっていません。
それぞれのコンテナに8CPUずつのリソースを割り当ててしまうとリソース要求量が実際に必要なリソース量の2倍になってしまいます。
Z Lab では苦肉の策として、Runnerコンテナ内で別プロセスとして dockerd を起動することにしました。
ぶっちゃけコミュニティ版ARCの entrypoint-dind.sh
をほぼ丸コピです。
log.debug 'Starting Docker daemon'
sudo /usr/bin/dockerd &
log.debug 'Waiting for processes to be running...'
processes=(dockerd)
for process in "${processes[@]}"; do
if ! wait_for_process "$process"; then
log.error "$process is not running after max time"
exit 1
else
log.debug "$process is running"
fi
done
docker自体は最新のバージョンが ghcr.io/actions/actions-runner
のイメージに含まれているため別途インストールする必要はなかったです。
これもPodレベルのresource.requests
/ resources.limits
が実装されれば綺麗に1コンテナ1プロセスで実装できたのですが、KEPを見る限り時間がかかってそうです。
dockerdを別プロセスとして起動する際の注意点としては、/var/lib/docker
をVOLUMEとして切り出しておかないとdocker pull
などの動作が激しく重くなることなどでしょうか。
VOLUME /var/lib/docker
クラスターのローリングアップデート時にJobを中断したくない
さて、コミュニティ版のARCには RUNNER_GRACEFUL_STOP_TIMEOUT
という非常に便利な機能がありました。
通常、KubernetesではPodが削除された場合コンテナ内のプロセスに対してSIGTERM
が送られます。何も考えないとSIGTERM
はそのままRunnerのプロセスに送られてプロセスが終了してしまいます。
それを防ぐのが RUNNER_GRACEFUL_STOP_TIMEOUT
という環境変数でした。
この環境変数を設定すると、Jobを実行中のRunnerのプロセスに対して設定した秒数間だけSIGTERM
を送ることを遅らせることができます。
つまり、一番時間のかかるJobに合わせてこの値を設定しておくことで、クラスターのローリングアップデートに際してもJobが完全に実行されることを保証することができます。
もちろん、クラスターのローリングアップデートを行う仕組みとの協調は必要です。
公式版の Runner Scale Set で同様のことをするには、結局公式のイメージ ghcr.io/actions/actions-runner
に同様の機能を移植する必要がありました。
具体的にいうとrunner/graceful-stop.sh
をいい感じに修正してエントリポイントから呼び出す必要がありました。
ただ、この runner/graceful-stop.sh
は、Runnerのコンテナ内のプロセス自体が GitHub からの Runner の登録の削除を行うといった手順と協調することで、
アトミック (削除中のRunnerが新たなJobを実行し始めたりしないように) にプロセスが終了できるのですが、公式版のRunnerでは、GitHubへのRunnerの登録などの処理がコントローラに移行するなどのアーキテクチャ上の変更により、アトミックにRunnerの終了が行えるように移植はできませんでした。
コンテナイメージの小手先でどうにかなるような話ではなさそうだったので、Runner自体の改善が必要そうな感じでした。残念。
まとめ
と、いうことで、Z Lab ではコミュニティ版のARCから、GitHub公式版のARCへの移行を進めているのですが、贔屓目に見てコミュニティ版の機能がまだまだ公式版に足りてないなーという印象でした。
(っていうか、完全に内部が書き変わってるしコンテナイメージも別物だし、すごい労力かかってるなこれ。)