【Claude Code 連載 第53回】Google Cloud にゲートウェイをデプロイする — 実装例で読む具体的な組み立て

スポンサーリンク
【Claude Code 連載 第53回】Google Cloud にゲートウェイをデプロイする — 実装例で読む具体的な組み立て 用語解説
【Claude Code 連載 第53回】Google Cloud にゲートウェイをデプロイする — 実装例で読む具体的な組み立て
この記事は約26分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

スポンサーリンク

背景

この連載では、Claude Codeの公式ドキュメントを1ページずつ読み解いていきます。公式ドキュメントは情報が網羅されている分、「結局どの機能を、どんな場面で使えばいいのか」は自分で考える必要があり、読むのに意外と時間がかかります。そこで、私が実務で使うために読み込んだ内容を「使う場面→実例コード」の順に整理して残していくことにしました。専門家の解説というより、一次情報を読んだ記録です。推測や動作を確認していない部分には、その都度そう書きます。

1. 一言でいうと何か

このページは、Google Cloud で Claude アプリゲートウェイを実際に動かすまでの、コマンド付きの実装例です。

第49〜52回はプラットフォーム非依存の説明でした。「TLS 終端プロキシの背後では public_url が必須」「trusted_proxies にプロキシの CIDR を設定する」——正しいのですが、では Cloud Run では具体的に何を書くのかは分かりません。このページはそこを埋めます。

最初に重要な但し書きがあります。これはサポートされた本番デプロイメントではなく、カスタマー管理インフラの実装例です。「各部分がどう組み合わさるかを確認してから、自分の環境に適応させてください」と明記されています。コピペで本番、という位置づけではありません。

構成する要素は7つ。

  • ゲートウェイコンテナを動かす Cloud Run サービスまたは GKE Deployment
  • イメージ用の Artifact Registry リポジトリ
  • プライベート IP のみの Cloud SQL for PostgreSQL インスタンス
  • gateway.yaml・JWT 署名キー・OIDC クライアントシークレット・Postgres URL 用の Secret Manager シークレット
  • roles/aiplatform.user を持つ サービスアカウント(Cloud Run に直接アタッチ、または GKE で Workload Identity 経由でバインド)
  • HTTPS 用の Internal Application Load Balancer(Cloud Run)または クラス gce-internal の内部 GKE Ingress

モデルのアップストリームは Google Cloud の Agent Platform、IdP の例は Google Workspace ですが、OIDC 準拠なら何でも動き、変わるのは oidc ブロックだけです。

そしてTerraform リファレンスと setup.sh が公式リポジトリで公開されている点も、このページの価値のひとつです。

2. どういう場面で役立つか

シーン1:GCP でゲートウェイを立てる

そのままの用途です。API の有効化からサインイン確認まで、gcloud コマンドが順に並んでいます。

シーン2:他クラウドで立てるときの「翻訳元」にする

AWS や Azure で構築する場合でも、「何と何を用意すればいいのか」の一覧としてこのページは使えます。プライベート IP の DB、シークレットストア、ワークロードアイデンティティ、内部ロードバランサー、コンテナレジストリ——構成要素の対応関係は他クラウドでもほぼ同じです。

シーン3:trusted_proxies に何を書くか確定させる

第50回で「プロキシの CIDR を書く」とだけ説明した項目に、具体的な値の表が与えられます。ここは自力で導くのが面倒な部分なので、実務価値が高い。

シーン4:GCP 特有の詰まりどころを事前に知る

Cloud Run の invoker IAM チェック、*.run.app がパブリック解決される問題、VPC ピアリングの非推移性、GKE Ingress の30秒タイムアウト——どれも知らずに踏むと原因究明に時間がかかる類のものです。

不要・向かないケース

  • そのまま本番に載せたい。前述のとおり実装例です。レビューと適応が前提。
  • 外部公開のクラス gce GKE Ingress を使いたい表にリストされていません。パブリックな転送ルールアドレスをプロビジョニングするため、/login のプライベートネットワークチェックが拒否します
  • Cloud Run をロードバランサーなしで、かつプライベート DNS 基盤なしで使いたい。**Cloud Run はプライベート解決可能なホスト名をプロビジョニングしません。**内部 ALB を立てるか、Private Service Connect + Cloud DNS プライベートゾーンの基盤が既にある必要があります。
  • Workload Identity クラスタでメタデータサーバーへのエグレスを塞ぎたい。塞げません(後述)。
  • Cloud SQL に VPC ピアリング経由で到達させたいピアリングは非推移的なので機能しません(後述)。

