🏷️

【Cloud Runのタグ付きリビジョン】プルリクエストごとに自動でプレビュー環境を立ち上げる

に公開

1. はじめに

この記事では、プルリクエストごとに自動でプレビュー環境を立ち上げる仕組みを構築する手順を紹介します。
Cloud Runの「タグ付きリビジョン」 とGitHub Actionsを利用した方法です。

2. 課題と目的

現在、弊社ではAI駆動開発の確立に取り組んでいます。その取り組みの一環として、プレビュー環境を立ち上げる仕組みを検証しました。

開発者の皆さんは、AIエージェントが作成したプルリクエストの動作確認をどのように行っていますか?
方法は色々あるかと思うのですが、私はエージェントが作成したプルリクエストのブランチを自分のローカル環境に落としてきてから、ローカルで立ち上げて動作確認をしていました。これが少し面倒なのですよね...

そこで、AIエージェントが(もちろん人間の開発者も)プルリクエストを作成した時に自動でプレビュー環境を立ち上げることで、人間が簡単に確認できる仕組みを検証することになりました。

3. 使用技術

Cloud Runのタグ付きリビジョン

どうやってプレビュー環境を立ち上げるかというと、Cloud Runの「タグ付きリビジョン」という仕組みを使います。

リビジョンとは?
Cloud Runサービスでデプロイしたアプリケーションは、リビジョンという設定のまとまりでバージョン管理されます。デプロイのたびに、新しいリビジョンが作られます。

タグ付きリビジョンとは?
リビジョンに対してタグを付与することができます。タグを付与したリビジョンは、通常のデプロイ先のURLとは別に、タグが付与された専用URLが発行されます。
プルリクエスト作成時にタグ付きリビジョンを作成し専用URLを発行することで、そのURLにアクセスするだけで素早く動作確認することができるようになります。

4. 目指す仕組み

今回は、以下のようにプレビュー環境の立ち上げや削除の仕組みを作成します。検証のため、mainブランチではなくdevelopブランチを通常デプロイの対象としています。

  1. developブランチ向けにプルリクエストを作成した時
    • タグ付きリビジョンをデプロイし、プレビュー環境を立ち上げる
  2. developブランチ向けのプルリクエストをマージした時
    • タグ付きリビジョンのタグを除去し、プレビュー環境のURLを無効化する
    • developブランチにマージされた内容を、通常のデプロイ先にデプロイする
  3. developブランチ向けのプルリクエストをクローズした時
    • タグ付きリビジョンのタグを除去し、プレビュー環境のURLを無効化する

5. 構築手順

以下の手順で進めます。

  1. 前提
  2. 準備
    • デプロイ用サービスアカウント作成
    • 権限の付与
    • サービスアカウントキーの生成
  3. GitHub Actionsワークフローの作成
    • 通常のデプロイを行うワークフロー
    • プレビュー環境を立ち上げるワークフロー
    • プレビュー環境の削除を行うワークフロー

1) 前提

以下を前提として進めます。

  • Google Cloudのプロジェクトがあること
  • Google Cloudプロジェクトの以下のAPIが有効になっていること
    • Cloud Run Admin API
    • Cloud Build API
    • Artifact Registry API
  • GitHub上にリポジトリがあること
    • 今回は検証用にNext.jsプロジェクトを作成しました。Cloud Runを使用しますが、コンテナは使わずソースコードから直接デプロイします。

【注意点】認証方法について

GitHub ActionsからCloud Runの操作をするために認証が必要です。認証の方法として、主にWorkload Identityとサービスアカウントキーの2パターンあります。
Workload Identityでは、キー発行が不要でトークンを使った認証ができます。サービスアカウントキーは従来のキーによる認証です。

今回は検証目的のため、簡易的に試せるサービスアカウントキーを採用しました。サービスアカウントキーは、キーの管理コストや漏洩リスクがあるため、実運用では Workload Identity Federation の利用を推奨します。

2) 準備

デプロイ用サービスアカウント作成

環境変数を設定します。PROJECT_IDの値は自分のプロジェクトに合わせて変更してください。

PROJECT_ID=sample-project
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')

デプロイ用のサービスアカウント(DEPLOYER_SA)を作成します。

gcloud iam service-accounts create gha-deployer \
  --display-name="GitHub Actions deployer"
DEPLOYER_SA="gha-deployer@$PROJECT_ID.iam.gserviceaccount.com"

権限の付与

この後の作業に必要な権限を追加します。権限追加ができない場合は、該当のGoogle Cloudプロジェクトの権限を持っている方に依頼して付与してもらってください。

# 作成したデプロイ用サービスアカウントに、Cloud Run へ「ソースからデプロイ」するために必要な権限を付与
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$DEPLOYER_SA" \
  --role="roles/run.sourceDeveloper"

