Claude Media
Claude apps gatewayをGCPにデプロイする — Cloud RunとCloud SQLの構成例

Claude apps gatewayをGCPにデプロイする — Cloud RunとCloud SQLの構成例

Claude apps gatewayをCloud Run/GKE・Cloud SQL・Secret Manager・サービスアカウント認証でGoogle Cloudに構築する実践例をまとめます。

この構成で何が組み上がるか

基本のセットアップを終えたあと、実際にGoogle Cloud(GCP)でどこまでのリソースを組むかは、AWSやオンプレのKubernetesとは細部が違います。本記事はGoogle Cloud's Agent Platform(旧称Vertex AI)をモデルの上流としたGCP上の構成例を、実際のgcloudコマンドで組み立てます。

でき上がるのは次の構成です。

  • Cloud RunサービスまたはGKE Deploymentでゲートウェイコンテナを稼働
  • Artifact Registryリポジトリにゲートウェイイメージを保管
  • Cloud SQL for PostgreSQLをプライベートIPのみで配置し、ゲートウェイのstoreとして使用
  • Secret Managergateway.yaml本体・JWT署名鍵・OIDCクライアントシークレット・Postgres接続文字列を保管
  • サービスアカウントroles/aiplatform.userを付与し、Cloud Runなら直接、GKEならWorkload Identityで束縛
  • 前段のHTTPS終端は自分で用意する。Cloud Run前段の内部Application Load Balancer、またはGKEのgce-internalクラスの内部Ingress

IdPの例はGoogle Workspaceですが、oidcブロックだけ差し替えればOIDC準拠の他IdPでも同じ構成が通用します。

前提条件

項目内容
GCPプロジェクト内容課金有効化済みで、上記リソースを作成できる権限
ツール内容gcloud CLI(gcloud auth login済み)とDockerがローカルにあること
GKEを使う場合内容kubectlと、本記事で作るVPC上のGKEクラスター
モデルアクセス内容必要なClaudeモデルがModel Gardenでそのリージョンに公開されていること
IdP内容リダイレクトURIをhttps://<ゲートウェイのホスト>/oauth/callbackとしたGoogle WorkspaceのOAuth 2.0 Webアプリケーションクライアント
TLS内容ロードバランサー用の内部DNSホスト名

IdP側のOAuthクライアント登録やシークレットローテーションのようなクラウドに依存しない運用手順はデプロイと運用で扱っているので、本記事ではGCP固有の手順に絞ります。

PROJECT_IDと、Claude Codeが必要とするモデルがModel Gardenで公開されているリージョンを指すREGIONを先に決めます。

export PROJECT_ID=<your-project>
export REGION=us-east5
gcloud config set project "$PROJECT_ID"

手順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はCloud SQLをプライベートIPで置くために必要、runはCloud Run、containerはGKEのみに要る、という組み合わせなので、どちらか一方のトラックしか使わないなら不要なAPIは省略できます。

手順2: サービスアカウントを作りIAMを付与する

ゲートウェイはGoogle Cloud's Agent Platformを呼び出す専用のサービスアカウントで動きます。Cloud SQLへはVPC経由のパスワード認証で到達するため、Cloud SQL側に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

続けて、プロジェクトで使うClaudeモデルをModel Gardenで有効にします。モデルはリージョンごとに個別公開されるため、使うモデルカードで対象リージョンを確認してください。

手順3: Artifact Registryへイメージをビルド&プッシュする

gcloud artifacts repositories create claude-gateway \
  --repository-format=docker --location="$REGION"
gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
 
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>"

Cloud Runはlinux/amd64必須です。--provenance=falseを付けないと、buildxが作るOCIイメージインデックスをCloud Runが拒否します。

手順4: Cloud SQL for PostgreSQLを用意する

Private Services Access経由でVPCに紐づけ、公開IPを持たせません。この構成はconstraints/sql.restrictPublicIp組織ポリシーが強制されているプロジェクトでも通ります。

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
 
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 Services Accessの2つのコマンド(managed servicesアドレスの確保とVPCピアリング)はVPC単位で1回だけ実行します。GKEを使う場合、クラスターは必ずこのステップで作った$VPC上に置く必要があります。VPCピアリングだけでは届きません。Cloud SQLのプライベートIP自体がピアリングされたネットワークであり、ピアリングは推移的でないためです。このVPC上に新規クラスターを作るなら、gcloud container clusters create--network="$VPC" --subnetwork=cc-gateway-subnetを渡します。

手順5: gateway.yamlを書く