3. 構築手順とコマンド

前提条件

  • 課金が有効な GCP プロジェクトと、リソース作成権限
  • gcloud auth login 済みの gcloud CLI と、ローカルの Docker
  • GKE トラックなら kubectl と、後述の VPC 上に作った GKE クラスタ
  • Model Garden で必要な Claude モデルへのアクセス(それらが公開されている地域内)
  • リダイレクト URI が https://<gateway-host>/oauth/callback の Google Workspace OAuth 2.0 ウェブアプリケーションクライアント
  • ゲートウェイ用の TLS ホスト名(通常はロードバランサーを指すプライベート DNS 名

プロジェクトと地域を先に設定します。

export PROJECT_ID=<your-project>
export REGION=us-east5   # a region where the Claude models you need are published in Model Garden
gcloud config set project "$PROJECT_ID"

**地域の選び方が「必要な Claude モデルが Model Garden で公開されている地域」**である点に注意してください。好きなリージョンを選べるわけではありません。

ステップ1:API を有効にする

gcloud services enable \
  aiplatform.googleapis.com \
  artifactregistry.googleapis.com \
  sqladmin.googleapis.com \
  secretmanager.googleapis.com \
  iamcredentials.googleapis.com \
  iam.googleapis.com \
  compute.googleapis.com \
  servicenetworking.googleapis.com \
  run.googleapis.com \
  container.googleapis.com

全部が常に要るわけではありません。computeservicenetworking はプライベート IP Cloud SQL パス用、run は Cloud Run のみ、container は GKE のみです。

ステップ2:サービスアカウントと IAM

gcloud iam service-accounts create claude-gateway --display-name="Claude apps gateway"
SA="claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:${SA}" --role="roles/aiplatform.user" --condition=None

付与するのは roles/aiplatform.user だけです。理由が明快で——VPC 経由で Cloud SQL に到達するパスワードユーザーなので、Cloud SQL の IAM ロールは不要

このあと Model Garden でプロジェクト用に Claude モデルを有効化します。モデルは特定の地域に公開されるので、各モデルカードの確認が必要です。

ステップ3:イメージをビルドして Artifact Registry へ

gcloud artifacts repositories create claude-gateway \
  --repository-format=docker --location="$REGION"
gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
# Cloud Run requires linux/amd64. --provenance=false avoids a buildx OCI
# image index that Cloud Run rejects.
docker build --platform=linux/amd64 --provenance=false \
  -t "${REGION}-docker.pkg.dev/${PROJECT_ID}/claude-gateway/gateway:<version>" .
docker push "${REGION}-docker.pkg.dev/${PROJECT_ID}/claude-gateway/gateway:<version>"

コメントに書かれている2つのフラグが罠回避です。Cloud Run は linux/amd64 を要求し、--provenance=false は Cloud Run が拒否する buildx の OCI イメージインデックス生成を避けるためのもの。Apple Silicon の Mac でビルドすると確実に踏みます。

イメージ自体は第51回で扱ったコンテナイメージ要件(glibc ベース、書き込み可能な CLAUDE_CONFIG_DIRlinux-x64 バイナリ)に従います。

ステップ4:Cloud SQL をプライベート IP で作る

VPC=cc-gateway-vpc
gcloud compute networks create "$VPC" --subnet-mode=custom
gcloud compute networks subnets create cc-gateway-subnet \
  --network="$VPC" --region="$REGION" --range=10.0.0.0/24
# Private Services Access: one-time per VPC
gcloud compute addresses create "google-managed-services-${VPC}" \
  --global --purpose=VPC_PEERING --prefix-length=16 --network="$VPC"
gcloud services vpc-peerings connect \
  --service=servicenetworking.googleapis.com \
  --ranges="google-managed-services-${VPC}" --network="$VPC"
gcloud sql instances create claude-gateway-db \
  --database-version=POSTGRES_16 --tier=db-g1-small --region="$REGION" \
  --network="projects/${PROJECT_ID}/global/networks/${VPC}" --no-assign-ip
gcloud sql databases create claude_gateway --instance=claude-gateway-db
PGPASS="$(openssl rand -hex 24)"
gcloud sql users create gateway --instance=claude-gateway-db --password="$PGPASS"
PRIVATE_IP="$(gcloud sql instances describe claude-gateway-db \
  --format='value(ipAddresses[0].ipAddress)')"
GATEWAY_POSTGRES_URL="postgres://gateway:${PGPASS}@${PRIVATE_IP}:5432/claude_gateway?sslmode=require"

Private Services Access 経由で VPC 上に作り、パブリック IP を持たせない構成です。副次的な利点として、constraints/sql.restrictPublicIp が適用されているプロジェクトでも通ります

そして重要な制約:Cloud Run または GKE のランタイムは、この VPC 上にあるか、この VPC にルーティングされている必要があります。

ステップ5:gateway.yaml を書く

その前に、listen の2フィールドはゲートウェイの前段に何を置くかで決まるという説明があります。

public_url は Cloud Run / GKE Ingress の背後で必須。ゲートウェイは IdP の redirect_uri と検出ドキュメントをこの値からのみ組み立て、X-Forwarded-* ヘッダーからは決して組み立てません。

trusted_proxies はフロントエンドのソース範囲。ゲートウェイは TCP ピアがこのリストにある場合にのみ X-Forwarded-For を尊重し、信頼できるホップを超えてチェーンを辿るので、IP ごとのサインインレート制限と監査イベントがロードバランサーの IP ではなく開発者の IP を記録します。

具体値の表がこれです。

フロントエンドtrusted_proxies
ロードバランサーなしで直接到達する Cloud Run[169.254.0.0/16]
Cloud Run の前の Internal Application Load Balancer169.254.0.0/16 +プロキシのみサブネットの CIDR
GKE 内部 Ingress(クラス gce-internalプロキシのみサブネットの CIDR

クラス gce の外部 GKE Ingress がリストにないのは前述のとおり、パブリックアドレスになって /login に拒否されるからです。

以下は Cloud Run +内部ロードバランサーの例です。

listen:
  host: 0.0.0.0
  port: 8080
  public_url: https://claude-gateway.internal.example.com
  trusted_proxies: [169.254.0.0/16, <your-proxy-only-subnet-cidr>]
oidc:
  issuer: https://accounts.google.com
  client_id: <your-oauth-client-id>
  client_secret: ${OIDC_CLIENT_SECRET}           # GKE: ${file:/secrets/oidc-client-secret}
  allowed_email_domains: [example.com]
  # Google ignores offline_access; these yield refresh tokens:
  scopes: [openid, profile, email]
  extra_auth_params: { access_type: offline, prompt: consent }
session:
  jwt_secret: ${GATEWAY_JWT_SECRET}              # GKE: ${file:/secrets/jwt-secret}
store:
  postgres_url: ${GATEWAY_POSTGRES_URL}          # GKE: ${file:/secrets/postgres-url}
upstreams:
  - provider: vertex
    region: <your-region>                        # must match $REGION
    project_id: <your-project>
    auth: {} # ADC via the runtime service account

第51回で扱った Google の作法がそのまま現れています——offline_access を無視するので scopes から外し、extra_auth_paramsaccess_type: offlineprompt: consent でリフレッシュトークンを得る。コメントにも明記されています。

auth: {} は Application Default Credentials、つまりランタイムのサービスアカウントから認証することを意味します。

グループベースのポリシーを使いたい場合の注意も再掲されています。Google の id_tokengroups クレームを含まないので、oidc.google_groups(ドメイン全体の委任を持つサービスアカウントで Admin SDK Directory API を叩く)を設定するか、代わりに email_domain でマッチさせます

ステップ6:Secret Manager に4つ格納する

claude-gateway サービスアカウントに roles/secretmanager.secretAccessor を付与したうえで、次の4つを作ります。

シークレットソース
gateway-jwt-secretopenssl rand -base64 32
gateway-oidc-client-secretGoogle Cloud Console → OAuth クライアント
gateway-postgres-urlCloud SQL ステップの $GATEWAY_POSTGRES_URL
gateway-config前ステップの完全な gateway.yaml

シークレットの届き方がトラックによって違う点が実務上の分岐です。

  • GKE:Secret Manager CSI ドライバー経由で /secrets にマウントし、gateway.yaml${file:/secrets/...} を参照する。
  • Cloud Run複数のシークレットを1つのディレクトリにマウントできないため、gateway.yaml をファイルとしてマウントし、他の3つは環境変数として注入する。だから gateway.yaml 側は ${GATEWAY_JWT_SECRET} / ${OIDC_CLIENT_SECRET} / ${GATEWAY_POSTGRES_URL} を参照する。

上の YAML 例でコメントとして両方の書き方が併記されていたのは、この違いのためです。

ステップ7A:Cloud Run にデプロイする

gcloud run deploy claude-gateway \
  --image="${REGION}-docker.pkg.dev/${PROJECT_ID}/claude-gateway/gateway:<version>" \
  --region="$REGION" \
  --service-account="claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com" \
  --min-instances=1 \
  --timeout=3600 \
  --ingress=internal-and-cloud-load-balancing \
  --network="$VPC" --subnet=cc-gateway-subnet --vpc-egress=private-ranges-only \
  --set-secrets=/etc/claude/gateway.yaml=gateway-config:latest,GATEWAY_JWT_SECRET=gateway-jwt-secret:latest,OIDC_CLIENT_SECRET=gateway-oidc-client-secret:latest,GATEWAY_POSTGRES_URL=gateway-postgres-url:latest \
  --no-invoker-iam-check

フラグの意味を押さえておきます。

Direct VPC egress--network / --subnet / --vpc-egress=private-ranges-only)により、サービスは Cloud SQL のプライベート IP に直接到達できます。そしてAgent Platform エンドポイントと accounts.google.com へのパブリックエグレスは VPC を通さず直接インターネットへ出るので、Cloud NAT は不要です。ここは見落としがちなコスト・構成要素なので明示されているのが親切です。

**invoker IAM チェックはオープンにするか無効にする必要があります。**理屈はこうです——ゲートウェイは自前で OIDC を実行し、そのクライアントは GCP トークンを持たないので、Cloud Run の invoker チェックは認証されていないリクエストを許可しなければならない。認証はコンテナに到達してからゲートウェイの OIDC サインインが行い、どのドメインがサインインできるかは allowed_email_domains が制御します。「Cloud Run 側の認証を切る」ことが安全なのは、その内側に本物の認証があるからです。

方法は2つ。--no-invoker-iam-checkallUsers バインディングを作らずチェックを無効化。Domain Restricted Sharing の下でも機能する)か、--allow-unauthenticatedallUsersrun.invoker を付与。組織が前者を許さない場合はこちら)。

