GitHub Actions Self-hosted runner を Google Compute Engine で構築する

GitHub Actions を利用する際には、デフォルトで利用できる GitHub-hosted runner を利用することが多いでしょう。しかし、特定のユースケースにおいては、独自に用意したインフラストラクチャ上で実行される Self-hosted runner を利用する必要が出てきます。

このページでは、まず最初に Self-hosted runner についての一般的な前提知識を述べます。次に、Webhook を利用したシンプルでスケーラブルなアーキテクチャの概要を示し、Google Cloud におけるその具体的な実装例について解説します。

以降、単に runner と表記した場合、Self-hosted runner を表すものとします。

Self-hosted runner の種類 #

Self-hosted runner には運用上の差がある 3 種類があります。

また、これら runner の登録スコープとして次の 3 種類があります。

Normal runner を複数リポジトリで利用できるように運用する場合、organization または enterprise スコープに登録するのが運用上は楽でしょう。Ephemeral / JIT runner はジョブごとにプロビジョニングする前提であるので、どのスコープに登録しても最終的に得られる結果に大きな差はありません。ただし、runner group など管理上の機能を利用する場合には organization または enterprise スコープに登録する必要があります。結論として、organization または enterprise のうち、要件に適合するスコープを選んで登録するのがよいでしょう。

Self-hosted runner の構成パターン #

運用観点から考えると、runner を提供するインフラの主要な構成パターンとして次のものが考えられます。

B において「ジョブ需要を検知する」具体的なアプローチとしては、以下のものが知られています。

  1. Webhook を利用する
    • Webhook は、GitHub 上のさまざまなイベントに対応して外部システムを呼び出す仕組みです。
    • 利用可能なイベントの一つである workflow_job (action=queued) は、ジョブの起動に伴い呼び出されます。
    • (a) 直接的には、このイベントは単一のジョブ需要に対応していて、イベントを受信するごとに Ephemeral runner または JIT runner を起動すればよいでしょう。
      • このアプローチはイベントドリブンでステートレスであり非常に見通しが良いです。
    • (b) より進んだ実装としては、イベントを“キュー”に格納して、これを非同期的に消費するワーカーで、Ephemeral runner または JIT runner を起動する方法です。
      • 1-a の課題は、runner の起動に失敗した時のリトライ処理、runner の上限/下限といったスケーリングポリシー、などステートフルな実装を直接するのが難しいことです。
      • イベントをキューに格納してバッファリングした上で、定期的に起動するワーカーが runner を正常に起動するごとにキューからイベントを取り除くようなアーキテクチャにすることで、上記のような点を実装に組み込むのが容易になるでしょう。
    • (c) Normal runner を利用する場合、1-b におけるキューの長さをジョブ待機数の指標とみなしてスケーリングアルゴリズムを実装することができるでしょう。
      • ただし、この場合は workflow_job (action=completed) イベントも受信して、キューを減少させるといったことが追加で必要です。
      • 指標が得られればよいので、実装上は“キュー”である必要はなく単純なカウンターでもよいでしょう。
  2. ジョブ需要の直接的なメトリクスを得る
    • GitHub Actions Runner Controller (ARC) がこの方法を採用しています。
      • 公開情報として知られている GitHub API に直接的に「ジョブ需要」を得るものは存在しません。
      • ARC では未公開の API を利用して (参考) この「ジョブ需要」を取得し、これに基づいて Kubernetes の Pod を JIT runner として起動するオートスケーリングアルゴリズムを実装しています。
      • ARC は Kubernetes の存在を前提としているため、クラウドで構築する場合には GKE, EKS などを用意するのが基本となり一定の固定費用が生じてしまい、コスト面ではワークロードに見合っているか判断が必要です。
    • ARC 以外では、1-c のように間接的に指標を得ることになるでしょう。

小規模なワークロードの場合や実装および運用規模を小さく維持したい場合には、1-a が適しているでしょう。1-a でカバーできないような大規模なワークロードや、複雑なスケーリングポリシーを適用したい場合には、1-b, 1-c も検討できるでしょう。ただし、1-b, 1-c を独自に実装およびメンテナンスするのはコストがかかるため、ARC を最初から検討しても良いかもしれません。

以降では 1-a のアプローチを具体的に検討します。

Webhook ベースのシンプルなアーキテクチャ #

Webhook は設定できるスコープにいくつかの種類があります。

また、webhook とはあまり関係ない一般論ですが、runner を登録するために GitHub API へリクエストを送る際の認証情報として、次のいずれかが必要になります。

上記の 2 観点の選択肢の違いは、セットアップ手順のほか、実装で容易に吸収できる細部の差にとどまるため、いずれの構成を仮定しても一般性を失わないでしょう。ただし、最終的には次のパターンが実践的には選択されるでしょう:

Enterprise については検証環境を持っていないためこれ以上は議論しません。後述の実装例は Organization/Repository スコープを前提としています。次に示す具体的な実装例では、後者で述べた構成を採用しています。

Google Cloud での実装例 #

terraform-module-simple-gha-runner-gce は、Google Cloud 上にシンプルな Self-hosted runner のためのインフラを構築する実装例です。モジュールを利用した実際の構築手順は README を参照してください。以下では簡単な解説にとどめます。

terraform-module-simple-gha-runner-gce のアーキテクチャ

terraform-module-simple-gha-runner-gce のアーキテクチャ

GitHub App webhook を利用し、GitHub App がインストールされたリポジトリにおけるジョブ要求を監視します。イベントを受信するエンドポイントは Cloud Run functions でホストされています。Runner は Compute Engine インスタンスで提供される JIT runner です。

このモジュールは概念実装として具体例を示すために用意したものであり、非常に簡素化されています。実用上は、次のような点で拡張が必要でしょう。

まとめ #

Self-hosted runner を構築するにあたって前提となる以下のような観点を整理しました。

また、Webhook ベースのシンプルなアーキテクチャを提案し、具体例として Google Cloud 上に構築する再利用可能な Terraform モジュールを紹介しました。今回提案した手法は小規模な環境に対しては十分に実用的で運用もしやすいと考えていますが、より大規模な条件においては ARC を念頭に検討するのがよいでしょう。