Claude Codeセルフホスト環境でオンデマンドRunnerを起動する
固定Runner群の代わりに、セッションが来るたびにRunnerを1台起動する構成です。orchestratorとspawn-runnerフックの契約、監視の要点を確認します。
Claude Codeセルフホスト環境は、常時起動の固定Runner群だけでなく、セッションが来るたびにRunnerを1台起動する「オンデマンドRunner」構成にも対応します。実体はorchestratorというサブコマンドで、Anthropicにスポーン要求をポーリングし続け、要求のたびに自作のspawn-runnerフックを呼びます。フックはKubernetesのJob・EC2インスタンス・Nomadディスパッチといった自社の実行基盤にワークロードを投入する役割を担います。固定Runner群との使い分けと、spawn-runnerフックが守るべき契約を確認します。
オンデマンドRunnerが解決する課題
固定Runner群では、environment secretがすべてのRunnerホストに常駐します。そのホストはユーザーのセッションコードも実行する場所なので、セッションから読める状態になります。オンデマンドRunnerでは、environment secretはユーザーコードを一切実行しないオーケストレーターホストだけに留まります。スポーンされた各Runnerが受け取るのは、そのRunnerを1台だけ登録して失効する使い捨てのwork orderです。認証情報の露出面を、常駐ホスト全部からオーケストレーター1箇所に絞り込む構成です。
前提として、実行基盤へワークロードを投入できるスクリプト(spawn-runnerフック)をあらかじめ用意しておく必要があります。それを使ってオーケストレーターを1つのEnvironmentに対して立てます。
オーケストレーターを起動する
environment secretと、実行可能なspawn-runnerスクリプトを含むフックディレクトリを渡して起動します。
claude self-hosted-runner orchestrator \
--environment-secret-file /etc/claude/environment-secret \
--hooks-dir /etc/claude/hooksオーケストレーターはポーリング間で状態を持ちません。同じEnvironmentに対して2台以上のレプリカを冗長構成で動かせます。各スポーン要求はサーバー側でちょうど1つのレプリカにしか割り当てられません。次の5つのフラグは把握しておく価値があります。
| フラグ | 既定値 | 内容 |
|---|---|---|
--hook-timeout <sec> | 既定値60 | 内容フックのプロセスツリーをこの秒数で終了させる。5秒のkill猶予を足した値が--expected-spawn-secondsを下回っている必要があり、起動時にオーケストレーターが検証する |
--expected-spawn-seconds <sec> | 既定値120 | 内容スポーンされたRunnerの起動時間のp99見込み(サーバー側で10〜3600秒の範囲を強制)。この時間内にRunnerが登録しないと、新しいorder IDで再要求される |
--min-idle <n> | 既定値0 | 内容アイドルのセッションスロットを最低N個確保するため、待機用Runnerを先回りで起動する。Runner側の--exit-if-unused-minと組み合わせて余剰の待機Runnerを自己解放させる |
--hook-concurrency <n> | 既定値4 | 内容並列で走らせるspawn-runnerフックの上限。1回のポーリングでクレームするスポーン要求数の上限も兼ねる |
--debug-dir <path> | 既定値未設定 | 内容スポーン要求ごとのwork orderとフックのstderrをディスクに書き出す。デバッグ専用で、本番では設定しない |
--expected-spawn-secondsはすべてのレプリカで同じ値を使う必要があります。サーバー側のリース期間として扱われるためです。orchestratorサブコマンドは--api-url・--environment-secret-file・--hooks-dir・--health-port・--log-levelをRunnerと共通のフラグとして受け取ります。ただし--hooks-dirだけはRunnerと違い必須で、中に実行可能なspawn-runnerスクリプトが無いと起動しません。
spawn-runnerフックが受け取る環境変数
オーケストレーターはスポーン要求1件につき${hooks-dir}/spawn-runnerを1回呼びます。フックは--hook-timeout(既定60秒)以内に、Runnerの起動を待たずに非同期でワークロードを投入して戻る必要があります。
| 変数 | 内容 |
|---|---|
CLAUDE_RUNNER_WORK_ORDER_FILE | 内容署名済みwork order JWTを含む一時ファイルへのパス。フック終了後に削除される |
CLAUDE_RUNNER_ORDER_ID | 内容スポーン要求ごとに一意な冪等性キー。Kubernetesのリソース名にそのまま使える形式 |
CLAUDE_RUNNER_SESSION_ID | 内容対象セッションのID。プリウォーミング要求では空になるため、値が入っている前提でコードを書かない |
CLAUDE_RUNNER_ATTEMPT | 内容このセッションに対する何回目のスポーン要求か。プリウォーミング要求では0 |
CLAUDE_RUNNER_POOL_ID | 内容新しいRunnerが参加すべきEnvironmentのID(ccpool_...形式) |
CLAUDE_RUNNER_PRIMARY_REPO_URL | 内容セッションの最初のgitソースのURL。そのリポジトリを事前ウォームしたRunnerへのルーティングに使う |
CLAUDE_RUNNER_ACCOUNT_EMAIL | 内容セッションを起票したアカウントのメールアドレス。個人情報として扱いログに残さない |
起動したRunnerをwork orderで登録する
スポーンされたRunnerは、environment secretの代わりにwork orderで登録します。3点押さえておきます。
- work orderで起動する:
--environment-secret-fileにwork order JWTを含むファイルを指定するか、SELF_HOSTED_RUNNER_ENVIRONMENT_SECRETにJWTの値を直接設定します - フックが終了する前にJWTをコピーする: オーケストレーターはフック終了後にwork orderファイルを削除します。ファイルパスをそのまま渡すのではなく、投入するワークロード(KubernetesのSecretなど)にJWTの中身をコピーします
- スポーンしたRunnerは
--capacity 1にする: セッションに紐づいたwork orderはちょうど1台のRunnerだけを登録するため、--capacityを上げても処理されないスロットが増えるだけで、Runnerは起動時に警告をログに出します
プリウォーミング要求で起動する待機Runnerだけは例外です。どのセッションにも紐づかない状態で登録され、固定Runner群と同じようにキューされた仕事を拾います。
spawn-runnerフックが守るべき4つの契約
プロビジョナーの実装を問わず、spawn-runnerフックは次の4つを守る必要があります。
CLAUDE_RUNNER_ORDER_IDに対して冪等であること: 同じ要求が再配信されても、起動するRunnerは最大1台に留めます。IDから決定論的なリソース名を導出し、重複をプラットフォーム側に拒否させます- ワークロード自体をリトライしないこと: 1つのorder IDにつき作成するワークロードは最大1つです。Runnerが登録しなければ、Anthropicが
--expected-spawn-secondsの経過後に新しいorder IDで再要求します - 終了コードの契約を守ること:
0は投入成功、1はリトライ可能な失敗(セッションはバックオフして再提示される)、2以上はリトライ不可(Ownerが環境のActivityタブでRetryを選ぶまでセッションは再スポーンをブロックされる)を意味します。非ゼロ終了時はstderrの末尾が失敗理由として表示されるため、実用的なエラーだけを書き、認証情報は書きません --expected-spawn-secondsをp99起動時間以上に設定すること: サーバー側のリース期間なので、全レプリカで同じ値を共有します
プリウォーミング要求には失敗させるべきセッションが存在しません。フックが非ゼロで終了しても、オーケストレーターはローカルにログを残すだけで、サーバー側はリース期間の経過後にプリウォーミングを再要求します。
詰まったキューをどう見つけるか
フックが標準出力・標準エラーに書いた内容は、認証情報が自動的にマスクされた状態でオーケストレーターのログに残ります。セッションがキューに滞留している場合は、オーケストレーターの/healthzエンドポイントのボディでキューの件数を確認し、続けてCloud environments管理ページのActivityタブを開きます。失敗したセッションを展開するとスポーンエラーの内容が分かり、Retryでスポーンを再要求できます。
/healthzは常にHTTPステータス200を返すため、死活監視はステータスコードではなくボディのconnectedフィールド(直近のポーリングが成功したか)で判定します。ボディにはさらに状態別のスポーンキュー件数がqueue_countsとして入ります。SCMコネクタを設定している場合はscm_connector_connectedフィールドも追加されます。加えてconnected・last_connected_at・last_error・reconnects・requests_forwardedを含むscm_connectorオブジェクトも入ります。--scm-connector-hostを設定していなければ両方ともnullです。
SCMコネクタでGitHub Enterprise Serverに到達する
オーケストレーターは、Anthropicのコントロールプレーンに対して常設のWebSocket接続を持つこともできます。目的は、セッション開始前にホスト側で動く処理から、自社ネットワークの内側にしか届かないGitHub Enterprise Serverへ到達させることです。対象はリポジトリピッカーやブランチ・ref解決といった処理です。--scm-connector-hostを設定しない限りこの接続は張られません。
必須のフラグは2つです。--scm-connector-host <host[:port]>(ポート省略時は443)と、--scm-connector-id(自社のGitHub Enterprise Server接続の数値ID。Anthropicのアカウントチームに確認)です。プロバイダーを識別する--scm-connector-providerは既定ghe、TLS接続に追加のCA証明書が要るなら--scm-connector-ca-fileを渡します。コネクタはオーケストレーターが持つenvironment secretで認証します。接続が切れたときは指数バックオフで、別レプリカが接続を保持している場合は固定30秒間隔で再接続します。
オーケストレーターの状態をPrometheusで監視する
オーケストレーターは--health-port(Runnerと共通、既定8080)の/metricsに自身のPrometheus系列を、/healthzに人間が読めるJSONを出します。オートスケーリング基盤に組み込むなら、次の系列を押さえます。
| 系列 | 意味 |
|---|---|
claude_code_self_hosted_orchestrator_connected | 意味直近のポーリングが成功していれば1。失敗した瞬間0に落ちる。レプリカの死活監視はステータスコードでなくこの値で判定する |
claude_code_self_hosted_orchestrator_pool_pending_sessions | 意味Environment全体でRunnerを待っているセッション数。全レプリカで同じ値になるEnvironment単位の集計値なので、HPAやKEDAへ食わせるときはSUMではなくMAXを使う |
claude_code_self_hosted_orchestrator_queue_circuit_broken_sessions | 意味OwnerのRetry待ちでブロックされているスポーン要求数。0より大きい状態はそのままアラートにする価値がある |
claude_code_self_hosted_orchestrator_spawn_hooks_total{result} | 意味spawn-runnerフックの結果をok・retryable・non_retryable別に積算する。プリウォーミングや同一セッションへの再スポーンでセッション数とは一致しないため、成否比率の監視に使う |
claude_code_self_hosted_orchestrator_last_poll_age_seconds | 意味直近のポーリング試行(成功・失敗を問わない)からの経過秒数。Runner側の同名メトリクスは直近の成功からの経過秒数を測る点が異なるので混同しない。--hook-timeoutに余裕を足した値(既定構成でおよそ90秒)を超えたらアラートにする |
claude_code_self_hosted_orchestrator_pool_pending_sessionsはEnvironment単位の集計、queue_pending_sessionsはそのレプリカが今すぐクレームできる件数という違いもあります。スケーリングの判断材料には前者、そのレプリカ自身の詰まり具合を見るなら後者、と使い分けます。
固定Runner群との使い分け早見表
| 観点 | 固定Runner群 | オンデマンドRunner |
|---|---|---|
| environment secretの置き場所 | 固定Runner群すべてのRunnerホスト(セッションコードも実行する場所) | オンデマンドRunnerオーケストレーターホストのみ(ユーザーコードは実行しない) |
| アイドル時のコスト | 固定Runner群Runnerが常駐する分のインフラ費用 | オンデマンドRunnerセッションが来るまでインフラを起動しない(--min-idleのプリウォーミングは例外) |
| 起動レイテンシー | 固定Runner群既存Runnerがすぐ拾う | オンデマンドRunner--expected-spawn-seconds分の起動待ちが発生する |
| 向いている用途 | 固定Runner群セッション頻度が高く常時稼働が前提の環境 | オンデマンドRunnerセッション頻度が低い、または認証情報の露出面を絞りたい環境 |
よくあるつまずき
- work orderファイルをそのまま参照する:
CLAUDE_RUNNER_WORK_ORDER_FILEのパスをワークロードに渡しても、フック終了後にファイルが削除されているため使えません。フックが終わる前にJWTの中身をコピーします - スポーンしたRunnerに
--capacityを1より大きく設定する: work orderは1台のRunnerしか登録しないため、余ったスロットは永遠に仕事を受け取らず、起動時に警告が出るだけで気づきにくい失敗です - フック自身でリトライを実装してしまう: オーケストレーター側がすでに
--expected-spawn-seconds経過後の再要求を担っています。フック内で追加のリトライを書くと、1つのorder IDから複数のワークロードが生まれて冪等性の契約を破ります --hook-timeoutと--expected-spawn-secondsの関係を無視する:--hook-timeoutに5秒のkill猶予を足した値が--expected-spawn-secondsを超えていると起動時に拒否されます
まとめ
オンデマンドRunnerは、orchestratorサブコマンドと自作のspawn-runnerフックの組み合わせです。environment secretをオーケストレーターホストだけに閉じ込められる分、セッション頻度が低い環境や認証情報の露出を絞りたい環境に向きます。フックは冪等性・非リトライ・終了コード・リース期間の4契約を守れば、実行基盤の種類を問わず実装できます。セッションの中身をラッパースクリプトやライフサイクルフックで作り込む話はClaude Codeセルフホスト環境をラッパースクリプトとフックで拡張するにまとめています。本番投入時のハードニングは本番運用を参照してください。