以下の内容はhttps://tech.newmo.me/entry/2025/04/11/165239より取得しました。


Cloud Service Mesh for Cloud Run で実現する PR 環境

この記事では、Cloud Service Mesh for Cloud Run を利用して PR 環境を構築する方法について紹介します。

背景・概要

newmo ではトランクベース開発を行なっているため、開発環境での動作確認は main branch (trunk) に merge されていることが前提になっています。

そのため現状では、手軽に開発環境で API の動作確認ができなかったり、動作検証が十分でないコードが main branch に merge されてしまう課題があります。CI での test 実行などにより一定品質は担保していますが 、PR 環境 (GitHub の Pull Request ごとに用意される一時的な環境) で QA ができれば問題発見のタイミングを前にずらすことができます。

PR 環境の要件は以下の通りです。

  • 機能追加を行なった PR が実際に Cloud Run Service としてデプロイされ、クライアントアプリからのトラフィックを受け付ける状態になっていること。
  • クライアントアプリはもちろん、その他関連するコンポーネントのコードに手を入れずに、接続する PR 環境を変更できること。
  • PR 環境へのアクセスは HTTP ヘッダ (e.g. pr-number: 2000) のみによりコントロールすること。

本記事の内容は静的コンテンツを配信する Web サーバーなどにも流用できますが、今回は API サーバーに限定してお話しします。

Cloud Service Mesh for Cloud Run とは

Cloud Service Mesh は、Google Cloud が提供する フルマネージドなサービスメッシュ機能です。主に Google Kubernetes Engine で利用されてきましたが、Cloud Run でも利用できるようになりました。(2025/4/8 時点では Preview 機能)

サービスメッシュを導入することで一般的には以下のことが可能になります。

  • サービス間通信の可視化
  • トラフィックの高度なルーティング
    • e.g. Canary リリース、Blue/Green デプロイメントを支える L7 でのルーティング
  • mTLS による通信のセキュリティ強化
  • リトライ・タイムアウト・サーキットブレイカーなどの仕組みによる通信の信頼性向上

Cloud Service Mesh for Cloud Run は現時点でこの全てに対応しているわけではありませんが、今回必要な「高度なルーティング機能」には対応しています。

そのため Cloud Service Mesh for Cloud Run を利用して PR 環境を構築することにしました。

アーキテクチャ

Cloud Service Mesh for Cloud Run は、以下の主要なコンポーネントで構成されています。(公式ドキュメントにプロビジョニングの手順が記載されているのでここでは詳細は割愛します。)

  • Cloud Service Mesh
    • Google Cloud が提供するフルマネージドなトラフィック管理サービス。 (=サービスメッシュのコントロールプレーン)
    • Mesh Client に sidecar として Envoy proxy を注入する。
  • Mesh Client
    • 本記事では、サービスメッシュにクライアントとして参加している Cloud Run のことを Mesh Client と定義します。
    • 下図で言うところの、GraphQL Federation にあたります。
      • GraphQL Federation から PR 環境は単一のホストに見えており、ただヘッダを伝播してリクエストしているだけ。
      • (余談ですが、newmo では GraphQL Federation を採用しています。@mrno110 さんの記事も是非ご覧ください。)
  • Mesh Server
    • 本記事では、サービスメッシュにサーバーとして参加している Cloud Run のことを Mesh Server と定義します。
    • 本記事のメイントピックである PR 環境がこの Mesh Server にあたります。
    • Mesh Server には、さらに以下のリソースが必要です。
      • Backend Service
      • Serverless Network Endpoint Group
  • HTTP Route
    • 特定のホスト名に対して、トラフィックを Mesh Server にルーティングするためのリソースです。
      • Istio の VirtualService に相当。
    • 様々なルーティングのルールを定義することができますが、本記事では HTTP Header によるルーティングを行います。
  • Cloud DNS
    • HTTP Route で使用するホスト名を解決するための Private な DNS です。
    • A レコードの値は RFC 1918 の Private IP アドレスであれば何でも良いです。

PR 環境の構築

PR 環境を新たに構築するには以下の手順が必要です。

  1. PR 環境用の Cloud Run Service を新たに作成する。
  2. Serverless Network Endpoint Group を作成する。
  3. Backend Service を作成し、Serverless Network Endpoint Group をアタッチする。
  4. HTTP Route に新たにルールを追加する。