upstreamsはGoogle Cloud's Agent Platformをauth: {}で指すだけで、ランタイムのサービスアカウントからApplication Default Credentialsで認証します。trusted_proxiesは前段の構成によって値が変わり、Google由来のid_tokenにはgroupsクレームが無いため、グループベースのポリシーを使うならAdmin SDK Directory APIを呼ぶoidc.google_groupsの設定か、email_domainでのマッチに頼ります。

前段trusted_proxies
Cloud Runを直接叩く(ロードバランサーなし)trusted_proxies169.254.0.0/16
Cloud Run前段の内部Application Load Balancertrusted_proxies169.254.0.0/16とプロキシオンリーサブネットのCIDR
GKEの内部Ingress(gce-internalクラス)trusted_proxiesプロキシオンリーサブネットのCIDR

外部向けのgceクラスGKE Ingressはここには含めていません。公開のforwarding-ruleアドレスを払い出すため、/loginのプライベートネットワークチェックが拒否します。

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}
  allowed_email_domains: [example.com]
  scopes: [openid, profile, email]
  extra_auth_params: { access_type: offline, prompt: consent }
 
session:
  jwt_secret: ${GATEWAY_JWT_SECRET}
 
store:
  postgres_url: ${GATEWAY_POSTGRES_URL}
 
upstreams:
  - provider: vertex
    region: <your-region>
    project_id: <your-project>
    auth: {}

scopesoffline_accessを含めていないのは、Googleがそのスコープを無視するためです。代わりにextra_auth_paramsaccess_type: offlineprompt: consentを指定すると、リフレッシュトークンが発行されます。必須5セクション以外の項目まで含めた全項目はgateway.yamlリファレンスにまとめてあり、upstreamstrusted_proxiesまわりだけがGCP固有の書き方になります。

手順6: Secret Managerにシークレットを置く

4つのシークレットを作り、claude-gatewayサービスアカウントにroles/secretmanager.secretAccessorを付与します。

シークレット由来
gateway-jwt-secret由来openssl rand -base64 32
gateway-oidc-client-secret由来Google Cloud ConsoleのOAuthクライアント
gateway-postgres-url由来手順4で組み立てた接続文字列
gateway-config由来手順5のgateway.yaml本体

シークレットの届き方はトラックによって違います。GKEではSecret Manager CSIドライバーがファイルとしてマウントし、gateway.yaml${file:/secrets/...}で参照します。Cloud Runは1つのディレクトリに複数シークレットをマウントできないため、gateway.yamlだけファイルとしてマウントし、残り3つは環境変数として注入し、gateway.yaml側は${GATEWAY_JWT_SECRET}のような環境変数参照にします。

手順7: デプロイする

Cloud RunとGKEでは前段の構築責任が変わります。

観点Cloud RunGKE
モデル認証Cloud Runランタイムに直接付けたサービスアカウントGKEWorkload Identity
ロードバランサーCloud Run自前で内部ALBを用意(このページのgateway.yamlが前提)GKEgce-internalクラスのIngressをAWS同様に自分で作成
起動時のOIDCディスカバリー遅延対策Cloud Runmin-instances=1GKE不要(常駐Pod)
コネクション数の上限Cloud Runインスタンス数×store.max_connections(既定5)がCloud SQLの上限を超えないようmax-instancesを調整GKEノード数に応じて別途調整

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 --max-instances=8 --timeout=3600 --ingress=internal \
  --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

--network--subnet--vpc-egress=private-ranges-onlyによるDirect VPC egressが、Cloud SQLのプライベートIPへ直接到達する経路を作ります。Google Cloud's Agent Platformのエンドポイントとaccounts.google.comへの公開向けの通信はVPCを介さず直接インターネットへ出るため、Cloud NATは不要です。

invoker IAMチェックは開放するか無効化する必要があります。ゲートウェイは自前でOIDCサインインを行い、クライアントはGCPトークンを持たないため、Cloud Run側の認証チェックが未認証リクエストを通す設定になっていないと、ゲートウェイ自身のサインインフローにすら到達できません。--no-invoker-iam-checkallUsersバインディングを作らずにチェックを無効化でき、Domain Restricted Sharingの下でも機能します。それが使えない組織ポリシーなら--allow-unauthenticatedallUsersrun.invokerを付与します。--ingressによるアクセス制限はこれとは独立したレイヤーで、社内ネットワークに絞るために別途設定します。

既定の*.run.appドメインは公開アドレスに解決されるため、/loginのプライベートネットワークチェックに拒否されます。プライベートに解決できるホスト名を用意するトポロジーは2つあり、どちらもCloud Run自体は作ってくれません。1つはこのページのgateway.yamlが前提とする内部Application Load Balancerで、public_urlをそのホスト名にします。もう1つは*.run.appのまま、社内ネットワークにPrivate Service ConnectエンドポイントとCloud DNSプライベートゾーンを別途構築してプライベート解決させる方法です。