そして**--ingress によるイングレス制限は invoker チェックとは独立した別レイヤー**なので、サービスを企業ネットワークに限定するために設定したままにします。

*.run.app の問題も明記されています。デフォルトではパブリックアドレスに解決するので /login に拒否されます。開発者にプライベート解決可能なホスト名を与えるトポロジーは2つあり、Cloud Run はどちらもプロビジョニングしません

  1. Internal Application Load Balancer(上のデプロイコマンドが想定):--ingress=internal-and-cloud-load-balancing でデプロイし、前段に内部 ALB を立て、内部 DNS 名と証明書を使い、listen.public_url をそのホスト名にする。
  2. ロードバランサーなしの内部のみイングレス--ingress=internal でデプロイし、public_url*.run.app のままにする。ただしネットワークチームが既に、Google API 用の Private Service Connect エンドポイント、*.run.app をそれに解決する Cloud DNS プライベートゾーン、そのエンドポイントへのオンプレミスルーティングを運用していることが条件。

プライベートホスト名で提供できるまでは、Cloud Run のログでコンテナがブートしたことを確認する、という現実的な進め方も示されています。

最後に順序の注意。最初のサインイン前に OAuth クライアントの認可リダイレクト URI を <public_url>/oauth/callback に更新し、public_url を変えたら再デプロイします。理由は繰り返し出てくるあれです——ゲートウェイは設定からのみパブリックオリジンを組み立て、X-Forwarded-HostX-Forwarded-Proto を無視するから。

