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

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

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

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

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

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

目標

このチュートリアルの内容は次のとおりです。

  • Cloud Run ワーカープールを Cloud Run にデプロイする。
  • ワーカープールのスケーリングをサポートする Cloud Run 関数をデプロイする。
  • Secret Manager のシークレットを作成して、トークンとシークレットを安全に保存する。
  • GitHub リポジトリをサポートするセルフホスト型 GitHub ランナーをデプロイする。

費用

このドキュメントでは、課金対象である次の Google Cloudコンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。

新規の Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

始める前に

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  5. Verify that billing is enabled for your Google Cloud project.

  6. Enable the Cloud Run, Secret Manager, Artifact Registry, and Cloud Build APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  7. 必要なロール

    チュートリアルを完了するために必要な権限を取得するには、プロジェクトに対する次の IAM ロールを付与するよう管理者に依頼してください。

    ロールの付与については、プロジェクト、フォルダ、組織に対するアクセス権の管理をご覧ください。

    必要な権限は、カスタムロールや他の事前定義ロールから取得することもできます。

    セルフホスト型ランナーを構成するには、GitHub リポジトリの設定を編集する権限が必要です。リポジトリは、ユーザー所有のリポジトリにすることも組織所有のリポジトリにすることもできます。

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

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

  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. Cloud 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 ランナーのオートスケーラーをデプロイする

元のプールに 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. [Content type] で、[application/json] を選択します。

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

    4. [SSL verification] で、[Enable SSL verification] を選択します。

    5. 「Which events would you like to trigger this webhook?」に対して、[Let me select individual events] を選択します。

    6. イベントの選択で、[Workflow jobs] を選択します。他のオプションの選択を解除します。

    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 の進行状況は、リポジトリの [Actions] タブで確認できます。

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

クリーンアップ

Google Cloud アカウントで追加料金が発生しないようにするには、このチュートリアルでデプロイしたすべてのリソースを削除します。

プロジェクトを削除する

このチュートリアル用に新規プロジェクトを作成した場合は、そのプロジェクトを削除します。既存のプロジェクトを使用し、このチュートリアルで行った変更を加えずに残す場合は、チュートリアル用に作成したリソースを削除します。

課金をなくす最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。

プロジェクトを削除するには:

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

チュートリアル リソースを削除する

  1. このチュートリアルでデプロイした Cloud Run サービスを削除します。Cloud Run サービスの費用は、リクエストを受け取るまでは発生しません。

    Cloud Run サービスを削除するには、次のコマンドを実行します。

    gcloud run services delete SERVICE-NAME

    SERVICE-NAME は、サービスの名前に置き換えます。

    Cloud Run サービスは Google Cloud コンソールで削除することもできます。

  2. チュートリアルの設定時に追加した gcloud のデフォルトのリージョン構成を削除します。

     gcloud config unset run/region
    
  3. プロジェクト構成を削除します。

     gcloud config unset project
    
  4. このチュートリアルで作成した他の Google Cloud リソースを削除します。

次のステップ