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 Managerに
gateway.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.comcomputeとservicenetworkingは、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_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"最後の2行で組み立てるGATEWAY_POSTGRES_URLが、手順6でgateway-postgres-urlに入れる値です。パスワードを生成したシェルを閉じる前に、Secret Managerへ登録するところまで進めてください。Cloud RunでもGKEでもランタイムはこのVPCの中にいるか、VPCへ経路が通っている必要があります。
Private Services Accessの2つのコマンド(managed servicesアドレスの確保とVPCピアリング)はVPC単位で1回だけ実行します。GKEを使う場合、クラスターは必ずこのステップで作った$VPC上に置く必要があります。VPCピアリングだけでは届きません。Cloud SQLのプライベートIP自体がピアリングされたネットワークであり、ピアリングは推移的でないためです。このVPC上に新規クラスターを作る場合は、次の2つのフラグを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 Balancer | trusted_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}
# readiness_grace_seconds: 300 # Cloud SQLのフェイルオーバー中も/readyzを通す
upstreams:
- provider: vertex
region: <your-region>
project_id: <your-project>
auth: {}scopesにoffline_accessを含めていないのは、Googleがそのスコープを無視するためです。代わりにextra_auth_paramsでaccess_type: offlineとprompt: consentを指定すると、リフレッシュトークンが発行されます。必須5セクション以外の項目まで含めた全項目はgateway.yamlリファレンスにまとめてあり、upstreamsとtrusted_proxiesまわりだけがGCP固有の書き方になります。
trusted_proxiesを設定する意味は、ゲートウェイがX-Forwarded-Forを、TCPの接続元がこのリストに入っているときだけ信用する点にあります。信用したうえでチェーンをたどるので、サインインのIP別レート制限と監査イベントには、ロードバランサーではなく開発者のIPが残ります。逆に空のままだと、前段のアドレスがクライアントIPとして記録されます。
コメントアウトしたreadiness_grace_secondsは、Postgresが応答しなくなった後も/readyzが準備完了を返し続ける秒数です。既定は0で、その場合はデータベースが落ちた瞬間に全レプリカが同時に未準備になり、ロードバランサーの背後では推論も含めた全トラフィックが止まります。0〜3600の整数で、Claude Code v2.1.282以降のサーバーが要り、古いバージョンはキーがあると起動を拒否します。フェイルオーバーより長い値(例は300)にします。ただし、支出上限を既定のfail-openで有効にしていると、レプリカが準備完了のままの間は計測されない推論が通ります。必要な長さに留めるのが前提です。enforcement.fail_closed_on_error: trueにしていれば、Postgresの復旧まで推論は429で拒否されます。
手順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 RunとGKEで自分の責任になる範囲
Cloud Run
- モデル認証はランタイムに直接付けたサービスアカウント
- 前段は自前の内部Application Load Balancer(このページの
gateway.yamlが前提) - 起動直後のOIDCディスカバリーを避けるため
min-instances=1 - インスタンス数×
store.max_connections(既定5)がCloud SQLの接続上限を超えないようmax-instancesで抑える
GKE
- モデル認証はWorkload Identity
- 前段は
gce-internalクラスのIngressを自分で作る - 常駐するPodなのでmin-instancesに当たる設定は要らない
- レプリカ数×
store.max_connectionsがデータベースのmax_connectionsを下回るようにする
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-checkCloud SQLのプライベートIPへの経路は、Direct VPC egressが作ります。指定しているのは--network・--subnet・--vpc-egress=private-ranges-onlyの3つです。Google Cloud's Agent Platformのエンドポイントとaccounts.google.comへの通信は、VPCを通らず直接インターネットへ出ます。そのためCloud NATは要りません。
--max-instances=8は適当な数字ではありません。各インスタンスが最大store.max_connections本(既定5)のPostgres接続を持つので、8×5で最大40本になります。この積がCloud SQLのティアの接続上限を下回るよう、構成例のリファレンスアセットはdb-g1-small向けにインスタンス数を8で頭打ちにしています。ティアを変えるなら、この掛け算をやり直します。
このデプロイで混同しやすいのは、リクエストが通るかどうかを決める層が3つ別々にあることです。invokerのIAMチェックを開けても、ingressを絞ったことにはなりません。*.run.appに付く公開アドレスの問題も、前の2つとは別の層でClaude Code側が判定します。
接続を左右する3つの独立した層
invoker IAMチェック
Cloud Runが、クライアントのGCPトークンを要求するかどうかです。クライアントはGCPトークンを持たないので、未認証リクエストを通す設定が要ります。
--no-invoker-iam-checkはallUsersバインディングを作らず、Domain Restricted Sharingの下でも機能します。使えない組織ポリシーなら--allow-unauthenticatedでallUsersにrun.invokerを付与します。ingress設定
--ingress=internalは、社内ネットワークからの通信に絞る別のレイヤーです。内部Application Load Balancerからのトラフィックはinternalで通ります。internal-and-cloud-load-balancingは外部Application Load Balancerまで通す設定です。外部の公開アドレスは/loginの検査で弾かれるので、このページのどの構成でも使いません。/loginの私設アドレス検査
Claude Code側の検査です。ゲートウェイのホスト名が、RFC 1918・リンクローカル・CGNAT(
100.64.0.0/10)・IPv6 ULA・ループバックの私設アドレスだけに解決される必要があります。判定するのは開発者のマシン上のClaude Codeで、Cloud Runの設定では変えられません。
既定の*.run.appドメインは公開アドレスに解決されるため、そのままでは3つ目の検査に拒否されます。プライベートに解決できるホスト名を用意するトポロジーは2つあり、どちらもCloud Run自体は作ってくれません。1つはこのページのgateway.yamlが前提とする内部Application Load Balancerで、内部DNS名と証明書を付けてpublic_urlをそのホスト名にします。もう1つは、*.run.appのままpublic_urlを既定の*.run.app URLにしておく方法です。この場合は次の3つが前提です。1つ目はGoogle API向けのPrivate Service Connectエンドポイント、2つ目は*.run.appをそこへ引くCloud DNSプライベートゾーンです。3つ目はそこまでのオンプレミス経路で、いずれもネットワークチームがすでに運用している必要があります。
公開IPv4空間を自組織で保有して社内網に使っている場合は、gatewayInternalNetworksというmanaged settingsのキーでそのブロックを宣言します。すると、開発者のマシンも同じブロック内のアドレスから接続している場合に限り、/loginがその範囲内のゲートウェイを受け入れます。詳しくはゲートウェイの社内ネットワーク設定にあります。開発者のマシンがClaude Code v2.1.268以降である必要があり、古いバージョンはキーを無視して私設アドレスの規則を適用します。開発者のHTTPSが社内プロキシを通る環境では、プロキシのホストも私設アドレスに解決されなければサインインできないため、そうでなければゲートウェイのホストをNO_PROXYに入れます。
初回サインイン前に、OAuthクライアントの承認済みリダイレクトURIを<public_url>/oauth/callbackに更新してください。public_urlを変更したら再デプロイも必要です。ゲートウェイは公開オリジンをこの設定値だけから組み立て、X-Forwarded-HostやX-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"ゲートウェイ本体は、標準的なDeploymentとService、gce-internalクラスの内部Ingressで動かします。PodにはserviceAccountName: gatewayを指定します。シークレットはSecret Manager CSIドライバーで/secretsにマウントし、レディネスプローブはGET /readyzに向けます。
GKE Ingressの背後にあるロードバランサーのバックエンドは、既定30秒でタイムアウトします。これでは長いストリーミング応答が切れるので、timeoutSecを引き上げたBackendConfigをゲートウェイのServiceに付けます。Workload Identityクラスターではメタデータサーバー(169.254.169.254)への到達をegress NetworkPolicyで塞がないでください。Podの認証情報取得に必要で、ゲートウェイ内蔵のSSRFガードがその面の防御を別に担っています。ゲートウェイは起動時に「メタデータエンドポイントに到達可能」という警告ログを出し、egress NetworkPolicyの適用を勧めてきます。Workload Identityクラスターではこの到達性そのものが必要なので、警告は想定どおりで設定ミスではありません。
起動したかどうかを順番に確かめる
デプロイが成功しても、ゲートウェイが正常に起動したとは限りません。/loginで試す前に、手前の層から順に確かめると原因を絞れます。
疎通確認の順番
- 1
Cloud Runのログでコンテナの起動を見る
私設ホスト名がまだ無い段階では、コンテナが起動したことをCloud Runのログで確かめます。
- 2
/healthzと/readyzを見る
/healthzは生存確認、/readyzはストアに届くかの確認です。/readyzが通らないときは、Cloud SQLへの経路(VPCとDirect VPC egress)を疑います。 - 3
OAuthディスカバリーのURLが200を返すか見る
/.well-known/oauth-authorization-serverが200を返すのは、起動処理がすべて成功した後だけです。成功が必要なのは、設定の読み込み・OIDCディスカバリー・上流クライアントの構築・Postgresのマイグレーションの4つです。起動全体のエンドツーエンドの確認になります。 - 4
私設ホスト名でサインインする
最後に、開発者のマシンの
/loginから試します。ここで失敗するなら、前の3つではなくpublic_url・リダイレクトURI・アドレス解決の問題です。
起動時にPostgresへ届かない場合、ゲートウェイは2秒間隔で3回まで接続を試してから終了します。ログにcould not connect to Postgres at boot, attempt 1 of 3が出ても、異常とは限りません。Cloud Runのコールドインスタンスで、ネットワークの準備が間に合っていないときにも出ます。その後に起動が完了するなら対処は要りません。接続が拒否ではなくタイムアウトで失敗するなら、store.connect_timeout_secondsを引き上げます。
手元のClaude Code v2.1.287でclaude gateway --helpを実行してみました。サブコマンドの説明は「Run the enterprise auth/telemetry gateway」です。オプションは--config <path>(YAMLの設定ファイルのパス)だけでした。min-instancesや接続数のようなランタイムの調整はCLIのフラグではなく、設定ファイルとGCP側の設定で行います。
手順8: ゲートウェイURLを開発者マシンへ配信する
ゲートウェイが稼働しても、それだけでは/loginの選択肢にゲートウェイは出てきません。forceLoginMethod・forceLoginGatewayUrl・parentSettingsBehavior: "merge"の3つを、MDM経由で各デバイスに配る必要があります。各キーの意味はmanaged settingsの設定キー一覧にあります。開発者が手動で選べるゲートウェイ用の項目はないので、ここを飛ばすとインフラが正常でも誰もサインインできません。
Terraformでコード化する
Cloud Runトラックを自動化した参照アセット一式があります。setup.shは、API有効化から初回デプロイまでを冪等なgcloudスクリプトで通します。terraform/は同じ構成を宣言的に作るもので、Artifact Registryリポジトリだけを先にapplyし、ビルド&プッシュの後に全体をapplyします。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-unauthenticatedでallUsersにrun.invokerを付与する |
--no-invoker-iam-checkがinvoker_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でデプロイする |
まとめ
Cloud RunかGKEかは、組織ポリシーがinvokerチェックの開放を許すかでほぼ決まります。許すならCloud Runのほうが前段の作業が少なく、許さないならGKEが残ります。IAM設定の考え方は個々の開発者端末向けのVertex AI IAM設定(チーム展開ガイド)とも重なる部分があるので、あわせて確認しておくと理解が早くなります。上流のGoogle Cloud's Agent Platformで使える機能がBedrockやMicrosoft Foundryとどう違うかは、プロバイダー別の機能比較で個別に整理しています。