ステップ7B:GKE にデプロイする

まずクラスタの配置が制約になります。**クラスタは Cloud SQL ステップで作った $VPC 上にある必要があります。**理由が技術的に興味深い——VPC ピアリングだけでは機能しません。Cloud SQL のプライベート IP 自体がピアリングされたネットワークであり、ピアリングは非推移的だからです。新規クラスタなら gcloud container clusters create--network="$VPC" --subnetwork=cc-gateway-subnet を渡します。

次に Workload Identity のバインドです。

gcloud container clusters update <cluster> --region="$REGION" \
  --workload-pool="${PROJECT_ID}.svc.id.goog"
# On a Standard cluster, existing node pools also need GKE_METADATA;
# Autopilot enables this by default.
gcloud container node-pools update <pool> --cluster=<cluster> \
  --region="$REGION" --workload-metadata=GKE_METADATA
kubectl create namespace claude-gateway
kubectl create serviceaccount gateway -n claude-gateway
gcloud iam service-accounts add-iam-policy-binding \
  "claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role roles/iam.workloadIdentityUser \
  --member "serviceAccount:${PROJECT_ID}.svc.id.goog[claude-gateway/gateway]"
kubectl annotate serviceaccount gateway -n claude-gateway \
  iam.gke.io/gcp-service-account="claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com"