初回サインイン前に、OAuthクライアントの承認済みリダイレクトURIを<public_url>/oauth/callbackに更新してください。public_urlを変更したら再デプロイも必要です。ゲートウェイは公開オリジンをこの設定値だけから組み立て、X-Forwarded-HostX-Forwarded-Protoは無視するためです。

GKEでは、クラスターとノードプールでWorkload Identityを有効化し、GoogleサービスアカウントとKubernetesサービスアカウントを紐づけます。

gcloud container clusters update <cluster> --region="$REGION" \
  --workload-pool="${PROJECT_ID}.svc.id.goog"
# Standardクラスターでは既存のノードプールにもGKE_METADATAが必要です。
# Autopilotは既定で有効になっています。
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"

GKE Ingressの背後にあるロードバランサーバックエンドは既定30秒でタイムアウトし、長いストリーミング応答を切ってしまうため、timeoutSecを引き上げたBackendConfigをゲートウェイのServiceに付けます。Workload Identityクラスターではメタデータサーバー(169.254.169.254)への到達をegress NetworkPolicyで塞がないでください。Podの認証情報取得に必要で、ゲートウェイ内蔵のSSRFガードがその面の防御を別に担っています。ゲートウェイは起動時に「メタデータエンドポイントに到達可能」という警告ログを出しegress NetworkPolicyの適用を勧めてきますが、Workload Identityクラスターではその到達性自体が必要なので、この警告は想定内であり設定ミスではありません。

手順8: ゲートウェイURLを開発者マシンへ配信する

ゲートウェイが稼働しても、forceLoginMethodforceLoginGatewayUrlparentSettingsBehavior: "merge"managed settingsの設定キー一覧からMDM経由で各デバイスに届くまで、/loginの選択肢にゲートウェイは出てきません。

Terraformでコード化する

Cloud Runトラックを自動化した参照アセット一式があります。setup.shはAPI有効化から初回デプロイまでを冪等なgcloudスクリプトで通し、terraform/は同じ構成を宣言的に、Artifact Registryリポジトリの部分apply→ビルド&プッシュ→全体applyという2段階で構築します。gateway.yaml.exampleとDockerfileはCloud Run・GKEどちらのトラックにも共通で使えます。既定のCloud Runのingressはこのページのデプロイコマンドと同じinternalで、invoker層は--no-invoker-iam-checkではなくallUsersへのrun.invoker付与を既定にしています。どちらも動きますが、組織のポリシー制約次第でどちらを選ぶかが決まります。

よくあるつまずき

症状原因対処
Cloud Runがコンテナに到達する前に403 Forbidden原因invoker IAMチェックが有効なまま対処--no-invoker-iam-checkでデプロイするか、--allow-unauthenticatedallUsersrun.invokerを付与する
--no-invoker-iam-checkinvoker_iam_disabled is not currently availableで拒否される原因constraints/run.managed.requireInvokerIam組織ポリシーでブロックされている対処--allow-unauthenticatedを使う。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を持っていない対処--network--subnetでDirect VPC egressを設定し、Cloud SQLは--no-assign-ipとそのVPCを指す--networkで作成する
Google Cloud's Agent Platformへのリクエストが403 PERMISSION_DENIED原因ランタイムがclaude-gatewayサービスアカウントを使っていないか、対象リージョンのModel Gardenでモデルが未有効対処Cloud Runは--service-account、GKEはWorkload Identityを設定し、各Claudeモデルを対象リージョンのModel Gardenで有効にする
ストリーミング応答が一定時間で打ち切られる原因前段のリクエストタイムアウト(GKE Ingressのバックエンドは既定30秒、Cloud Runは既定300秒)対処GKEはBackendConfigのtimeoutSecを引き上げ、Cloud Runは--timeout=3600でデプロイする

まとめ

GCP上のClaude apps gatewayは、Cloud RunかGKEかという計算基盤の選択と、invokerチェックをどう開放するかという認証層の選択で構成の大半が決まります。Cloud SQLのプライベート接続とSecret Managerの配線はどちらのトラックでも共通で、GCP特有の落とし穴はほぼ*.run.appの公開解決とinvoker IAMチェックの2点に集約されます。IAM設定の考え方は個々の開発者端末向けのVertex AI IAM設定(チーム展開ガイド)とも重なる部分があるので、あわせて確認しておくと理解が早くなります。上流のGoogle Cloud's Agent Platformで使える機能がBedrockやMicrosoft Foundryとどう違うかは、プロバイダー別の機能比較で個別に整理しています。

この記事を共有:XはてブLinkedIn