PR 環境を削除するには逆に以下の手順が必要です。

  1. HTTP Route からルールを削除する。
  2. Backend Service を削除する。
  3. Serverless Network Endpoint Group を削除する。
  4. PR 環境用の Cloud Run Service を削除する。

エラーハンドリングなどを考えると、この手順は GitHub Actions 内で gcloud コマンドで行うには煩雑であるため、Go の SDK を利用した CLI を謹製しました。

また、CI の workflow をシンプルにするため、CLI は冪等性を持つような設計にしました。(冪等性がないと CI 側で gcloud コマンドを実行して存在確認する必要がある。テストもしにくい。)

以下は CLI のインターフェースのイメージです。

# 以下で作成
./newmo-mesh create --pr <PR number> --app <cloud run service name>

# 以下で削除
./newmo-mesh delete --pr <PR number> --app <cloud run service name>

CI ベース (Push 型)

CI ベースでは、GitHub Actions の workflow を利用して PR 環境を構築します。

PR 環境のライフサイクルは以下のように設計しました。

  • PR に特定のラベルが付与された場合に PR 環境を構築する。
  • 特定のラベルがついた PR に更新の commit があった場合に Cloud Run を更新する。
    • Mesh 関連のリソースは更新しない。
  • 特定のラベルがついた PR が merge/close された場合に PR 環境を削除する。

このライフサイクルを実現するために、GitHub Actions の workflow を以下のように記述します。

.github/workflows/create-pr-environment.yaml (一部抜粋)

name: Create PR Environment
on:
  pull_request:
    types:
      - labeled
      - synchronize
...
jobs:
  create-pr-env:
    steps:
      - ...
      - name: Check whether a PR environment needs to be created
        id: set-result
        env:
          EVENT_TYPE: ${{ github.event.action }}
          LABEL_NAME: ${{ github.event.label.name }}
          LABELS_JSON: ${{ toJSON(github.event.pull_request.labels) }}
        run: |
          deploy_pr_env="false"

          # label が付与された場合、そのラベルが特定のラベル名に合致するか
          if [[ "${EVENT_TYPE}" == "labeled" ]]; then
            if [[ "${LABEL_NAME}" == "<特定のラベル名>" ]]; then
              echo "This PR is labeled for PR Environment deployment."
              deploy_pr_env="true"
            fi

          # 更新の commit があった場合、特定のラベルが既に付与されているか
          elif [[ "${EVENT_TYPE}" == "synchronize" ]]; then
            echo "PR labels: ${LABELS_JSON}"
            if echo "${LABELS_JSON}" | grep -E -q '"name": ?"<特定のラベル名>"'; then
              echo "This PR is labeled for PR Environment deployment."
              deploy_pr_env="true"
            fi
          else
            echo "Unsupported event type: ${EVENT_TYPE}"
          fi

          echo "deploy_pr_env=${deploy_pr_env}" >> "$GITHUB_OUTPUT"
      
      # docker build と Artifact Registry への push
      - ...

      # gojq を用いて、以下の変更を行なった YAML を生成
      # (1) .metadata.name を <cloud run service name> + "-" + <PR 番号> に変更
      # (2) .spec.template.spec.containers[0].image を Artifact Registry のイメージに変更
      - ...

      # Cloud Run にデプロイ
      - name: Deploy to Cloud Run
        run: |
          gcloud run services replace ".../tmp-service.yaml"

      # Cloud Service Mesh に必要なリソースの作成
      - name: Create PR Environment
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          APP: <cloud run service name>
        run: |
          ./newmo-mesh create --pr "${PR_NUMBER}" --app "${APP}"

.github/workflows/delete-pr-environment.yaml (一部抜粋)

name: Delete PR Environment
on:
  pull_request:
    types:
      - closed
...
jobs:
  delete-pr-env:
    steps:
      - ...
      # Cloud Service Mesh に必要なリソースの削除
      - name: Delete PR Environment
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          APP: <cloud run service name>
        run: |
          ./newmo-mesh delete --pr "${PR_NUMBER}" --app "${APP}"

      # Cloud Run の削除
      - name: Delete Cloud Run Service
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          APP: <cloud run service name>
        run: |
          service_name="${APP}-${PR_NUMBER}"
          
          if gcloud run services describe "${service_name}" > /dev/null 2>&1; then
            echo "Service ${service_name} exists. Deleting..."
            gcloud run services delete "${service_name}" --quiet
          else
            echo "Service ${service_name} does not exist."
            exit 0
          fi