デプロイ自体は第51回で扱った標準構成——Deployment、Service、クラス gce-internal の内部 Ingress——で、指定するのは serviceAccountName: gateway、Secret Manager CSI ドライバーによる /secrets へのマウント、GET /readyz を指す readiness プローブ。

忘れると必ず踏む設定が1つゲートウェイ Service に BackendConfig をアタッチして timeoutSec を上げること。GKE Ingress の背後のロードバランサーバックエンドサービスはデフォルト30秒で、長いストリーミング応答が切断されます

そしてやってはいけないことが明示されています。**Workload Identity クラスタで 169.254.169.254 をブロックするエグレス NetworkPolicy を適用してはいけません。**ポッドは認証情報のためにメタデータサーバーへ到達する必要があるからです。そこでの防御はゲートウェイの組み込み SSRF ガード(第51回)が担います。

紛らわしい挙動の説明も添えられています。ゲートウェイはメタデータエンドポイントに到達可能であることを示すブート警告をログに出し、エグレス NetworkPolicy の適用を提案します。しかしWorkload Identity の下ではポッドがそのエンドポイントを必要とするので、その警告は想定内です。警告に従って塞ぐと壊れる、という罠を先回りしています。

ステップ8:開発者マシンに URL をプッシュする

ここまででゲートウェイは動いていますが、開発者は到達できません。MDM 経由で各デバイスに配る管理設定ファイルに forceLoginMethodforceLoginGatewayUrl を設定します。開発者が手動で選べるログインピッカーのゲートウェイオプションは存在しません(第49・50回で繰り返された点)。

Terraform リファレンス

公式リポジトリの examples/gateway/gcp に、このページの Cloud Run トラックを自動化したアセットがあります。

  • setup.sh:API 有効化から最初のデプロイまでを通す、冪等な gcloud プロビジョナー
  • terraform/:同じデプロイを IaC で。greenfield では**「Artifact Registry リポジトリを作る対象適用 → イメージをビルドしてプッシュ → 完全適用」**の順
  • gateway.yaml.example と、distroless ランタイムイメージ用の Dockerfile

ただしデフォルト値がこのページの本文と2箇所で異なります。アセットはイングレスをデフォルトで internal(ロードバランサー不要)にし、invoker レイヤーをデフォルトで allUsersrun.invoker 付与にします——本文の --no-invoker-iam-check とは逆です。本文の構成に合わせたければ、INGRESS=internal-and-cloud-load-balancingsetup.sh を実行するか、Terraform 変数 ingressINGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER にします。どちらでも機能し、選択は組織のポリシー制約次第です。

