🔖

Cloud Run で Pull Request 毎の確認環境を作ろう

2023/12/06に公開

この記事は Google Cloud Advent Calendar 2023 (通常版) の 12/6 の記事です。

Cloud Run はコンテナ アプリをサーバーレス で実行するためのプロダクトですが、統合されている機能を利用せずに、単にアプリ基盤として利用するだけではもったいありません。今回は開発用途、また CI/CD を応用する例を考えてみたいと思います。

要約

ソースコード管理として GitHub を利用し、CI/CD 経由で Cloud Run へアプリをデプロイするフローを応用して、Pull Request 毎にアプリを確認できる URL を作成するフローを作ってみたいと思います。GitHub 以外でも可能ですが、今回の例では GitHub Actions を一部利用しています。

全体のアーキテクチャは以下のようになります。

サンプル コードはこちらにあります。
https://github.com/tyorikan/cloud-run-tag-dev-example

  • gcloud run services update は既存サービスが存在しないと失敗します
  • サービスアカウント作成や権限付与含め、Cloud Run サービスの事前作成などを IaC やコンソール操作で行っている前提のサンプルになります。

Git リポジトリのイベントをトリガー

実現のために、この例では 3 つのトリガーを仕込んで Cloud Build を起動しています(3 は GitHub Actions からのトリガー)。

1. Pull Request 時

通常の開発フローでは、機能開発をする際にブランチを切って trunk(main ブランチと同義なので以後 main で記載)に対して PR を送るでしょう。
https://dora.dev/devops-capabilities/technical/trunk-based-development/

今回の例では PR イベントをトリガーとして、Cloud Run にデプロイします。Cloud Build に設定できるイベントとして PR が存在しているので、そちらを利用します。

デプロイ時は --no-traffic オプションをつけて、エンドポイントからのトラフィックは該当リビジョンに到達させず、ブランチ名を Cloud Run のタグとして設定することで、タグのエンドポイントからのみアクセスできる状態にします。

Cloud Build 構成ファイルの該当部は以下のようになります。

  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
    args:
      - run
      - services
      - update
      - $_SERVICE_NAME
      - '--platform=managed'
      - >-
        --image=$_AR_HOSTNAME/$PROJECT_ID/cloud-run-source-deploy/$REPO_NAME/$_SERVICE_NAME:$COMMIT_SHA
      - >-
        --labels=managed-by=gcp-cloud-build-deploy-cloud-run,commit-sha=$COMMIT_SHA,gcb-build-id=$BUILD_ID,gcb-trigger-id=$_TRIGGER_ID
      - '--region=$_DEPLOY_REGION'
      - >-
        --tag=$BRANCH_NAME
      - '--quiet'
      - '--no-traffic'
    id: DeployTag
    entrypoint: gcloud

2. main ブランチへ Merge(Commit)時

feature ブランチの確認を終えたら、main ブランチへマージされ、Cloud Run の開発用サービスにデプロイします。
単にデプロイするのであれば、Cloud Build から Cloud Run にデプロイするだけで完了しますが、Cloud Deploy という CD 特化のプロダクトがあるので、それを使うとより便利です。

  • Cloud Run の Traffic Split を利用した Canary release / rollback が容易

Cloud Run にはリビジョン単位でトラフィックの割合を設定可能なため、この機能を利用して最新アプリはまず 10 % などでデプロイし、徐々に割合を増やす、Canary release というデプロイ手法があります。

Cloud Deploy では「開発環境は 0 to 100% で Blue/Green デプロイ」だけど「本番環境は 25%, 50% と徐々に割合を上げて、問題なさそうなら 100%」という感じで、環境毎にデプロイ方式を柔軟に設定できます。

また、リリース時に何か問題を発見した場合に、稼働していた以前のリビジョンに rollback も可能なため、Cloud Run へのデプロイと非常に相性が良いプロダクトです。

  • 開発環境からステージング、本番という流れで昇格(プロモート)が可能

開発環境で確認したものをステージング、ステージングで確認後本番へ、といったデプロイ フローが一般的かと思いますが、各環境毎にビルドやテストを実行するのは得策ではありません。
一連のテストやビルド、脆弱性スキャンが通ったものを、全環境へ順にデプロイするのが、環境差異をなくし、効率良くデプロイするフローといえるでしょう。

