WIFをGoogle Cloudと連携する — Claude APIをAPIキーレスにする手順
Cloud Run・GCE・GKEからGoogle署名のIDトークンでClaude APIを呼ぶ、Workload Identity Federationの設定手順を追います。
WIFでGoogle CloudからClaude APIを呼ぶとは何か
Google Cloud上のワークロード(Cloud Run・Cloud Functions・App Engine・GCE・GKE)は、インスタンスのメタデータサーバーから自分のサービスアカウントに紐づくGoogle署名のIDトークンを取得できます。Workload Identity Federation(WIF)は、このIDトークンをAnthropicのPOST /v1/oauth/tokenエンドポイントに渡し、短命のClaude APIアクセストークンと交換する仕組みです。
長期間有効なsk-ant-...のAPIキーをリポジトリや環境変数に置く必要がなくなります。トークンの発行元はhttps://accounts.google.comで、AnthropicはこのIssuerを標準のOIDC discoveryでそのまま検証できます。Google Cloud側に追加設定は不要で、サービスアカウントを正しく紐づけるだけです。
前提として、WIFの基本概念(サービスアカウント・連合発行者・連合ルール)はWIFリファレンスを先に読んでおくと設定の意味が追いやすくなります。
Google Cloud側で必要な設定
標準のコンピュート環境(Cloud Run・Cloud Functions・App Engine・GCE)とGKEでは、IDトークンの取得手順がわずかに異なります。
Cloud Run・Cloud Functions・App Engine・GCEの場合
まず、Compute Engineのデフォルトサービスアカウントではなく、専用のユーザー管理サービスアカウントをワークロードに紐づけます。
gcloud run deploy my-service \
--service-account inference-worker@my-project.iam.gserviceaccount.comワークロード内部では、メタデータサーバーへのリクエストでIDトークンをオンデマンド取得できます。audienceにはAnthropic側で登録する値(https://api.anthropic.com)を指定し、format=fullを付けてemailクレームを含めます。
curl -sS -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full"gcloudコマンドでも同じトークンを取得できます。
gcloud auth print-identity-token \
--audiences="https://api.anthropic.com" \
--include-emailデコードしたトークンのペイロードは次の形です。
{
"iss": "https://accounts.google.com",
"aud": "https://api.anthropic.com",
"sub": "104892...",
"azp": "104892...",
"email": "inference-worker@my-project.iam.gserviceaccount.com",
"email_verified": true,
"exp": 1775527120
}subはサービスアカウントの数値の一意IDで、emailは人が読めるサービスアカウントのアドレスです。連合ルールでは両方をマッチ条件に含めます。
GKE(Workload Identity)の場合
GKEでは、クラスターでWorkload Identityを有効化し、KubernetesのサービスアカウントにアノテーションでGoogleサービスアカウントを紐づけます。Workload Identityはノードのメタデータサーバーが返すトークンを、ノード自体のサービスアカウントではなく、アノテーションで指定したGoogleサービスアカウントのものに差し替える仕組みです。クラスター作成時に--workload-pool=PROJECT_ID.svc.id.googを指定してプールを有効化し、対象のNamespace/ServiceAccountの組ごとにGoogleサービスアカウント側でroles/iam.workloadIdentityUserのIAMバインディングを追加しておく必要があります。バインディングが抜けているとPodはメタデータサーバーへのリクエスト自体は成功しても、紐づけ前のデフォルトサービスアカウント名義のトークンが返り、後段の連合ルールにマッチしません。
apiVersion: v1
kind: ServiceAccount
metadata:
name: inference-worker
namespace: prod
annotations:
iam.gke.io/gcp-service-account: inference-worker@my-project.iam.gserviceaccount.comこの紐づけができていれば、GKEのメタデータサーバーもCloud Run・GCEと同じhttps://accounts.google.com発行・同じemailクレームのトークンを返します。Anthropic側の設定も共通です。
format=fullのGKEトークンには、google.compute_engine.project_id・google.compute_engine.zone・google.compute_engine.instance_nameのクレームが追加されます。これらは連合ルールのCEL条件式(claims.google.compute_engine.project_id == "my-project")で、特定のプロジェクトやノードプールにアクセスを絞り込むのに使えます。
KubernetesのサービスアカウントをGoogleサービスアカウントに紐づけたくない場合、GKEのPodはクラスター自身のOIDC発行者(https://container.googleapis.com/v1/projects/PROJECT/locations/REGION/clusters/CLUSTER)を、projected serviceAccountTokenボリューム経由で使う方法もあります。この場合の発行者はaccounts.google.comではなくクラスター固有のものになります。この構成はKubernetesクラスター自体のWIF連携で扱う手順と同じ仕組みです。
Anthropic側で必要な設定
Claude ConsoleのSettings → Workload identityを開き、Connect workloadからGoogle Cloudタイルを選ぶと、発行者の登録・サービスアカウントの作成・連合ルールの作成をウィザードが順に案内します。
ウィザードは以下のリソースを作成します。同じリソースはAdmin APIから直接作成・更新することもでき、POST /v1/organizations/{organization_id}/federation_issuersで発行者を、POST /v1/organizations/{organization_id}/federation_rulesで連合ルールを作成します。エンドポイントの認証にはAnthropicの管理者APIキー(x-api-keyヘッダー)を使うため、ワークロード側のWIF設定と管理APIの認証は別系統です。CI/CDパイプラインで複数プロジェクト分のルールをコード管理したい場合はウィザードよりAdmin APIが向いていますが、値そのものはウィザードで作るものと共通です。
連合発行者: GoogleはOIDC discoveryドキュメントを公開しているため、discoveryモードを使います。この1つの発行者だけで、Cloud Run・GCE・Cloud Functions・App Engine・GKE(Workload Identity)のすべてのGoogle Cloudサーフェスをカバーできます。ワークロードの違いは発行者ではなくルールで区別します。
{
"name": "gcp",
"issuer_url": "https://accounts.google.com",
"jwks": { "type": "discovery" }
}連合ルール: subとemailの両方のクレームをマッチ条件にします。emailは読みやすいサービスアカウントのアドレスで、subはサービスアカウントの数値の一意IDです。Googleはsubを再利用しないため、これを固定しておくと、サービスアカウントが削除されて同じメールアドレスで新しいアカウントが作られても、ルールが誤って一致することを防げます。一意IDは次のコマンドで確認できます。
gcloud iam service-accounts describe SA_EMAIL --format='value(uniqueId)'{
"name": "gcp-inference-worker",
"issuer_id": "fdis_...",
"match": {
"audience": "https://api.anthropic.com",
"claims": {
"sub": "104892101234567890123",
"email": "inference-worker@my-project.iam.gserviceaccount.com"
}
},
"target": {
"type": "service_account",
"service_account_id": "svac_..."
},
"workspace_id": "wrkspc_...",
"oauth_scope": "workspace:developer",
"token_lifetime_seconds": 600
}workspace_idは、交換で得たアクセストークンがどのワークスペースの権限で動くかを決めます。省略すると組織直下のデフォルトワークスペースになるため、コストやレート制限をワークスペース単位で分けて管理している組織は明示しておきます。oauth_scopeはそのトークンでできる操作の範囲で、workspace:developerは通常のAPI呼び出し(メッセージ送信・モデル一覧の取得など)に必要な権限一式を指し、ワークスペースの設定変更やメンバー管理といった管理系の操作は含みません。token_lifetime_secondsは交換で発行されるアクセストークンの有効秒数で、Googleが返すIDトークンの有効期限(約1時間)とは無関係の、Anthropic側だけで完結する値です。短く設定するほど漏洩時の影響時間を絞れますが、その分SDKのトークンプロバイダーが呼ばれる頻度が上がります。
トークンを取得してClaude APIを呼ぶ
ワークロード内でメタデータサーバーからIDトークンを取得し、POST /v1/oauth/tokenで交換して、返ってきたベアラートークンでClaude APIを呼びます。各Anthropic SDKは、メタデータサーバーから新しいIDトークンを返すトークンプロバイダー関数を渡すだけで、交換と更新のループを自動でこなします。
# メタデータサーバーからGoogle署名のIDトークンを取得
JWT=$(curl -sS -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full")
# Anthropicのアクセストークンに交換
RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \
-H "content-type: application/json" \
--data @- <<JSON
{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": "$JWT",
"federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
"organization_id": "$ANTHROPIC_ORGANIZATION_ID",
"service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
"workspace_id": "$ANTHROPIC_WORKSPACE_ID"
}
JSON
)
ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)
curl -sS https://api.anthropic.com/v1/messages \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"claude-opus-5","max_tokens":1024,"messages":[{"role":"user","content":"Hello from Cloud Run"}]}'Python SDKではWorkloadIdentityCredentialsにトークン取得関数を渡すだけで済みます。
import os
import anthropic
import google.auth.transport.requests
import google.oauth2.id_token
from anthropic import WorkloadIdentityCredentials
AUDIENCE = "https://api.anthropic.com"
def fetch_google_identity_token() -> str:
request = google.auth.transport.requests.Request()
return google.oauth2.id_token.fetch_id_token(request, AUDIENCE)
client = anthropic.Anthropic(
credentials=WorkloadIdentityCredentials(
identity_token_provider=fetch_google_identity_token,
federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
),
)Google発行のIDトークンはおよそ1時間で失効します。SDKは失効前にトークンプロバイダーを再実行し、自動で交換をやり直します。アクセストークンのexpires_inより長く動くシェルスクリプトでは、タイマーで定期的に更新処理を回します。
設定を検証する
ワークロード内からIDトークンをデコードし、クレームがルールと一致しているか確認します。
curl -sS -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full" \
| jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'issがhttps://accounts.google.com、audがhttps://api.anthropic.com、emailがルールの値と一致していることを確認したら、トークン交換を実行します。成功時はsk-ant-oat01-で始まるaccess_tokenとexpires_inが返ります。401のauthentication_error(メッセージはAuthentication failedという不透明な固定文言)で失敗する場合、認証履歴ページで拒否理由を確認します。Google Cloud側で最も多い原因はemailクレームの欠落で、IDトークンのリクエストにformat=fullを付ければ解決します。
失敗の返り方は原因によって変わるため、401以外のレスポンスも切り分けの手がかりになります。audクレームがAnthropicに登録した値と食い違っている場合は、メタデータサーバーへのリクエストで指定したaudienceと、連合ルールのmatch.audienceのどちらかが古いままになっているのが典型で、レスポンスはinvalid_grant系のエラーになります。発行者(issuer_url)自体をAnthropic側にまだ登録していない場合は、トークンのデコードには成功するのにトークン交換だけが失敗し、federation_issuer_not_foundに近いエラーコードが返ります。IDトークンは正しく取れているのに連合ルールのどれにも一致しない場合は、subまたはemailの値の食い違いが原因であることが大半で、認証履歴ページのリクエスト詳細に実際に送られたクレームの値が残るため、ルール側の値と1文字ずつ突き合わせて確認します。
ルールを絞り込むときの注意点
Googleのsubクレームは安定したプレフィックスを持たない不透明な数値IDです。末尾に*を付けたsubject_prefixは、すべてのGoogle Cloudプロジェクトの任意のサービスアカウントにマッチしてしまい、そのどれもが連合済みのAnthropicトークンを取得できてしまいます。
ルールのmatchブロックは、用途に合う最も狭いスコープに絞ります。
| 絞り込み | やり方 |
|---|---|
subを完全一致させる | やり方完全な数値の一意IDをclaims.subに設定し、Googleトークンにはsubject_prefixを使わない |
emailクレームも固定 | やり方claims.emailをsubと併記し、安定したIDと読みやすいアドレスの両方を一致条件にする |
| audienceを固定 | やり方メタデータサーバーへリクエストする値と同じ値をaudienceに設定し、他の用途向けトークンを拒否する |
| GKEのプロジェクトを固定 | やり方format=fullトークンにclaims.google.compute_engine.project_id == "my-project"のような条件を追加し、特定プロジェクトのノードだけに絞る |
本番と検証環境で連合ルールを分けておくと、片方を無効化しても他方に影響しません。環境ごとに専用のAnthropicサービスアカウントを用意する設計が安全です。
APIキー運用からの移行
既存のワークロードが環境変数やSecret ManagerでAPIキーを配っている場合、キーを即座に無効化せずWIFと並行稼働させながら切り替えます。アプリケーション側は「WIFのトークンプロバイダーが取得に失敗したら環境変数のAPIキーにフォールバックする」形のクライアント初期化にしておくと、連合ルールの設定ミスがそのままサービス停止に直結しません。切り替えの手順は次の順で進めます。
- 検証環境のワークロード1つにだけ連合ルールを紐づけ、APIキーは残したままWIF経由の呼び出しが成功するかを確認する
- 本番の対象ワークロードを1つずつWIF経由に切り替え、それぞれで認証履歴ページに拒否ログが出ないことを確認してから次のワークロードへ進める
- 全ワークロードの切り替えが終わったら、Secret Manager上のAPIキーのバージョンを無効化する(削除は一定期間ログを見てから)
- IAMの監査ログで、無効化したAPIキーへのアクセス試行が発生していないかを1〜2週間分さかのぼって確認する
この順序を踏むと、途中で連合ルールの不備が見つかってもAPIキーでの呼び出しに切り戻すだけで済み、ダウンタイムなしで移行できます。
GitHub ActionsやKubernetesとの違い
同じWIFでも、IdPが変わればトークンの発行元とクレームの形が変わります。GitHub Actionsのワークフローから同じ仕組みでAPIキーレス化する手順はWIFをGitHub Actionsと連携する、自己管理のKubernetesクラスターから使う場合はWIFをKubernetesと連携するで扱っています。Google Cloud上でGKEを使っている場合でも、Workload Identityを使わず素のKubernetes発行者に寄せる構成ならこちらの手順が該当します。
いずれの構成でも、連合ルールの粒度設計とトークン交換のリクエスト・レスポンス仕様は共通です。エラーコードの逆引きやCEL条件式の書き方はWIFリファレンスにまとめています。
まとめ
Google CloudのCloud Run・Cloud Functions・App Engine・GCE・GKEは、専用のサービスアカウントさえ紐づければ、追加のGoogle Cloud側設定なしにAnthropicへ連合できます。設定するのはAnthropic側の発行者とルールだけです。subとemailの両方をルールに固定し、GKEではプロジェクトIDまで絞り込むのが安全な運用です。すでにAPIキーを環境変数やSecret Managerで配っているワークロードをWIFへ移行するときは、まず検証環境で1つのサービスアカウントを連合し、認証履歴ページで拒否理由が出ないことを確認してから本番のルールを追加するのが着実です。