こちらもサポートされた本番アーティファクトではなく実装例である旨が繰り返されています。

GCP 固有のトラブルシューティング

プラットフォーム非依存のものは第51回の表(公式では deploy ページ)を参照し、ここには GCP 固有の5件が載っています。

  • コンテナに到達する前に 403 Forbidden → invoker IAM チェックが有効なまま。--no-invoker-iam-check--allow-unauthenticated
  • --no-invoker-iam-checkinvoker_iam_disabled is not currently available で拒否されるconstraints/run.managed.requireInvokerIam でブロックされている。--allow-unauthenticated を使う。constraints/iam.allowedPolicyMemberDomains による Domain Restricted Sharing がそれもブロックする場合は GKE トラックへ(ネットワークレイヤーで公開するので allUsers バインディングが不要)。
  • Container manifest type … must support amd64/linux → 非 amd64 ホストでビルドしたか、buildx が OCI イメージインデックスを出した。--platform=linux/amd64 --provenance=false でビルド。
  • Cloud Run でブートが Postgres 接続タイムアウトで終了 → サービスが VPC にアタッチされていないか、Cloud SQL がその VPC にプライベート IP を持たない。ストアは5秒で待機をやめます。
  • Agent Platform が 403 PERMISSION_DENIED → ランタイムが claude-gateway サービスアカウントを使っていないか、Model Garden でそのモデルがプロジェクト向けに有効になっていない
  • ストリーミング応答が一定時間で切れる → フロントエンドのリクエストタイムアウト。GKE Ingress の背後は既定30秒、Cloud Run は300秒。GKE は BackendConfig の timeoutSec、Cloud Run は --timeout=3600

最後の項目は、上のデプロイコマンドに --timeout=3600 が入っていた理由でもあります。

4. まとめ + 次回予告

  • このページは GCP でゲートウェイを動かす実装例サポートされた本番構成ではない——読んで理解し、自環境に適応させる前提。
  • 構成は Cloud Run または GKE + Artifact Registry + プライベート IP の Cloud SQL + Secret Manager + roles/aiplatform.user のサービスアカウント + 内部ロードバランサー/内部 Ingress
  • リージョンは「必要な Claude モデルが Model Garden で公開されている地域」から選ぶ。
  • ビルドは --platform=linux/amd64 --provenance=false。Apple Silicon で踏む罠。
  • trusted_proxies の具体値は3パターンの表にある。外部 GKE Ingress(クラス gce)は使えない——/login に拒否される。
  • Cloud Run は複数シークレットを1ディレクトリにマウントできないので、gateway.yaml はファイル、残り3つは環境変数。GKE は CSI で /secrets にマウント
  • **Cloud Run の invoker IAM チェックは開ける必要がある。**認証はその内側でゲートウェイの OIDC が行う。イングレス制限は別レイヤーなので残す。
  • ***.run.app はパブリック解決されるので使えない。**内部 ALB を立てるか、PSC + Cloud DNS プライベートゾーンの基盤が要る。
  • Direct VPC egress を使えば Cloud NAT は不要。
  • GKE では VPC ピアリングでは Cloud SQL に届かない(Cloud SQL 側もピアリングで、ピアリングは非推移的)。クラスタを同じ VPC に置く。
  • GKE Ingress の30秒タイムアウトは BackendConfig で上げる。忘れるとストリーミングが切れる。
  • **Workload Identity クラスタでメタデータサーバー(169.254.169.254)を塞いではいけない。**ブート警告が出るがそれは想定内。
  • setup.sh と Terraform が公式リポジトリにあるが、イングレスと invoker のデフォルトが本文と逆

これで第49回から続いたゲートウェイ関連5本(概要・設定・運用・支出制限・GCP 実装例)が完結しました。管理者向けトピックとしてはひと区切りです。

次回予告(暫定):第45回の予告で挙げたまま未着手だったチャネルリファレンス(channels-reference)に戻り、既製プラグインではなく自分でチャネルを構築する側——プロトコルの契約、権限プロンプトのリレー、ウェブフックレシーバーの作り方——を扱う予定です。


よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました