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

記事タイトルとURLをコピーする

G-gen の藤岡です。当記事では Google Cloud(旧称 GCP)で Cloud Run のタグ付きリビジョン(tagged revision)機能を使い、GitHub のプルリクエストをトリガとしたプレビュー環境の自動デプロイを実装する方法を紹介します。

概要

Netlify や Vercel などのホスティングサービスでは「Deploy Previews」と呼ばれる GitHub と連携してプルリクエストをトリガとして自動でプレビュー環境をデプロイする機能が提供されています。 Google Cloud のホスティングサービスである Firebase Hosting でも同様にプルリクエストごとにプレビュー環境を自動で作ることができます。

今回は、プルリクエストをトリガにして Cloud Run サービスの「タグ付きリビジョン」をデプロイすることで、本番環境へデプロイ前に試験や動作確認をできるようにします。 構成としては以下の通りです。

アーキテクチャ

前提知識

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

Cloud Run は、デプロイされるリビジョンごとにタグを付与できます。タグを付与すると通常のデプロイ時に生成される URL とは別に、タグが付与された URL が割り当てられます。

# Cloud Run の通常の URL 例
https://<servicename>-xxxxxxxxxx-an.a.run.app
   
# タグ付きリビジョンの URL 例(リビジョンに dev タグを付与した場合)
https://dev---<servicename>-xxxxxxxxxx-an.a.run.app

そのため、タグ付きリビジョン URL へのトラフィックを 0% にしてデプロイすることで、本番環境へ影響することなく事前にフロントエンド等のテストをすることができます。

タグ付きリビジョンを使用した時のアクセスの流れ

Cloud Run のタグ付きリビジョンに関する詳細は、以下の記事をご参照ください。 blog.g-gen.co.jp

GitHub Actions と Workload Identity 連携

GitHub Actions では OIDC(OpenID Connect)トークンを使った認証がサポートされており、Workload Identity 連携と組み合わせることで従来のようにサービスアカウントキーを発行せずに Google Cloud へ認証が可能です。

GitHub Actions と Workload Identity 連携に関する詳細は、以下の記事をご参照ください。 blog.g-gen.co.jp

アーキテクチャ

これ以降では、以下の構成で検証をしていきます。

(再掲)アーキテクチャ

事前準備

前提

以下の環境および条件を前提とします。

ディレクトリ構成

ディレクトリ構成は以下の通りです。

.
├── README.md
└── web  # サンプルアプリ
    ├── go.mod
    └── main.go
.github
└── workflows  # GitHub Actions を定義
    ├── preview-workflow.yaml
    └── production-workflow.yaml

GitHub Actions のワークフローファイル

ワークフローファイルは以下の 2 つを用意しました。

# preview-workflow.yaml
name: Preview Workflow
  
on:
  pull_request:
    branches:
      - '**'
    types:
      - opened
      - synchronize
  
env:
  PROJECT_ID: "PROJECT_ID"
  SERVICE: "sample-app"
  REGION: "asia-northeast1"
  SERVICE_ACCOUNT: "workload@PROJECT_ID.iam.gserviceaccount.com"
  PROJECT_NUMBER: "PROJECT_NUMBER"
  WORKLOAD_IDENTITY_POOL_ID: "cloud-run-test"
  WORKLOAD_IDENTITY_PROVIDER_ID: "github-actions"
  
