Claude Media
Claude apps gatewayのデプロイと運用 — IdP登録からアップグレードまで

Claude apps gatewayのデプロイと運用 — IdP登録からアップグレードまで

IdPへのOAuthクライアント登録からコンテナイメージのビルド、Kubernetes/Cloud Runへのデプロイ、シークレットローテーションとアップグレードまでの運用手順をまとめます。

この記事で扱う範囲

基本の使い方ではDocker Composeでの最小構成を組み立てました。本番環境ではそこからさらに、IdPへのOAuthクライアント登録・コンテナイメージのビルド・Kubernetes/Cloud Runでの実運用・シークレットローテーション・アップグレードという一連のライフサイクルを回すことになります。本記事はそのライフサイクル全体を、選択が必要な最初の2段階(IdP設定・デプロイ方式)と、動き始めてから参照する運用面(ログ・ローテーション・アップグレード・セキュリティ)に分けて扱います。

ステップ1: IdPにOAuthクライアントを登録する

リダイレクトURIをhttps://<ゲートウェイのホスト>/oauth/callbackとする単一のOAuth/OIDC Webアプリケーションを登録し、ゲートウェイへのアクセスを許可するユーザーまたはグループに割り当てます。IdPは次の3条件を満たす必要があります。

  • /.well-known/openid-configurationをHTTPSで配信していること(本番。ループバックの発行者はさらにCLAUDE_GATEWAY_ALLOW_LOOPBACK=1が必要)
  • 認可コードフローに対応していること(PKCEは既定で有効。対応しないIdPではoidc.use_pkce: false)
  • id_tokenまたはuserinfoエンドポイントでemailと、必要ならgroupsを返せること

