Claude Media
WIFをKubernetesと連携する — Podの署名トークンでAPIキー不要に

WIFをKubernetesと連携する — Podの署名トークンでAPIキー不要に

自己管理Kubernetesクラスターのprojected service account tokenでClaude APIを呼ぶ、Workload Identity Federationの設定手順を追います。

KubernetesのPodからAPIキーなしでClaude APIを呼ぶ仕組み

自己管理のKubernetesクラスター(kubeadm・k3s・OpenShift・オンプレミス各種)は、projected service account tokenの仕組みで、すべてのPodに対してOIDCのJSON Web Token(JWT)を発行できます。クラスターのAPIサーバー自身がOIDC発行者になり、各トークンのsubクレームはsystem:serviceaccount:<namespace>:<service-account>という形式です。

Workload Identity Federation(WIF)は、このトークンをPOST /v1/oauth/tokenで短命のAnthropicアクセストークンに交換する仕組みです。長期間有効なAPIキーをPodのSecretに置く必要がなくなります。

クラスターの発行者URLは、discoveryドキュメントを読めば確認できます。

kubectl get --raw /.well-known/openid-configuration | jq -r .issuer

この仕組み(projected service account tokenとAPIサーバーがOIDC発行者になる構成)はKubernetes自体にネイティブな機能で、あらゆるディストリビューションの土台になっています。マネージドKubernetesサービスを使っている場合は、クラウドプロバイダー側のガイド(AWS EKS / Google Cloud GKE / Azure AKS)でプロバイダー管理の発行者URLの探し方が示されています。SPIREを使っている場合は、クラスターのAPIサーバーではなくSPIRE OIDC Discovery Providerが発行者になります。それ以外のディストリビューションやガイドに載っていないマネージドプロバイダーでは、本記事の手順をそのままクラスターが報告する発行者URLに適用できます。

WIFの基本用語(サービスアカウント・連合発行者・連合ルール)はWIFリファレンスにまとめています。

前提条件

  • クラスターのAPIサーバーに--service-account-issuerフラグが設定されていること。ほとんどのディストリビューションではデフォルトで設定済みで、kubeadmクラスターは通常 https://kubernetes.default.svc.cluster.local を使う
  • 以下のいずれかで、Anthropicがトークンの署名を検証できること
    • 発行者のJWKSエンドポイントが443番ポートでインターネットから到達可能
    • クラスター内部からJWKSを取得し、inlineモードで登録できる(手順は後述の「Anthropic側の設定」)
  • Claude ConsoleでAnthropicの組織にサービスアカウント・連合発行者・連合ルールを作成できる権限

Kubernetes側の設定

Podに、連合ルールが期待するaudienceと有効期限でサービスアカウントトークンを投影します。serviceAccountTokenのprojectionは、マウントパスに新しいJWTを書き込み、expirationSecondsが経過する前にローテーションします。

apiVersion: v1
kind: Pod
metadata:
  name: inference-worker
  namespace: inference
spec:
  serviceAccountName: inference-worker
  volumes:
    - name: anthropic-token
      projected:
        sources:
          - serviceAccountToken:
              audience: https://api.anthropic.com
              expirationSeconds: 3600
              path: token
  containers:
    - name: app
      image: your-registry/inference-worker:latest
      env:
        - name: ANTHROPIC_IDENTITY_TOKEN_FILE
          value: /var/run/secrets/anthropic.com/token
        - name: ANTHROPIC_FEDERATION_RULE_ID
          value: fdrl_...
        - name: ANTHROPIC_ORGANIZATION_ID
          value: 00000000-0000-0000-0000-000000000000
        - name: ANTHROPIC_SERVICE_ACCOUNT_ID
          value: svac_...
        - name: ANTHROPIC_WORKSPACE_ID
          value: wrkspc_...
      volumeMounts:
        - name: anthropic-token
          mountPath: /var/run/secrets/anthropic.com
          readOnly: true

このPodに発行されるトークンはsub: "system:serviceaccount:inference:inference-worker"aud: ["https://api.anthropic.com"]を持ちます。

Anthropic側の設定