# デプロイ用サービスアカウントが、有効化済みの各種 Google API を利用できるようにする基本ロール
gcloud projects add-iam-policy-binding $PROJECT_ID --member="serviceAccount:$DEPLOYER_SA" --role="roles/serviceusage.serviceUsageConsumer"

# 実行用サービスアカウントは、今回は「プロジェクトのデフォルト実行サービスアカウント」を利用する
RUNTIME_SA="$PROJECT_NUMBER-compute@developer.gserviceaccount.com"

# デプロイ用サービスアカウントが、実行用サービスアカウントを Cloud Run サービスに紐づけられるように委任
gcloud iam service-accounts add-iam-policy-binding "$RUNTIME_SA" \
  --member="serviceAccount:$DEPLOYER_SA" \
  --role="roles/iam.serviceAccountUser"

# Cloud Build のデフォルトサービスアカウントに、 Cloud Run へ「ソースからデプロイ」するために必要な権限を付与
CLOUD_BUILD_SA="$PROJECT_NUMBER@cloudbuild.gserviceaccount.com"
gcloud projects add-iam-policy-binding $PROJECT_ID --member="serviceAccount:$CLOUD_BUILD_SA" --role="roles/run.builder"

サービスアカウントキーの生成

デプロイ用サービスアカウントに紐づく、サービスアカウントキーを生成します。検証用リポジトリ内で以下のコマンドを実行してください。

gcloud iam service-accounts keys create gha-deployer-key.json \
  --iam-account "$DEPLOYER_SA"

コマンドを実行すると、リポジトリ内にgha-deployer-key.jsonが生成されます。こちらのファイルは絶対にGitHubにpushしないようにしてください。

GitHub Secretsに作成したキーを登録してください。

Name:GCP_SA_KEY
Secret:gha-deployer-key.json の中身を丸ごと貼り付け

登録したら、gha-deployer-key.jsonは削除することを推奨します。

3) GitHub Actionsワークフローの作成

ワークフローを3つ作成します。

通常のデプロイを行うワークフロー

初回デプロイを含む、通常のデプロイ用ワークフローです。Cloud Runサービスがまだなければ作成されるため、こちらのワークフローで初回デプロイもできます。

developブランチ向けのプルリクエストがマージされた時や、直接developブランチにpushされた時にこちらのワークフローが実行され、通常のデプロイ先にデプロイされます。

ポイント:
こちらのワークフローのデプロイによって作成された新しいリビジョンに確実にトラフィックを流すため、revision_traffic: 'LATEST=100'としています。

.github/workflows/deploy.yml
name: Deploy to Cloud Run

on:
  push:
    branches: [develop]  # developブランチへのpush時に実行

permissions:
  contents: read

env:  # 自分のプロジェクトに合わせて変更してください
  PROJECT_ID: sample-project
  REGION: asia-northeast1
  SERVICE: sample-service

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # サービスアカウントキーでGoogle Cloudに認証
      - name: Auth to Google Cloud (via SA key)
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}

      # ソースコードから直接Cloud Runにデプロイ
      - name: Deploy (from source) to Cloud Run
        id: deploy
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          project_id: ${{ env.PROJECT_ID }}
          region: ${{ env.REGION }}
          service: ${{ env.SERVICE }}
          source: .
          # 新しいリビジョンに100%トラフィックを流す(通常のデプロイ)
          revision_traffic: 'LATEST=100'

プレビュー環境を立ち上げるワークフロー

developブランチ向けにプルリクエストを作成した時に、タグ付きリビジョンを作成し専用URLを発行するワークフローです。

ポイント:

  • プレビュー環境にはトラフィックを流さないようにno_traffic: trueとしています。
  • プレビュー環境のURLをプルリクエストにコメントする処理も含まれています。
.github/workflows/preview.yml
name: Preview on PR

on:
  pull_request:
    branches: [develop]
    # PRの作成、更新、再オープン、レビュー準備完了時に実行
    types: [opened, synchronize, reopened, ready_for_review]

permissions:
  contents: read
  pull-requests: write  # PRにコメントするための権限