Okta・Microsoft Entra ID・Google Workspaceはクレームの扱いに差があります。Oktaの組織認可サーバー(https://example.okta.com)はid_tokenが薄くuserinfo_fallback: trueが必須ですが、カスタム認可サーバーなら不要です。EntraはグループをオブジェクトIDで返すためmanaged.policies.match.groupsにはGUIDを書くか、人が読める名前にはApp Rolesを使います。Googleのid_tokenにはグループクレームが無く、oidc.google_groupsでAdmin SDK Directory APIから取得するか、無ければallowed_email_domainsとメールドメインベースのポリシーに頼ります。Googleはoffline_accessスコープも無視するため、リフレッシュトークンを得るにはscopes: [openid, profile, email]extra_auth_params: { access_type: offline, prompt: consent }を設定します。

Oktaの組織認可サーバーとGoogle Workspaceでは、oidcブロックが次のように変わります。

# Okta(組織認可サーバー)
oidc:
  issuer: https://example.okta.com
  client_id: 0oa1example2
  client_secret: ${OIDC_CLIENT_SECRET}
  allowed_email_domains:
    - example.com
  userinfo_fallback: true    # id_tokenがemail/groupsを落とすため/userinfoで補う
# Google Workspace
oidc:
  issuer: https://accounts.google.com
  client_id: ${OIDC_CLIENT_ID}
  client_secret: ${OIDC_CLIENT_SECRET}
  scopes: [openid, profile, email]    # offline_accessは無視されるため外す
  extra_auth_params:
    access_type: offline              # リフレッシュトークンはこちらで要求する
    prompt: consent

リフレッシュトークンはサイレントなセッション更新とデプロビジョニングの両方を支えています。IdPでユーザーを無効化すると次回のリフレッシュが失敗し、session.ttl_hours以内にセッションが終了します。IdPがリフレッシュトークンを発行できない場合はセッション更新のたびにブラウザへ戻されるため、ttl_hoursを8〜12に伸ばすのが実務的ですが、その分デプロビジョニングの反映も遅くなるトレードオフがあります。

ステップ2: デプロイ方式を選ぶ

ゲートウェイはPostgresで状態を共有するステートレスなLinuxバイナリなので、組織が他のステートレスサービスをデプロイする方法にそのまま乗せられます。決めるべき点は4つです。

論点内容
コスト内容別ライセンスやseat課金は無し。既存の推論契約分の費用と、実行に使うコンピュートのみ
バイパス内容ゲートウェイはモデルへの唯一の経路であることを強制しない。独自クレデンシャルを持つ開発者は直接プロバイダーを呼べるため、塞ぐにはネットワークポリシー側でapi.anthropic.com等へのegressを制限する必要がある。ただしこれは各開発者マシンが行うWebFetchのドメイン安全性チェックも壊すため、managed policyでskipWebFetchPreflight: trueを設定する
複数ゲートウェイ内容それぞれ独立したデプロイで、CLIはゲートウェイのホスト名ごとに信頼とクレデンシャルを保持する。複数のOIDC発行者に対応するには別インスタンスを立てる
サーバーレス内容Cloud Runはmin-instances: 1を設定すればコールドスタートでのOIDCディスカバリー遅延を避けられる。Lambda・Cloud Functionsは長時間稼働のHTTPサーバーである必要があるため使えない

本番トポロジーでは、Ingress・Cloud Runのフロントエンド・ALBのようなL7プロキシが平文HTTPのレプリカの前段に立ちます。listen.trusted_proxiesにプロキシの送信元レンジを設定するとX-Forwarded-ForからクライアントIPを読み取れるようになり、設定しないと全リクエストがプロキシのIPから来たように見え、IP単位のレート制限が1つのバケットに集約されてしまいます。プロキシのアイドルタイムアウトは、ゲートウェイのキープアライブ間隔(Anthropic API以外のupstreamでは約15秒ごとのSSE ping)より長く設定します。

コンテナイメージ

標準のClaude Codeリリースからネイティブのclaudeバイナリを組み込んだイメージを自分でビルドします。固定したリリースからイメージアーキテクチャに合うLinuxビルドをダウンロードし、GPG署名済みのmanifest.jsonで検証してからビルドコンテキストに組み込みます。ビルドが公開のリリースホストへ到達できない環境では、内部レジストリへミラーしてフリート全体でバージョンを固定します。

イメージにはバイナリ以外に3点が必要です。

  • glibcベースのイメージ(muslベースの場合はlinux-x64-musl等の追加ビルドとパッケージが必要)
  • 書き込み可能な状態ディレクトリ(最小イメージにはホームが無いためCLAUDE_CONFIG_DIRを書き込み可能なパスに設定)
  • コンテナのコマンドをclaude gateway --config /etc/claude/gateway.yamlとし、設定ファイルは読み取り専用でマウント、シークレットは環境変数で渡す

ゲートウェイはlisten.port(既定8080)でリッスンします。実際のDeployment定義とCloud Runの起動コマンドはそれぞれ次節で示します。

Kubernetes

他のステートレスサービスと同様にDeploymentとして稼働させます。設定はConfigMapから、シークレットはSecretからマウントし、YAML内では${file:/path/to/secret}または環境変数として参照します。TLSはIngressで終端しlisten.public_urlをIngressのホスト名に設定します。readinessプローブはGET /readyz、livenessプローブはGET /healthzに向け、Podのspecには次のように書きます。

containers:
  - name: claude-apps-gateway
    readinessProbe:
      httpGet:
        path: /readyz
        port: 8080
      periodSeconds: 10
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      periodSeconds: 10

クラウドのワークロードアイデンティティを静的キーより優先し、クロスクラウドの組み合わせ(GKE上でAmazon Bedrockをupstreamにする等)ではupstreamのauthブロックに明示的なクレデンシャルを設定します。

Cloud Run

listen.portは既定の8080のままかport: ${PORT}に設定します。public_urlには外部から到達可能なオリジンを設定しますが、/loginは公開アドレスを拒否するため、*.run.appのURLはブラウザでのスモークテストにしか使えません(Private Service ConnectとCloud DNSのプライベートゾーンで*.run.appがプライベート解決される構成を除く)。本番では通常、内部ロードバランサーのホスト名をpublic_urlに使います。設定はシークレットボリュームとしてマウントし、初回リクエストでのOIDCディスカバリー遅延を避けるためmin-instances: 1を設定します。

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

ゲートウェイが稼働したら、forceLoginMethodforceLoginGatewayUrlparentSettingsBehavior: "merge"をMDMまたはOS別のmanaged-settings.json経由で各開発者のマシンへ配信します。これが無いと/loginは標準のアカウント選択画面を表示し、ゲートウェイの選択肢が出ません。詳細なキーの意味はmanaged settingsガイドを参照してください。ゲートウェイを既存のBedrockセットアップと切り替える計画がある場合は、/setup-bedrockウィザードとの使い分けも事前に確認しておくと移行の手戻りを避けられます。

運用: ログとヘルスチェック

ゲートウェイはstderrに2種類のJSON形式のログを書き出します。監査イベント(session.mintdevice.authorizeinferenceadmin.limit.upsert等、セキュリティに関わるイベント1行ごとに1エントリ)と、人間が読める[gateway]接頭辞の運用ログ(起動・警告・upstreamエラー)です。運用ログの詳細度はCLAUDE_GATEWAY_LOG_LEVEL環境変数(debug/info/warn/error、既定info)で制御できます。debugではid_tokenのクレーム名(値ではなく名前のみ)もログに出るため、PIIを出さずにgroups_claimemail_claimの設定を診断できます。

GET /healthzはliveness、GET /readyzはストアの到達性まで確認するreadinessです。どちらもaccess_control.allow_cidrsの対象外なので、アクセスを絞ったリスナーでもプローブは動き続けます。

運用: 障害時の挙動

Postgresが落ちても、ゲートウェイプロセス自体はサインイン済み開発者へのサービスを継続できます。ベアラートークンはJWTシークレットでローカルに検証され、セッションの更新もストアに触れないためです。一方で新規サインインはPostgresが復旧するまで失敗し、支出上限の強制は既定でフェイルオープン(推論は継続)になります。/readyzは障害中not-readyを報告するため、readinessでトラフィックを制御するオーケストレーターは全レプリカを一斉にローテーションから外し、ゲートウェイが継続できたはずの推論も含めてロードバランサー側で失敗します。サインイン済み開発者を障害中も動かし続けたい場合は、readinessプローブを/healthzに向ける選択肢もありますが、その場合readyと報告するレプリカへの新規サインインは失敗し続けます。IdPが落ちた場合はttl_hoursまで既存セッションは動き、新規ログインとリフレッシュは失敗します。

運用: シークレットローテーション

署名鍵のローテーションは既存セッションを無効化しない3ステップです。新しいシークレットを生成してsession.jwt_secret配列の先頭に追加し、デプロイをロールします(新しいトークンは新シークレットで署名され、古いトークンも引き続き検証される)。ttl_hoursに余裕を持たせた時間が経過してから、古いシークレットを配列から外して再度ロールします。配列に古いシークレットを残さず一気に置き換えると、その時点で稼働中の全セッションが即座に無効になります。個別の開発者を強制的にログアウトさせる手段は無く(ベアラートークンはローカル検証のためセッション単位の失効機能が無い)、個別オフボーディングはIdP側でユーザーを無効化しttl_hours以内の自然な失効を待つ形になります。

TLS証明書のローテーションは開発者体験への影響が別種です。CLIはホスト名ごとに初回接続時の証明書フィンガープリントをピン留めするため、証明書を更新するたびに全開発者へ信頼プロンプトが再表示されます。事前周知なしに実施すると「見慣れない警告が出た」という問い合わせが集中するため、計画されたイベントとして扱います。

運用: Postgresとアップグレード

ゲートウェイはkv(デバイス許可とレート制限カウンター)・spendspend_limitsadmin_auditprincipal_emailsの5テーブルと_migrationsテーブルを保持します。支出上限を使わない構成ではkvだけが書き込まれます。30秒ごとのループが期限切れのkv行を削除し、1時間ごとのスイープが支出系テーブルの保持期間を強制するため、テーブルサイズは無制限には増えません。支出上限を使う構成ではデータベースの喪失が単なる再ログインではなく支出追跡と上限設定自体の喪失を意味するため、定期バックアップが前提になります。

レプリカはステートレスなので、いつでもローリング再起動して問題ありません。ゲートウェイは起動時に自分でスキーママイグレーションを実行するため、新しいバイナリをデプロイするだけでデータベースが自己マイグレーションされます。複数レプリカが同時に起動してもPostgresのアドバイザリロックで直列化され、マイグレーションは1度だけ適用されます。マイグレーションは追記のみなので、より少ないマイグレーションしか知らない旧バイナリへロールバックしても安全です。ただし新しいリリースで追加されたキーを設定に使っていると、旧バイナリのスキーマ検証で起動が失敗するため、ロールバック前にそのキーを設定から外します。

セキュリティ: データフローと脅威モデル

推論(プロンプト・応答)はConfigured upstreamがAnthropic APIの場合のみAnthropicへ送られ、それ以外のupstream(Bedrock等)では送られません。テレメトリ・ID情報・managed settings・監査ログはいずれもAnthropicへは送られず、組織が設定した宛先のみに流れます。ただしこれはゲートウェイのデータプレーンに限った話で、開発者の各マシン側には別経路のトラフィックが残ります。CLIはサインイン後こそ自身の利用分析・エラーレポートを止めますが、初回サインイン前は起動イベントをAnthropicへ送るため、DISABLE_TELEMETRYをクライアント側managed settingsで配信しないと止まりません。WebFetchのホスト名安全性チェックとバージョンチェックも各開発者マシンからAnthropicへ送られ続け、CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1とmanaged policyのskipWebFetchPreflight: trueを設定して初めて止まります。

脅威モデルは、ゲートウェイがネットワーク境界の内側にありつつ、開発者の個々のラップトップは信頼しないという前提で設計されています。開発者は生の上流キーではなく短命なJWTを保持し、CLIとゲートウェイ間はRFC 8628のデバイスグラント、ゲートウェイとIdP間の認可コード交換は既定でPKCEを使うため、傍受された認可コードは単体では使えません。/deviceページで入力するuser_codeは20文字のアルファベットから8文字を選ぶため組み合わせは約256億通りあり、10分で失効します。これに加えてゲートウェイはデバイス認可エンドポイントにrate_limitsでIP単位のレート制限をかけており、ブルートフォースへの耐性はこの2つの組み合わせで成り立っています。ゲートウェイからの外向きリクエストはSSRF(サーバーサイドリクエストフォージェリ)ガードを通り、DNS解決・リンクローカルとクラウドメタデータアドレスのブロック・解決済みIPへの接続ピン留めを行うため、IdPやOTLP宛先のような運用者が指定するURLがクラウドメタデータエンドポイントへリダイレクトされる経路を塞いでいます。ゲートウェイホスト自体の侵害と、悪意あるOIDCプロバイダーの2つは、組織自身のインフラ・IdPセキュリティの責任範囲として脅威モデルの対象外です。

よくあるつまずき

  • public_urlが内部のhttp://のまま: ALBの背後で動かしているのにpublic_urlをローカルまたは内部のオリジンのままにすると、IdPへ渡すredirect_uriが誤り、サインインがALB経由で失敗する
  • IMDSv2のホップ制限でBedrockが502: EC2上でIMDSv2の既定ホップ制限(1)がコンテナ内からのインスタンスメタデータ要求を塞ぎ、Could not load credentials from any providersが出る。起動と/readyzは通過するのは、AWS SDKがクライアント構築時ではなく最初のリクエストでインスタンスクレデンシャルを解決するため。ホップ制限を2に上げるか、クレデンシャルをECSタスクロールから取得する構成に切り替える
  • trusted_proxies未設定によるレート制限の集約: 前段プロキシを置いたのにlisten.trusted_proxiesを設定し忘れると、全リクエストがプロキシのIPから来たように見え、IP単位のレート制限が1つのバケットに集約されて意図せず厳しくなる
  • 証明書ローテーションの周知漏れ: TLS証明書を更新すると全開発者に信頼プロンプトが再表示される。事前周知なしに更新すると問い合わせが集中する
  • 共有NAT配下でサインインが失敗する: rate_limitsの既定値はデバイス認可エンドポイントが単位時間あたり30回、user_code検証が10回で、単一の出口IPを共有する拠点やVPNではこの上限にすぐ達する。オフィス全体が1つのegress IPを共有する大規模組織では、rate_limitsを組織の同時サインイン規模に合わせて引き上げる

まとめ

本番デプロイはIdPへのOAuthクライアント登録、コンテナイメージのビルド、Kubernetes/Cloud Runでの稼働という順で組み立て、稼働後はログ・ヘルスチェック・障害時の挙動・シークレットローテーション・アップグレードという運用サイクルに入ります。ステートレスなレプリカとPostgresでの状態共有という設計のおかげでローリング再起動やロールバックは比較的安全ですが、支出上限を使う構成ではPostgresのバックアップが単なる可用性の問題ではなくデータ保全の問題になる点は覚えておく価値があります。設定項目の詳細は設定リファレンスを参照してください。

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