公式版GitHub Actions Runner Controllerに移行してみた (未)

[Z Lab Advent Calendar 2023](https://qiita.com/advent-calendar/2023/zlab) 4日目の記事になります。

Z Lab では今まで「コミュニティメンテナンス版」とも言われる[レガシーオートスケーリングモードのGitHub Actions Runner Controller (ARC)](https://github.com/actions/actions-runner-controller#legacy-documentation) を利用していましたが、GitHub 公式版のARCに乗り換えることにしました。

このコミュニティメンテナンス版のARCは元Z Labに所属していた @summerwind さんが開発を始め、これまた元Z Lab所属の @mumoshu さんがメンテナンスをしてきたという、弊社からするととても思い入れのあるプロダクトだったりするのですが、これもまた世の流れ、諸行無常、色即是空。

## 乗り換えにあたって問題となったこと

さて、GitHub公式版のインストール方法やら、使ってみた、公式版ARCを利用することのメリットなどは他の記事に任せるとして、Z Labでこの公式版に移行しようと思った際にその差異の中で問題となった部分について語ろうと思います。

1. Runner のラベルを複数指定する方法が現状ない
2. Job実行前後のHookがない
3. DinD構成だとdockerdとRunnerで別コンテナになってしまう
4. クラスターのローリングアップデート時に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` と複数のラベルを付与することができない、ということです。

```yaml
jobs:
  validate:
    name: Validate

    runs-on: ubuntu-22.04 # <- これ
```

公式としては今のところこれをサポートする予定はないようで、同じ `spec` の `AutoScalingRunnerSet` を複数デプロイするしかないです。

- [Deploy runner with multiple label with gha-runner-scale-set? #2921](https://github.com/actions/actions-runner-controller/discussions/2921)
- [Deploying runner scale sets with Actions Runner Controller / About runner scale sets](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#about-runner-scale-sets)

正直ちょっとやりづらいところがあるので、どうにかしてほしいところです。

### Job実行前後のHookがない

二つ目は「Job実行前後のHookがない」ことでした。

コミュニティメンテナンス版のARCでは、Runnerのコンテナイメージとして[`summerwind/actions-runner`](https://hub.docker.com/r/summerwind/actions-runner) が利用されていました。
これが意外と痒い所に手が届くようなコンテナイメージで、公式版のコンテナイメージである [`ghcr.io/actions/actions-runner`](https://github.com/actions/runner/pkgs/container/actions-runner) にはない機能がいくつかあったりします。
そのうちの一つが、Jobの実行前後のフックの提供です。

以下の二つのディレクトリにスクリプトを配置しておくと、適宜Jobの実行時、終了時に実行されます。

- `/etc/arc/hooks/job-started.d/`
- `/etc/arc/hooks/job-completed.d/`

Z LabではこのHookを利用して、どのPodがどのレポジトリのどのワークフローを実行しているのかについてトラックしていました。

具体的には以下のようなスクリプトを実行することで実行中のPodにアノテーションとしてJobの情報を付与し、その値をPrometheusのメトリクスとして取得していました。

```bash
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 の機能があったので、それを直接利用することで解決できました。

- [ADR: Notification Hooks for Runners](https://github.com/actions/runner/blob/main/docs/adrs/1751-runner-job-hooks.md)

Runnerに以下の二つの環境変数を設定することにより、Jobの実行時と終了時に任意のスクリプトを実行することができるようになるようです。

```bash
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に対して上記のリソースを上手に割り当てることは難しいです。

:::note info
8CPUを割り当てたいRunnerがいた際に、Runnerコンテナに8CPUを割り当てたとしてもDockerdコンテナにはCPUは割り当たっていません。
それぞれのコンテナに8CPUずつのリソースを割り当ててしまうとリソース要求量が実際に必要なリソース量の2倍になってしまいます。
:::

Z Lab では苦肉の策として、Runnerコンテナ内で別プロセスとして dockerd を起動することにしました。
ぶっちゃけコミュニティ版ARCの [`entrypoint-dind.sh`](https://github.com/actions/actions-runner-controller/blob/2c29cfb9944ec824c9c56ab4a076fb679834c75c/runner/entrypoint-dind.sh) をほぼ丸コピです。

```bash
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を見る限り時間がかかってそうです。

- [Pod level resource limits #2837](https://github.com/kubernetes/enhancements/issues/2837)
- [KEP-2837: Pod level resource limits #1592](https://github.com/kubernetes/enhancements/pull/1592)

dockerdを別プロセスとして起動する際の注意点としては、`/var/lib/docker` をVOLUMEとして切り出しておかないと`docker pull`などの動作が激しく重くなることなどでしょうか。

```Dockerfile
VOLUME /var/lib/docker
```

### クラスターのローリングアップデート時にJobを中断したくない

さて、コミュニティ版のARCには `RUNNER_GRACEFUL_STOP_TIMEOUT` という非常に便利な機能がありました。

通常、KubernetesではPodが削除された場合コンテナ内のプロセスに対して`SIGTERM`が送られます。何も考えないと`SIGTERM`はそのままRunnerのプロセスに送られてプロセスが終了してしまいます。
それを防ぐのが `RUNNER_GRACEFUL_STOP_TIMEOUT` という環境変数でした。

この環境変数を設定すると、Jobを実行中のRunnerのプロセスに対して設定した秒数間だけ`SIGTERM` を送ることを遅らせることができます。
つまり、一番時間のかかるJobに合わせてこの値を設定しておくことで、クラスターのローリングアップデートに際してもJobが完全に実行されることを保証することができます。

:::note info
もちろん、クラスターのローリングアップデートを行う仕組みとの協調は必要です。
:::

公式版の Runner Scale Set で同様のことをするには、結局公式のイメージ `ghcr.io/actions/actions-runner` に同様の機能を移植する必要がありました。
具体的にいうと[`runner/graceful-stop.sh`](https://github.com/actions/actions-runner-controller/blob/2c29cfb9944ec824c9c56ab4a076fb679834c75c/runner/graceful-stop.sh) をいい感じに修正してエントリポイントから呼び出す必要がありました。

ただ、この `runner/graceful-stop.sh` は、Runnerのコンテナ内のプロセス自体が GitHub からの Runner の登録の削除を行うといった手順と協調することで、
アトミック (削除中のRunnerが新たなJobを実行し始めたりしないように) にプロセスが終了できるのですが、公式版のRunnerでは、GitHubへのRunnerの登録などの処理がコントローラに移行するなどのアーキテクチャ上の変更により、アトミックにRunnerの終了が行えるように移植はできませんでした。

コンテナイメージの小手先でどうにかなるような話ではなさそうだったので、Runner自体の改善が必要そうな感じでした。残念。

## まとめ

と、いうことで、Z Lab ではコミュニティ版のARCから、GitHub公式版のARCへの移行を進めているのですが、贔屓目に見てコミュニティ版の機能がまだまだ公式版に足りてないなーという印象でした。

(っていうか、完全に内部が書き変わってるしコンテナイメージも別物だし、すごい労力かかってるなこれ。)