jobs:
  preview:
    runs-on: ubuntu-latest
  
    permissions:
      contents: read
      id-token: write
      pull-requests: write
  
    steps:
      - name: Checkout
        uses: actions/checkout@v4
  
      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v1
        with:
          workload_identity_provider: projects/${{ env.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/${{ env.WORKLOAD_IDENTITY_POOL_ID }}/providers/${{ env.WORKLOAD_IDENTITY_PROVIDER_ID }}
          service_account: ${{ env.SERVICE_ACCOUNT }}
  
      - name: Extract details for tag
        run: |
          echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c 1-4)" >> $GITHUB_ENV
          echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
  
      - name: Deploy to Cloud Run from Source
        id : deploy
        uses: google-github-actions/deploy-cloudrun@v1
        with:
          service: ${{ env.SERVICE }}
          region: ${{ env.REGION }}
          source: ./web
          tag: pr-${{ env.PR_NUMBER }}-commit-${{ env.SHORT_SHA }}
          flags: "--allow-unauthenticated --platform=managed --execution-environment=gen2"
          no_traffic: true
  
      - name: Get datetime for now
        run: echo "CURRENT_DATETIME=$(date)" >> $GITHUB_ENV
        env:
          TZ: Asia/Tokyo
  
      - name: Create comment
        uses: peter-evans/create-or-update-comment@v3
        with:
          comment-id: ${{ steps.find_comment.outputs.comment-id }}
          issue-number: ${{ github.event.pull_request.number }}
          body: |
            :tada: Successfully deployed preview revision for this PR (updated for commit ${{ github.event.pull_request.head.sha }}):
            <${{ steps.deploy.outputs.url }}>
            <sub>(:sparkles: updated at ${{ env.CURRENT_DATETIME }})</sub>
          edit-mode: replace
# production-workflow.yaml
name: Production Workflow
  
on:
  workflow_dispatch:
  pull_request:
    types: [ closed ]
  
env:
  PROJECT_ID: "PROJECT_ID"
  SERVICE: "sample-app"
  REGION: "asia-northeast1"
  SERVICE_ACCOUNT: "workload@PROJECT_ID.iam.gserviceaccount.com"
  PROJECT_NUMBER: "PROJECT_NUMBER"
  WORKLOAD_IDENTITY_POOL_ID: "cloud-run-test"
  WORKLOAD_IDENTITY_PROVIDER_ID: "github-actions"
  
jobs:
  deploy:
    runs-on: ubuntu-latest
  
    permissions:
      contents: read
      id-token: write
  
    steps:
      - name: Checkout
        uses: actions/checkout@v4
  
      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v1
        with:
          workload_identity_provider: projects/${{ env.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/${{ env.WORKLOAD_IDENTITY_POOL_ID }}/providers/${{ env.WORKLOAD_IDENTITY_PROVIDER_ID }}
          service_account: ${{ env.SERVICE_ACCOUNT }}
  
      - name: Check if service exists
        id: check-service
        run: |
          EXISTS=$(gcloud run services list --platform managed --region ${{ env.REGION }} | grep "${{ env.SERVICE }}" || true)
          if [[ -z "$EXISTS" ]]; then
            echo "Service does not exist, setting deploy flag."
            echo "DEPLOY_FLAG=true" >> $GITHUB_ENV
          else
            echo "Service already exists, skipping deploy."
            echo "DEPLOY_FLAG=false" >> $GITHUB_ENV
          fi
  
      - name: Deploy to Cloud Run from Source
        if: env.DEPLOY_FLAG == 'true'
        id : deploy
        uses: google-github-actions/deploy-cloudrun@v1
        with:
          service: ${{ env.SERVICE }}
          region: ${{ env.REGION }}
          source: ./web
          flags: "--allow-unauthenticated --platform=managed --execution-environment=gen2"
  
      - name: Extract details for tag
        if: env.DEPLOY_FLAG != 'true'
        run: |
          echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
          echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c 1-4)" >> $GITHUB_ENV
  
      - name: Remove revision with tag
        if: env.DEPLOY_FLAG != 'true'
        run: >
          gcloud run services update-traffic ${{ env.SERVICE }}
          --region ${{ env.REGION }}
          --remove-tags pr-${{ env.PR_NUMBER }}-commit-${{ env.SHORT_SHA }}
  
      - name: Add prod tag and Traffic routing
        if: env.DEPLOY_FLAG != 'true'
        run: >
          gcloud run services update-traffic ${{ env.SERVICE }}
          --region ${{ env.REGION }}
          --to-latest

デプロイとテスト

初回デプロイ

GitHub

初回は production-workflow.yaml のワークフローを手動実行し、Cloud Run へサンプルアプリをデプロイします。
トリガを workflow_dispatch とすることで、手動でワークフローの実行が可能になります。

# production-workflow.yaml
on:
  workflow_dispatch:
  pull_request:
    types: [ closed ]

GitHub のコンソールからは以下のように、ワークフローが手動で実行できる画面が表示されます。

初回デプロイ:GitHub

Run workflow をクリックすると、Cloud Run へサンプルアプリがデプロイされます。

Google Cloud

デプロイが完了すると、サンプルアプリの URL は https://sample-app-xxxx-an.a.run.app となっています。

初回デプロイ:Google Cloud

WEB 画面

ブラウザで URL へアクセスすると以下の画面が表示されます。

初回デプロイ:WEB 画面

プルリクエストの作成

GitHub

新しいブランチで main.go ファイルの中身を一部変更します。

# main.go
# 変更前
        if name == "" {
                name = "World"
        }
   
# 変更後
        if name == "" {
                name = "World!!!!!!!!"
        }

プルリクエストを作成すると、ワークフローがトリガされます。 ワークフローが完了すると、以下のようにプルリクエストにコメントと共にタグが付与された URL が表示されます。

プルリクエストの作成:GitHub

Google Cloud

ワークフローによってデプロイされた、タグ付きリビジョンのサービスがデプロイされました。

プルリクエストの作成:Google Cloud

WEB 画面

ブラウザから確認すると、タグが付与された URL には main.go の変更後が反映され、通常の URL は変更前のままです。

プルリクエストの作成:WEB 画面

プルリクエストをマージ

GitHub

プルリクエストをマージすると、ワークフローがトリガされます。

プルリクエストをマージ:GitHub

Google Cloud

ワークフローによって、タグが削除され、トラフィックが最新のデプロイに 100% 流れるようになっています。

プルリクエストをマージ:Google Cloud

WEB 画面

ブラウザから確認すると、タグが付与された URL は Error: Page not found となり、通常のサービス URL(https://sample-app-xxxx-an.a.run.app)へ変更が反映されています。

プルリクエストをマージ:WEB 画面

考慮事項

Google Cloud

  • Cloud Run のサービスアカウント
    Cloud Run のサービスアカウントはデフォルトの Compute Engine のサービスアカウントを使っているため、編集者(roles/editor) 権限が付与されています。最小限の権限の原則に従って適切な権限が付与されたサービスアカウントを使用してください。

  • ソースコードから Cloud Run をデプロイする時に作られるリソース
    ソースコードから Cloud Run をデプロイすると、Cloud Run 以外に以下のリソースが作られます。デプロイごとに容量が増えていくため、注意してください。

    • Cloud Storage:<PROJECT_ID>_cloudbuild> バケット(Cloud Build によってソースコードがアップロード)
    • Artifact Registry:cloud-run-source-deploy リポジトリ(Cloud Build によってコンテナ化されたイメージがアップロード)
  • Cloud Run のリビジョンタグ
    Cloud Run のタグが付与された URL にアクセスがある場合や最小インスタンスの設定によりコンテナが起動している場合、課金が発生します。 そのため、不要なリビジョンタグは削除すると意図しない課金が発生するリスクを軽減できます。

  • Cloud Run の認証
    Cloud Run サービスの公開(未認証)アクセスを許可するは、リビジョンではなくサービス自体に紐づいているため、本番環境のみ認証有り、プレビュー環境は認証無し、とすることは現状できません。

GitHub Actions

  • 環境変数
    今回は 2 つのワークフローで env として環境変数をファイル内で直接定義しています。共通する環境変数や機密情報を含むものは、GitHub Actions の変数シークレットを使用してください。

  • set-output ワークフローコマンドが廃止
    ワークフロー内のステップの出力値を設定し、後続のステップでその値を参照するために使われていた set-output ワークフローコマンドの廃止が発表されています。 当記事では使っていませんが、ワークフロー内で使っている場合は注意が必要です。

藤岡 里美 (記事一覧)

クラウドソリューション部

数年前までチキン売ったりドレスショップで働いてました!2022年9月 G-gen にジョイン。ハイキューの映画を4回は見に行きたい。

Google Cloud All Certifications Engineer / Google Cloud Partner Top Engineer 2024