CI により、単一の HTTP Route リソースを参照しルールの書き換えが行われます。そのため、これらの workflow の同時実行数は1にしておくのが良いです。(Control the concurrency of workflows and jobs)

Reconcile ベース (Pull 型)

こちらは GitOps に着想を得たアプローチで、定期的に実行されるジョブが Observe → Analyze → Act し、actual state (実際の状態、つまり Cloud Run としてデプロイされている PR 環境一覧) を desired state (あるべき状態、つまり GitHub 上で label がつけられた open な PR 一覧) に近づけるアプローチです。

この Reconciler のシーケンス図を書いた時点で、非常に複雑で要件に対して too much になることが判明したため newmo では CI ベースのアプローチを採用しました。

ハマりポイント

構築の中でいくつかハマったポイントがあったので、備忘録として残しておきます。

※ 以下の内容は、2025/4/8 時点での情報です。今後変更される可能性があります。

※ 公式ドキュメントに記載されていない内容もあるので、あくまで参考程度にしてください。newmo では開発環境にのみ適用していますが、この仕組みを本番環境に適用する際は十分に検証された上で適用してください。

YAML で Cloud Run を定義している場合

Cloud Run は serving.knative.dev/v1 の Service として YAML で定義することができます。(参考)

newmo では YAML で Cloud Run を定義していますが、YAML で Mesh Client として参加させるための方法が公式ドキュメントには未だ記載されていません。

そこでリバースエンジニアリング的に、実際に Console 画面から Mesh Client として参加させ、YAML の変更を見てみると以下の追加がありました。

spec:
  template:
    metadata:
      annotations:
        run.googleapis.com/mesh: "projects/<project-id>/locations/global/meshes/<mesh-name>"
        run.googleapis.com/mesh-dataplane: sidecar

逆にこの annotation を付与した状態で gcloud run services replace を実行すると、無事 Mesh Client として参加させることができました。

Shared VPC を利用している

Cloud Run をデプロイするプロジェクトで Shared VPC を利用している場合、以下のようにプロビジョニングするとうまくいきます。

項目 Host / Service
Cloud DNS Host
Cloud Service Mesh Service
HTTP Route Service
Backend Service Service
Serverless NEG Service
Cloud Run Service

今後の展望

PR 環境をさらに良いものにするためには、以下の機能が必要です。

  • Database のブランチ対応
    • PR が DB スキーマの変更を含む場合、現状の PR 環境の構成ではワークしない可能性があります。
    • そのため、メインの Database はそのままで PR 環境用の別の Database を一時的に作って新しいスキーマを migration して利用するような仕組みが必要です。(Neon など)
  • Pub/Sub や外部サービスとの連携
    • Pub/Sub を利用している PR 環境では、発行したメッセージを同じ PR 環境で受ける必要があります。
    • 同様に PR 環境から外部サービスへリクエストした際の callback のレスポンスを PR 環境で受ける必要があります。
  • Load Balancer と直接接続した PR 環境
  • Observability
    • PR 環境ごとのトラフィックの数やエラーレート、レイテンシなどを可視化。

Cloud Service Mesh for Cloud Run の今後のアップデートにも非常に期待しています。

+α: Envoy の xDS configuration と Cloud Service Mesh の debug

Cloud Service Mesh は詳細が隠蔽されており、複雑な仕組みになっているので通信できなかった時の debug が難しいです。

csds-client というツールを利用することで、

  • Envoy proxy が注入されているか
  • Envoy proxy が Cloud Service Mesh から xDS configuration を取得できているか
  • xDS configuration に正しい xDS が定義されているか

などを確認することができます。(参考)

例えば、これまで構築してきた Mesh Server に関する xDS configuration は以下のように確認できます。

  1. Route の確認

typeUrl = type.googleapis.com/envoy.config.route.v3.RouteConfiguration で、Private DNS に定義したドメイン名の route が定義されています。

  1. Cluster の確認

1で確認した RDS の routes[].route.wightedClusters.clusters[].name に、その route に対応する cluster が定義されています。

もちろん対応する Cluster の定義も存在し、かつ transportSocket も自動的に設定されているはずです。

これにより、Mesh Client のアプリケーションで http://<private-dns-name> でリクエストを投げたとしても、Envoy proxy が TLS origination を行い、HTTPS で Mesh Server にリクエストを投げていることがわかります。

書いた人: tobi




以上の内容はhttps://tech.newmo.me/entry/2025/04/11/165239より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14