Claude ConsoleのSettings → Workload identityを開き、Connect workloadからKubernetesタイルを選ぶと、発行者の登録・サービスアカウントの作成・連合ルールの作成をウィザードが順に案内します。

ウィザードが作成するリソースは、ウィザードで直接入力する場合も、Admin APIへ送る場合も値は共通です。

連合発行者: 多くの自己管理クラスターはhttps://kubernetes.default.svc.cluster.localのような、インターネットから到達できない発行者URLを使っています。該当する場合はinlineのJWKSソースを選び、クラスターの鍵を貼り付けます。クラスター内部から鍵を取得するには次のコマンドを使います。

kubectl get --raw /openid/v1/jwks

返ってきたkeys配列の中身({"keys": [...]}という外側のラッパーではなく中身)を使って発行者を設定します。

{
  "name": "onprem-k8s",
  "issuer_url": "https://kubernetes.default.svc.cluster.local",
  "jwks": {
    "type": "inline",
    "keys": [{ "kty": "RSA", "kid": "...", "n": "...", "e": "AQAB" }]
  }
}

inlineモードではissuer_urlはJWTのissクレームとの比較にのみ使われ、AnthropicがこのURLに実際にアクセスすることはありません。発行者がインターネットから到達可能な場合は、"jwks": {"type": "discovery"}を使います。

inlineの鍵を使う場合、クラスターがサービスアカウントの署名鍵をローテーションしたときに発行者を更新する責任は利用者側にあります。ローテーションは頻度が低く、通常はクラスターのアップグレード時だけですが、新しいJWKSを反映するまでトークン交換は署名エラーで失敗し続けます。

連合ルール: サービスアカウントのsubクレームと、投影したトークンに設定したaudienceをマッチ条件にします。

{
  "name": "onprem-inference",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "system:serviceaccount:inference:inference-worker",
    "audience": "https://api.anthropic.com"
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}

subject_prefixsystem:serviceaccount:inference:*まで緩めるのは、そのnamespace内のすべてのサービスアカウントを同じAnthropicサービスアカウントにマッピングしたい場合だけにします。作成したルールのfdrl_...IDは、PodのANTHROPIC_FEDERATION_RULE_ID環境変数に設定します。

トークンを取得してClaude APIを呼ぶ

「Kubernetes側の設定」のPod定義は、ANTHROPIC_IDENTITY_TOKEN_FILEを投影されたマウントパスに、ANTHROPIC_FEDERATION_RULE_IDANTHROPIC_ORGANIZATION_IDANTHROPIC_SERVICE_ACCOUNT_IDANTHROPIC_WORKSPACE_IDとあわせて設定済みです。これだけで、SDKは交換のたびにディスクからトークンを読み、Anthropicアクセストークンを自動で更新します。

import anthropic
 
# Podの環境からANTHROPIC_IDENTITY_TOKEN_FILE、ANTHROPIC_FEDERATION_RULE_ID、
# ANTHROPIC_ORGANIZATION_ID、ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_IDを読む
client = anthropic.Anthropic()
 
message = client.messages.create(
    model="claude-opus-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello, Claude"}],
)

curlで直接交換する場合は、マウントされたトークンファイルをそのままassertionに使います。

JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE")
 
ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \
  -H "content-type: application/json" \
  --data @- <<JSON | jq -r .access_token
{
  "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
)
 
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, Claude"}]}'

設定を検証する

交換に成功すると、sk-ant-oat01-で始まるaccess_tokenexpires_inが返ります。401authentication_error(メッセージはAuthentication failedという不透明な固定文言)で失敗する場合は、認証履歴ページで拒否理由を確認します。Kubernetes側で最も多い原因はJWKSの鍵の不一致です。inlineモードの場合はで鍵を再取得し、発行者の登録を更新します。

よくあるつまずき

トークン交換が失敗するときは、まず認証履歴ページの拒否理由を見てから、以下の早見表で原因を切り分けます。

