GitHub ActionsのSelf Hosted Runner向けにImage Cache Proxyを導入しました

システムプラットフォームチームの id:rskmm0chang です。9月に入社しました。 15年前にはてなのインフラ部でアルバイトをしておりまして、それ以来の入社となります。時の流れは早いですね。

この記事は、はてなのSREが毎月交代で書いているSRE連載の11月号です。10月の記事はid:s-shiroさんの既存のDeploy Preview環境をmirage-ecsに移行する - 設計編 -でした。

はてなでは、GitHub Actionsを多用しており、コスト削減などを期待し、Actions Runner Controllerを利用して、Self Hosted Runnerを使い始めています。

GitHub Actionsの中でdockerコマンドを使うようなジョブも多く、DinD*1の構成を取っていますが、その際に問題になるのがジョブ実行時のImageの取得です。各ジョブにおいて、利用するImageを都度都度取得すると次のような問題が考えられます。

  • ImageのPull速度の低下
    • インターネット経由で物理的に遠いところから取ってくる可能性がある
  • ネットワークコストの増加
    • 端的にNAT gatewaysの費用増
  • 各レジストリのRate Limit
    • 各runnerが都度都度imageを取得してしまうので、Rate Limitに到達しやすい

Imageをセルフレジストリにキャッシュし、runnerの環境の近く置くことで、これらの問題に対応できます。

ところで、KubernetesのPodとしてはcontainerdの設定で各種レジストリのミラーレジストリを設定し、キャッシュを利用することができますが、containerdの設定をしていても、DinDの構成ではPod内で動かしているdockerdの設定を使うことになります。

Imageのセルフレジストリについてはコンテナレジストリの可用性を高める取り組みに詳しく記載されていますが、dockerdではDocker Hub以外のレジストリに対して、ミラーレジストリを設定することができません。

はてなでは、distributionを導入し、DinDのdockerdのconfigに記載して、Docker HubのImageのキャッシュを行うようにしました。 しかしDocker Hub以外のレジストリ利用も多く、導入による効果は限定的でした。

そのため以降に記載するdistributionに代わる対応策を検討し、採用することで、各レジストリのImageをキャッシュすることができ、上記の問題を改善しました。

検討した対応案

今回の対応における前提として、GitHub Actionsの利用者側には修正が発生しないようにし、今回の変更を意識させないものにする、としました。

その上で、はてなでは対応案として下記を検討しました。

  1. dockercli + dockerdをnerdctl + containerdにする
  2. docker wrapperを作って、レジストリの向き先を変える
  3. proxyを使う(Man in the Middle方式)

dockercli + dockerdをnerdctl + containerdにする

containerdであれば各種レジストリに対応することが可能であり、そのcliツールである、nerdctlでは、ある程度dockercliとの互換性を保つように開発されているため、dockercliやdockerdの置き換えに対応できるのでは、と考えました。 ただし、そのまま導入しただけでは、dockerコマンドをすべてnerdctlに置き換える必要があるため、runner内においてdockerコマンドのパスをnerdctlに向けて、強制的にnerdctlを使う方法を検討しました。

しかし、すべてのジョブにnerdctlで対応できるかどうかわからず、「利用者側には修正が発生しない」という前提条件を満たせない可能性が高く、見送りました。

docker wrapperを作って、レジストリの向き先を変える

dockerdを利用しているときに任意のリポジトリに対してパススルー・キャッシュを使うを参考に、dockerコマンドのwrapperを作成して、wrapperの中でパース・変換して、レジストリの向き先をセルフレジストリに変える方法を検討しました。

しかし、この対応ではdocker composeなど、コマンド内部でレジストリを指定する場合には対応が難しく、現状と同様にImageのキャッシュの効果が上がらないことが考えられ、この案は見送りました。

proxyを使う(Man in the Middle方式)

この案は、docker用のproxyを用意し、dockerdのHTTPS_PROXYを設定し、コンテナレジストリへのアクセスをセルフレジストリに変更してしまう方式です。proxyとして採用したのはrpardini/docker-registry-proxyで、このツールはnginxを利用しており、nginxの機能を利用して、Imageのmanifestやblobをキャッシュします。そのため、Imageのセルフレジストリはこのproxyが兼ねることになります。

現状のはてなのGitHub Actionsの使い方を考慮しつつ、今回の前提を満たすことができるため、この方法が最適であると判断しました。

ただし、この構成の場合、proxyが単一障害点になりやすい点が懸念点です。ミラーレジストリの設定の場合は、ミラーレジストリに接続できない場合はアップストリーム側に接続を行いますが*2、HTTPS_PROXYの設定ではそのような動きにはなりません。 とはいえ、重要なGitHub Actions(リリース用のCIなど)はほとんどが業務時間内で動いており、proxyに問題が発生した際も迅速に対応できるため、この懸念点に関しては許容範囲であると判断しました。

このproxyをStatefulsetで動かし、Pod再起動時でもキャッシュを保存しているPVを再利用するするようにしています。

ただ、開発がやや止まっていることもあり、検証で利用していた際、いくつかの細かい問題があり、最終的にはそのforkである、coreweave/docker-registry-proxyを採用しました。こちらのほうが設定できるオプションが多く、ghcrに対する微妙な問題にも対応しています。

オプションに関しては、キャッシュのディスク空き容量が設定できたり、キャッシュを廃棄するまでの期間が設定できるようになっています。

本家: https://github.com/rpardini/docker-registry-proxy/blob/master/entrypoint.sh#L105

fork: https://github.com/coreweave/docker-registry-proxy/blob/coreweave/entrypoint.sh#L108

まとめ

Github ActionsのSelf Hosted Runner(DinD構成)に対して、proxyを導入して、Docker Hub以外のImageもキャッシュできるようにしました。これにより、Self Hosted Runner(DinD構成)のImageまわりの問題を改善することができました。 Self Hosted RunnerにおけるDinDの構成で、このあたりに苦慮している方の一助になれば幸いです。

*1:Docker in Docker。DockerのコンテナとしてDockerプロセスを動かす構成。Kubernetes側はcontainerdなので実際にはDocker in contanerdで、DinC

*2:何回かミラーレジストリへの接続をリトライするので遅くはなる