env:  # 自分のプロジェクトに合わせて変更してください
  PROJECT_ID: sample-project
  REGION: asia-northeast1
  SERVICE: sample-service

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # サービスアカウントキーでGoogle Cloudに認証
      - name: Auth to Google Cloud (via SA key)
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}

      # gcloudコマンドのセットアップ
      - name: Setup gcloud
        uses: google-github-actions/setup-gcloud@v2
        with:
          project_id: ${{ env.PROJECT_ID }}

      # プレビュー環境用のタグ付きリビジョンをデプロイ
      - name: Deploy preview
        id: deploy
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          project_id: ${{ env.PROJECT_ID }}
          region: ${{ env.REGION }}
          service: ${{ env.SERVICE }}
          source: .
          wait: true  # デプロイ完了を待つ
          # トラフィックを流さない
          no_traffic: true
          # PR番号をタグとして付与(例: pr-123)
          tag: pr-${{ github.event.number }}

      # jqコマンドをインストール(JSONパースに使用)
      - name: Install jq
        run: sudo apt-get update && sudo apt-get install -y jq

      # タグ付きリビジョンのURLを取得
      - name: Resolve tag URL
        id: tagurl
        env:
          REGION: ${{ env.REGION }}
          SERVICE: ${{ env.SERVICE }}
          TAG: pr-${{ github.event.number }}
        run: |
          json=$(gcloud run services describe "$SERVICE" --region "$REGION" --format=json)
          url=$(echo "$json" | jq -r --arg t "$TAG" '.status.traffic[]? | select(.tag==$t) | .url' | head -n1)
          echo "url=$url" >> "$GITHUB_OUTPUT"

      # プレビュー環境のURLをPRにコメント
      - name: Comment preview URL on PR
        if: success()
        uses: actions/github-script@v7
        with:
          script: |
            const url = `${{ steps.tagurl.outputs.url }}`
            const body = url ? `Preview: ${url}` : 'Preview URL not found.'
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body,
            })

プレビュー環境の削除を行うワークフロー

プルリクエストがマージやクローズされた時にはプレビュー環境を削除する必要があります。そのためのワークフローです。

注意点:
今回はタグを外す処理のみ実装し、リビジョン自体の削除処理は実装していません。 タグを外すとURLは無効化され、リクエスト処理による課金は発生しません。ストレージコストは微々たるものですが完全にゼロではないため、不要になったリビジョンは手動で削除してください。
実運用では、プレビュー環境のリビジョン自体を毎回削除するか、定期的に使っていないリビジョンを削除する処理を作成するのが良さそうです。

.github/workflows/preview-cleanup.yml
name: Cleanup PR preview

on:
  pull_request:
    branches: [develop]
    types: [closed]  # PRがマージまたはクローズされた時に実行

env:  # 自分のプロジェクトに合わせて変更してください
  PROJECT_ID: sample-project
  REGION: asia-northeast1
  SERVICE: sample-service

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      # サービスアカウントキーでGoogle Cloudに認証
      - name: Auth to Google Cloud (via SA key)
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}

      # gcloudコマンドのセットアップ
      - name: Setup gcloud
        uses: google-github-actions/setup-gcloud@v2
        with:
          project_id: ${{ env.PROJECT_ID }}

      # タグを削除してプレビュー環境のURLを無効化
      - name: Remove preview tag
        env:
          REGION: ${{ env.REGION }}
          SERVICE: ${{ env.SERVICE }}
          TAG: pr-${{ github.event.number }}
        run: |
          if gcloud run services update-traffic "$SERVICE" --region "$REGION" --remove-tags "$TAG"; then
            echo "Removed tag: $TAG"
          else
            echo "Tag not found or already removed: $TAG"
          fi

6. 動作確認

初回デプロイ

今回は簡易的に、3つのワークフローファイルをdevelopブランチに追加し、直接pushします。(developブランチ向けにプルリクエストを作成し、マージすることでも初回デプロイは可能です。)
developブランチへのpushで、1つ目のワークフローファイルdeploy.ymlが実行されて、Cloud Runにデプロイされるはずです。

Cloud Runのコンソールを確認してみましょう。

コンソールに表示されているURLにアクセスすると、トップ画面が表示されました。

プレビュー環境立ち上げ

developブランチから作業ブランチを切り、適当な変更を加えて、プルリクエストを作成してみましょう。

プルリクエストにプレビュー環境のURLがコメントされました。クリックすると、変更が反映されたトップ画面が表示されました🎉

Cloud Runコンソール上で、タグ付きリビジョンがデプロイされていることが確認できます。

通常のデプロイ先のURLにアクセスしてみると、こちらには変更が反映されていませんね。

プレビュー環境クローズ

先ほどのプルリクエストをマージすると、プレビュー環境がクローズし、通常デプロイ先に変更が適用されることを確認していきます。

先ほどのプレビュー環境のURLにはアクセスできなくなっています。

Cloud Runのコンソールを確認するとタグが外れて無効化されていることと、新しいリビジョンがデプロイされていることを確認できました。

新しいリビジョンがデプロイされ、通常デプロイ先に変更が反映されています。

7. 参考記事

プルリクエストをトリガとするCloud Runのプレビュー環境自動デプロイを実装してみた

dotD Tech Blog

Discussion