症状原因確認コマンド
401 authentication_error(Authentication failedの固定文言)原因拒否理由がメッセージに出ないため、まず認証履歴ページで内訳を見る必要がある確認コマンドcurl -sS https://api.anthropic.com/v1/oauth/token -H "content-type: application/json" --data @request.json
JWKSの鍵が一致しない原因inlineで登録した鍵がクラスターの現在の署名鍵と異なる(ローテーション後の更新漏れが典型)確認コマンドkubectl get --raw /openid/v1/jwks | jq .keys
audience不一致原因連合ルールのmatch.audienceと、PodのserviceAccountToken投影に設定したaudienceが食い違っている確認コマンドkubectl exec <pod> -- cat /var/run/secrets/anthropic.com/token | cut -d. -f2 | base64 -d | jq .aud
subject_prefix不一致原因ルールのsubject_prefixが、実際のnamespace・サービスアカウント名と完全一致していない(タイプミスや*の付け忘れ)確認コマンド上記と同じデコードコマンドで.subを確認する
issuer_urlissの不一致原因連合発行者に登録したissuer_urlと、トークンのissクレームの値(末尾スラッシュの有無や大文字小文字の違いを含む)が一致しない確認コマンド上記と同じデコードコマンドで.issを確認する
ローテーション直後にだけ失敗する原因expirationSecondsでトークンは自動更新されるが、inlineのJWKS登録側はクラスターの鍵ローテーションに追従しないため、両者がずれる確認コマンドkubectl get --raw /openid/v1/jwksの出力とAnthropic側に登録済みのJWKSを突き合わせる

いずれの場合も、まずトークンをデコードしてsubaudissを実際の値で確認してから、連合ルールと連合発行者の設定を突き合わせるのが最短です。JWTのペイロードはBase64URLエンコードされているだけなので、上記のようにcutbase64 -dで中身を直接読めます。

ルールを絞り込むときの注意点

system:serviceaccount:*だけのsubject_prefixは、クラスター内のすべてのサービスアカウントにマッチしてしまい、どのPodでも連合済みのAnthropicトークンを取得できてしまいます。audienceのマッチ条件がなければ、すべてのPodがすでに持っているデフォルトaudienceのトークンにもルールがマッチします。

ルールのmatchブロックは、用途に合う最も狭いスコープに絞ります。

絞り込みやり方
namespaceとサービスアカウント名を固定やり方末尾に*を付けず、完全なsystem:serviceaccount:<namespace>:<name>を使う
audienceを必ず設定するやり方ルールにaudienceを必須にし、PodのserviceAccountToken投影にも同じ値を設定してデフォルトaudienceのトークンを拒否する
namespaceごとに別ルールを用意するやり方1つのルールを広げるのではなく、namespaceごとに専用のルールとAnthropicサービスアカウントを作る
inline JWKS発行者をクラスター単位に絞るやり方複数クラスターが同じ発行者URLを共有していても、クラスターごとにJWKSを別の連合発行者として登録し、ルールをその発行者だけに紐づける

GCPやGitHub Actionsとの違い

同じWIFでも、Kubernetesの素の発行者と、Google Cloud・GitHub Actionsではトークンの発行元とクレームの形式が異なります。GKEでWorkload Identityを使う場合の手順はWIFをGoogle Cloudと連携する、CI/CDパイプラインからGitHub Actions経由で呼ぶ手順はWIFをGitHub Actionsと連携するにまとめています。

いずれの構成でも、トークン交換のリクエスト・レスポンス仕様と連合ルールの粒度設計は共通です。CEL条件式の書き方やエラーコードの逆引きはWIFリファレンスを参照してください。

まとめ

自己管理Kubernetesクラスターは、projected service account tokenとクラスター自身のOIDC発行者だけでWIFに対応できます。発行者URLが外部から到達できない構成が多いため、inlineのJWKS登録を選ぶケースが標準的です。連合ルールはnamespaceとサービスアカウント名を完全一致させ、audienceを必ず設定して、Pod側の投影設定と対にしておくのが安全です。まずは1つのnamespaceでルールを作り、認証履歴ページで想定どおりのクレームが記録されることを確認してから、他のnamespaceへ展開するのが着実な進め方です。

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