Cloud Deploy は、開発から本番まで一連のリリースを Release という単位でまとめて作成します。どのように Release を作成するかは、以下のように DeliveryPipeline の定義を参照します。

apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
  name: demo-backend-api
description: demo backend api pipeline
serialPipeline:
  stages:
  - targetId: demo-backend-api
    profiles: [dev]
  - targetId: demo-backend-api-prod
    profiles: [prod]
    strategy:
      canary:
        runtimeConfig:
          cloudRun:
            automaticTrafficControl: true
        canaryDeployment:
          percentages: [25, 50]
          verify: false
---

apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
  name: demo-backend-api
description: Cloud Run development service
run:
  location: projects/{PROJECT_ID}/locations/{REGION}
---

apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
  name: demo-backend-api-prod
description: Cloud Run production service
run:
  location: projects/{PROJECT_ID}/locations/{REGION}

3. ブランチ削除時

必要なくなった Cloud Run のタグは削除しておいたほうが無難です。特に、最小インスタンス数を 1 以上に設定している場合、アイドル状態のコストがかかる点に注意してください。
この例では、ブランチの削除をトリガーに、GitHub Action から Cloud Build を呼び出しています。

name: Trigger Cloud Build on branch deletion

on:
  delete:
    branches:
      - '**'

permissions:
  id-token: write

jobs:
  trigger:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Workload Identity Federation
        uses: google-github-actions/auth@v0.4.0
        with:
          workload_identity_provider: projects/${{ vars.GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/${{ vars.WORKLOAD_IDENTITY_POOL }}/providers/${{ vars.WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ vars.GCP_SA_ID }}

      - name: Trigger Cloud Build
        run: |
          gcloud builds triggers run ${{ vars.CLOUD_BUILD_TRIGGER_NAME }} --region=${{ vars.CLOUD_BUILD_REGION }} --substitutions=_BRANCH_NAME="${{ github.event.ref }}"

Cloud Build 側では削除を実行しているだけです。

  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
    args:
      - run
      - services
      - update-traffic
      - $_SERVICE_NAME
      - '--platform=managed'
      - '--region=$_DEPLOY_REGION'
      - '--remove-tags=$_BRANCH_NAME'
      - '--quiet'
    id: RemoveCloudRunTag
    entrypoint: gcloud

実行は単純ですが、以下の疑問を持つかもしれません。

  1. GitHub から Cloud Build をどうやって呼び出してるの?
    GitHub Actions の yaml には鍵情報は入っていませんが、workload_identity_provider という項目があります。これは、特定のリポジトリや特定の GitHub ユーザーに絞って、Google Cloud のサービス アカウント権限を借用させる、Workload identity federation という機能によるものです。鍵の漏洩を気にすることなく、セキュアに連携が可能です。
    詳しくは以下のブログを参照してください。

https://cloud.google.com/blog/ja/products/identity-security/enabling-keyless-authentication-from-github-actions

  1. 何で GitHub Actions で完結させないの?
    GitHub Actions で Cloud Run のタグ削除を実行すればシンプルです。それで問題があるわけではないですが、以下のようにサービス アカウントの権限を分離しています。
  • GitHub Actions で借用しているサービス アカウントの権限に「Cloud Build 編集者」を付与
  • Cloud Build 実行で利用しているサービス アカウントの権限に「Cloud Run 管理者」を付与

GitHub Actions ができるのは Cloud Build の実行に留めたかったため、このような方式にしています。

まとめ

Cloud Run は開発環境や本番環境で利用されるケースが増えていますが、開発中の用途には Cloud Run を利用していない、Cloud Deploy を利用していないという方も多いでしょう。CI/CD パイプラインを整備することで、開発速度やシステムの安定性が向上します。

Cloud Run は、開発速度や運用コストの向上に効果的です。さらに、Canary release などの手法を取り入れることで、機能を最大限に活かすことができます。ぜひ、ご検討ください。

Google Cloud Japan

Discussion