Cloud Run ワーカープールで GitHub ランナーをホストする

このチュートリアルでは、ワーカープールでセルフホスト型 GitHub ランナーを使用して、GitHub リポジトリで定義されたワークフローを実行する方法について説明します。

このワークロードを処理するために Cloud Run ワーカープールをデプロイし、必要に応じてワーカープールのスケーリングをサポートするために Cloud Run 関数をデプロイします。

セルフホスト型 GitHub ランナーについて

GitHub Actions ワークフローでは、ランナーはジョブを実行するマシンです。たとえば、ランナーはリポジトリをローカルにクローンし、テスト ソフトウェアをインストールしてから、コードを評価するコマンドを実行できます。

セルフホスト型ランナーを使用すると、Cloud Run ワーカー プール インスタンスで GitHub Actions を実行できます。このチュートリアルでは、実行中のジョブとスケジュールされていないジョブの数に基づいてランナーのプールを自動的にスケーリングする方法について説明します。ジョブがない場合は、プールをゼロにスケーリングすることもできます。

コードサンプルを取得する

使用するコードサンプルを取得するには:

  1. ローカルマシンにサンプル リポジトリのクローンを作成します。

    git clone https://github.com/GoogleCloudPlatform/cloud-run-samples
    
  2. Cloud Run のサンプルコードが含まれているディレクトリに移動します。

    cd cloud-run-samples/github-runner
    

コアコードについて

このサンプルは、以下のようにワーカープールとオートスケーラーとして実装されます。

ワーカープール

ワーカープールは、GitHub で作成された actions/runner イメージに基づく Dockerfile で構成されています。

このイメージには、小さなヘルパースクリプトを除き、すべてのロジックが含まれています。

FROM ghcr.io/actions/actions-runner:2.329.0

# Add scripts with right permissions.
USER root
# hadolint ignore=DL3045
COPY start.sh start.sh
RUN chmod +x start.sh

# Add start entrypoint with right permissions.
USER runner
ENTRYPOINT ["./start.sh"]

このヘルパースクリプトは、コンテナの起動時に実行され、作成するトークンを使用して、構成されたリポジトリにエフェメラル インスタンスとして登録されます。このスクリプトでは、コンテナがスケールダウンされたときに実行するアクションも定義します。

# Configure the current runner instance with URL, token and name.
mkdir /home/docker/actions-runner && cd /home/docker/actions-runner
echo "GitHub Repo: ${GITHUB_REPO_URL} for ${RUNNER_PREFIX}-${RUNNER_SUFFIX}"
./config.sh --unattended --url ${GITHUB_REPO_URL} --pat ${GH_TOKEN} --name ${RUNNER_NAME}

# Function to cleanup and remove runner from Github.
cleanup() {
   echo "Removing runner..."
   ./config.sh remove --unattended --pat ${GH_TOKEN}
}

# Trap signals.
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM

# Run the runner.
./run.sh & wait $!

オートスケーラー

オートスケーラーは、キューに新しいジョブがある場合はワーカー プールをスケールアップし、ジョブが完了した場合はスケールダウンする関数です。Cloud Run API を使用してプール内の現在のワーカー数を確認し、必要に応じてその値を調整します。

try:
    current_instance_count = get_current_worker_pool_instance_count()
except ValueError as e:
    return f"Could not retrieve instance count: {e}", 500

# Scale Up: If a job is queued and we have available capacity
if action == "queued" and job_status == "queued":
    print(f"Job '{job_name}' is queued.")

    if current_instance_count < MAX_RUNNERS:
        new_instance_count = current_instance_count + 1
        try:
            update_runner_instance_count(new_instance_count)
            print(f"Successfully scaled up to {new_instance_count} instances.")
        except ValueError as e:
            return f"Error scaling up instances: {e}", 500
    else:
        print(f"Max runners ({MAX_RUNNERS}) reached.")

# Scale Down: If a job is completed, check to see if there are any more pending
# or in progress jobs and scale accordingly.
elif action == "completed" and job_status == "completed":
    print(f"Job '{job_name}' completed.")

    current_queued_actions, current_running_actions = get_current_actions()
    current_actions = current_queued_actions + current_running_actions

    if current_queued_actions >= 1:
        print(
            f"GitHub says {current_queued_actions} are still pending."
            f"Won't change scaling ({current_instance_count})."
        )
    elif current_queued_actions == 0 and current_running_actions >= 1:
        print(
            f"GitHub says no queued actions, but {current_running_actions} running actions."
            f"Won't change scaling ({current_instance_count})."
        )
    elif current_actions == 0:
        print(f"GitHub says no pending actions. Scaling to zero.")
        update_runner_instance_count(0)
        print(f"Successfully scaled down to zero.")
    else:
        print(
            f"Detected an unhandled state: {current_queued_actions=}, {current_running_actions=}"
        )
else:
    print(
        f"Workflow job event for '{job_name}' with action '{action}' and "
        f"status '{job_status}' did not trigger a scaling action."
    )

IAM を構成する

このチュートリアルでは、プロビジョニングされたリソースを使用するために必要な最小権限を持つカスタム サービス アカウントを使用します。サービス アカウントを設定するには、次の操作を行います。

  1. gcloud でプロジェクト ID を設定します。

    gcloud config set project PROJECT_ID
    

    PROJECT_ID は、実際のプロジェクト ID に置き換えます。

  2. 新しい Identity and Access Management サービス アカウントを作成します。

    gcloud iam service-accounts create gh-runners
    

  3. プロジェクトでサービス アカウントとして動作する権限をサービス アカウントに付与します。

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role=roles/iam.serviceAccountUser
    

    PROJECT_ID は、実際のプロジェクト ID に置き換えます。

GitHub 情報を取得する

セルフホスト ランナーの追加に関する GitHub のドキュメントでは、GitHub ウェブサイトからランナーを追加し、認証に使用する特定のトークンを取得することが推奨されています。

このチュートリアルでは、ランナーを動的に追加および削除するため、静的 GitHub トークンが必要です。

このチュートリアルを完了するには、選択したリポジトリとやり取りするためのアクセス権を持つ GitHub トークンを作成する必要があります。

GitHub リポジトリを特定する

このチュートリアルでは、GITHUB_REPO 変数はリポジトリ名を表します。これは、個人ユーザー リポジトリと組織リポジトリの両方で、ドメイン名の後に続く GitHub リポジトリ名の部分です。

ユーザー所有のリポジトリと組織所有のリポジトリの両方で、ドメイン名の後に続くリポジトリ名を参照します。

このチュートリアルの内容:

  • https://github.com/myuser/myrepo の場合、GITHUB_REPOmyuser/myrepo です。
  • https://github.com/mycompany/ourrepo の場合、GITHUB_REPOmycompany/ourrepo です。

アクセス トークンを作成する

GitHub でアクセス トークンを作成し、Secret Manager に安全に保存する必要があります。

  1. GitHub アカウントにログインしていることを確認します。
  2. GitHub の [Settings] > [Developer Settings] > [Personal Access Tokens] ページに移動します。
  3. [Generate new token] をクリックし、[Generate new token (classic)] を選択します。
  4. 「repo」スコープで新しいトークンを作成します。
  5. [Generate token] をクリックします。
  6. 生成されたトークンをコピーします。

シークレット値を作成する

作成したシークレット トークンを取得して Secret Manager に保存し、アクセス権限を設定します。

  1. Secret Manager でシークレットを作成します。

    echo -n "GITHUB_TOKEN" | gcloud secrets create github_runner_token --data-file=-
    

    GITHUB_TOKEN は、GitHub からコピーした値に置き換えます。

  2. 新しく作成したシークレットへのアクセス権を付与します。

    gcloud secrets add-iam-policy-binding github_runner_token \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role "roles/secretmanager.secretAccessor"
    

ワーカープールをデプロイする

GitHub アクションを処理する Cloud Run ワーカープールを作成します。このプールは、GitHub で作成された actions/runner イメージに基づくイメージを使用します。

Cloud Run ワーカープールを設定する

  1. ワーカープールのサンプルコードに移動します。

    cd worker-pool-container
    
  2. ワーカープールをデプロイします。

    gcloud beta run worker-pools deploy WORKER_POOL_NAME \
      --region WORKER_POOL_LOCATION \
      --source . \
      --scaling 1 \
      --set-env-vars GITHUB_REPO=GITHUB_REPO \
      --set-secrets GITHUB_TOKEN=github_runner_token:latest \
      --service-account gh-runners@PROJECT_ID.iam.gserviceaccount.com \
      --memory 2Gi \
      --cpu 4
    

    次のように置き換えます。

    • WORKER_POOL_NAME: ワーカープールの名前
    • WORKER_POOL_LOCATION: ワーカープールのリージョン
    • GITHUB_REPO: 特定された GitHub リポジトリ名
    • PROJECT_ID: Google Cloud プロジェクト ID

    このプロジェクトで Cloud Run ソースのデプロイを初めて使用する場合は、デフォルトの Artifact Registry リポジトリの作成を求めるメッセージが表示されます。

ワーカープールの使用

これで、ワーカープールに単一のインスタンスが作成され、GitHub Actions からのジョブを受け入れる準備が整いました。

セルフホスト ランナーの設定が完了したことを確認するには、リポジトリで GitHub アクションを呼び出します。

アクションでセルフホスト ランナーを使用するには、GitHub アクションのジョブを変更する必要があります。ジョブで、runs-on の値を self-hosted に変更します。

リポジトリにアクションがまだない場合は、GitHub Actions のクイックスタートをご覧ください。

セルフホスト ランナーを使用するようにアクションを構成したら、アクションを実行します。

GitHub インターフェースでアクションが正常に完了したことを確認します。

GitHub Runner Autoscaler をデプロイする

元のプールに 1 つのワーカーをデプロイしました。これにより、一度に 1 つのアクションを処理できます。CI の使用状況によっては、実行する作業の急増に対応するためにプールをスケーリングする必要があります。

アクティブな GitHub ランナーを使用してワーカープールをデプロイしたら、アクション キュー内のジョブのステータスに基づいてワーカー インスタンスをプロビジョニングするようにオートスケーラーを構成します。

この実装は workflow_job イベントをリッスンします。ワークフロー ジョブが作成されると、ワーカー プールがスケールアップされ、ジョブが完了するとスケールダウンされます。構成されたインスタンスの最大数を超えてプールをスケーリングすることはありません。実行中のすべてのジョブが完了すると、ゼロにスケーリングされます。

このオートスケーラーは、ワークロードに基づいて調整できます。

Webhook シークレット値を作成する

Webhook のシークレット値を作成する手順は次のとおりです。

  1. 任意の文字列値を含む Secret Manager シークレットを作成します。

    echo -n "WEBHOOK_SECRET" | gcloud secrets create github_webhook_secret --data-file=-
    

    WEBHOOK_SECRET は任意の文字列値に置き換えます。

  2. オートスケーラー サービス アカウントにシークレットへのアクセス権を付与します。

    gcloud secrets add-iam-policy-binding github_webhook_secret \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role "roles/secretmanager.secretAccessor"
    

Webhook リクエストを受信する関数をデプロイする

Webhook リクエストを受信する関数をデプロイする手順は次のとおりです。

  1. Webhook のサンプルコードに移動します。

    cd ../autoscaler
    
  2. Cloud Run functions の関数をデプロイします。

    gcloud run deploy github-runner-autoscaler \
      --function github_webhook_handler \
      --region WORKER_POOL_LOCATION \
      --source . \
      --set-env-vars GITHUB_REPO=GITHUB_REPO \
      --set-env-vars WORKER_POOL_NAME=WORKER_POOL_NAME \
      --set-env-vars WORKER_POOL_LOCATION=WORKER_POOL_LOCATION \
      --set-env-vars MAX_RUNNERS=5 \
      --set-secrets GITHUB_TOKEN=github_runner_token:latest \
      --set-secrets WEBHOOK_SECRET=github_webhook_secret:latest \
      --service-account gh-runners@PROJECT_ID.iam.gserviceaccount.com \
      --allow-unauthenticated
    

    次のように置き換えます。

    • GITHUB_REPO: ドメイン名の後の GitHub リポジトリ名の部分
    • WORKER_POOL_NAME: ワーカープールの名前
    • WORKER_POOL_LOCATION: ワーカープールのリージョン
    • REPOSITORY_NAME: GitHub リポジトリ名
  3. サービスがデプロイされた URL をメモします。この値は後の手順で使用します。

  4. ワーカープールを更新する権限をサービス アカウントに付与します。

    gcloud alpha run worker-pools add-iam-policy-binding WORKER_POOL_NAME \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role=roles/run.developer
    

    PROJECT_ID は、実際のプロジェクト ID に置き換えます。

GitHub webhook を作成する

GitHub Webhook を作成する手順は次のとおりです。

  1. GitHub アカウントにログインしていることを確認します。
  2. GitHub リポジトリに移動します。
  3. [設定] をクリックします。
  4. [コードと自動化] で [Webhook] をクリックします。
  5. [Add webhook] をクリックします。
  6. 次の情報を入力します。

    1. [Payload URL] に、前にデプロイした Cloud Run 関数の URL を入力します。

      URL は https://github-runner-autoscaler-PROJECTNUM.REGION.run.app のようになります。ここで、PROJECTNUM はプロジェクトの一意の数値識別子、REGION はサービスをデプロイしたリージョンです。

    2. [コンテンツ タイプ] で、[application/json] を選択します。

    3. [Secret] に、前に作成した WEBHOOK_SECRET の値を入力します。

    4. [SSL 認証] で、[SSL 認証を有効にする] を選択します。

    5. [どのイベントでこの Webhook をトリガーしたいですか?] で、[個別のイベントを選択] を選択します。

    6. イベントの選択で、[ワークフロー ジョブ] を選択します。他のオプションの選択を解除します。

    7. [Add webhook] をクリックします。

ワーカープールをスケールダウンする

Webhook が配置されたため、プールに永続的なワーカーを配置する必要はありません。これにより、作業がないときに実行中のワーカーがなくなるため、コストを削減できます。

  • プールを調整してゼロにスケーリングします。

    gcloud beta run worker-pools update WORKER_POOL_NAME \
      --region WORKER_POOL_LOCATION \
      --scaling 0
    

自動スケーリング ランナーを使用する

自動スケーリング ランナーが正しく機能していることを確認するには、以前に runs-on: self-hosted に構成したアクションを実行します。

GitHub Actions の進行状況は、リポジトリの [アクション] タブで確認できます。

Webhook 関数とワーカープールの実行状況は、Cloud Run 関数と Cloud Run ワーカープールの [ログ] タブでそれぞれ確認できます。