3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI エージェント × セルフホステッドランナーの落とし穴

3
Posted at

はじめに

AI エージェントを CI に載せる話をしていると、遅かれ早かれ「GitHub ホステッドランナーだと足りない」という壁に当たります。ジョブが 6 時間で切られる、社内のリポジトリやデータベースに届かない、Actions の分課金が無視できない。そこで runs-on: self-hosted に切り替える、という判断は自然です。

ところが、この切り替えには公式ドキュメントの表に出てこない副作用があります。claude-code-action は、sudoapt が使えないランナーではサブプロセス隔離のセットアップを黙ってスキップします。 ステップには continue-on-error: true が付いているので、ワークフローは緑のまま通ります。緑のまま、エージェントは隔離のない環境で PR の内容を処理します。

結論から書きます。セルフホステッドランナーに移した瞬間、「クリーンな実行環境を保証する責任」がランナー層とエージェント層の二段でこちらに移ってきます。 そして、この二段を両方手当てするための公式ガイドは、GitHub 側にも Anthropic 側にも存在しません。本記事は、その空白がどこにあるのかを一次情報から特定し、代わりに何を自分で決めるべきかを整理したものです。

なお本記事では「セルフホステッドランナー」と「自前ランナー」を同じ意味で使い分けています。公式ドキュメントの用語に触れる文脈では前者、運用側の視点で書く文脈では後者を使っています。


1. 自前ランナーに移りたくなる理由のうち、公式に裏が取れるもの

まず、動機の側を数字で押さえておきます。感覚ではなく公式の記述で確認できるものは、実のところ多くありません。

1.1 ジョブの実行時間上限

一番大きいのはジョブの実行時間上限です。GitHub の公式リファレンスは、両者を別々の数字として明記しています。まず GitHub ホステッドランナー。

Each job in a workflow can run for up to 6 hours of execution time.

そしてセルフホステッドランナー。

Each job in a workflow can run for up to 5 days of execution time.

エージェントに大きめのリファクタリングやテスト修復を任せると、数時間単位のジョブは現実的に起こります。6 時間 → 5 日は、本テーマで公式に確認できる唯一かつ決定的な差分だと筆者は考えています。

ただし後述するとおり、この差分は設定を一行足さないと手に入りません(「ハマりどころ」参照)。

1.2 社内ネットワークへの到達性

Anthropic は「セルフホステッド環境」のページで、自社インフラで動かす動機を 3 点にまとめています。

内部サービスやレジストリへインターネットに晒さずに到達できること、ランナーイメージに社内 CLI を焼き込めること、チェックアウトと成果物が自社管理下に留まること。この 3 点はそのまま CI のセルフホステッドランナーを選ぶ理由と重なります。

面白いのは、同じページで Anthropic 自身がランナーという語を CI のそれと同一視している点です。

  • Runner: a program running on hosts inside your network. Runners execute the sessions; the idea is the same as a self-hosted CI runner.

(同上)

「考え方はセルフホステッド CI ランナーと同じ」と書かれています。エージェント実行基盤の設計問題は、CI ランナーの設計問題とほぼ同型だという認識が、ベンダー側にも共有されているわけです。

1.3 コスト

セルフホステッドランナーは Actions の分課金が発生しません(GitHub Docs: About billing for GitHub Actions)。マシン費は自己負担なので単純な勝ちではありませんが、長時間ジョブを回すほど効いてきます。


2. claude-code-action は「あなたのランナー上で完結する」

動機の裏付けが取れたので、実行される側を見ます。claude-code-action の README には、実行場所についての明快な一文があります。

The action executes entirely on your own GitHub runner (Anthropic API calls go to your chosen provider)

GitHub: anthropics/claude-code-action README

推論だけが選んだプロバイダ(Anthropic API / Amazon Bedrock / Google Vertex AI / Microsoft Foundry)に飛び、それ以外はすべてランナー上で動く、という設計です。

これは安心材料であると同時に、そのまま責任の所在を示しています。ランナーの素性が、エージェントの実行環境の素性そのものになるということです。GitHub ホステッドランナーは毎回クリーンな VM が割り当てられ、必要なランタイムがプリインストールされています。自前ランナーはそうではありません。

ちなみに、README を通読しても「self-hosted」という語は出てきません。ネットワーク制限やプロキシ設定の節もありません。ここが本記事の後半につながります。


3. 本題:サンドボックスが黙って外れる

3.1 action.yml に唯一存在するセルフホステッド言及

claude-code-action のソースで「self-hosted」を検索すると、action.yml のコメント 1 行に行き当たります。書き込み権限のないユーザーからのコンテンツを処理する際の、サブプロセス隔離パッケージのインストール手順です。

- name: Install subprocess isolation dependencies
  # Install subprocess isolation dependencies when processing content from non-write users.
  # Best-effort: skips on non-Linux or when sudo/apt unavailable (self-hosted runners).
  if: ${{ inputs.allowed_non_write_users != '' && runner.os == 'Linux' }}
  continue-on-error: true

GitHub: claude-code-action action.yml

「ベストエフォート: 非 Linux、あるいは sudo/apt が利用できない場合(セルフホステッドランナー)はスキップする」。丁寧なことに、括弧書きでセルフホステッドランナーが名指しされています。

このステップが実際にやっているのは、bubblewrapsocat の導入、そして Ubuntu 24.04 以降で非特権ユーザー名前空間を塞いでいる AppArmor の解除です。

if command -v apt-get >/dev/null && command -v sudo >/dev/null; then
  for i in 1 2 3; do
    sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends bubblewrap socat && break
# Ubuntu 24.04+ restricts unprivileged user namespaces via AppArmor.
# The sysctl doesn't exist on older kernels — that's fine.
if [ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ] && command -v sudo >/dev/null; then
  sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi

そして、このステップが提供している保護の中身は docs/security.md に書かれています。
GitHub: claude-code-action docs/security.md

サブプロセス環境からのシークレット除去と、bubblewrap が使える Linux ランナーでの PID 名前空間分離。この 2 つが、sudo/apt の無いランナーでは静かに失われます。 しかも continue-on-error: true なので、失われたことはワークフローの成否に現れません。

セキュリティ機構が「エラーで落ちる」のではなく「無いまま成功する」のは、運用上いちばん厄介な壊れ方だと筆者は思います。ジョブは緑になり、ログを深く追わないと気づかず、そして守られていません。

3.2 OpenAI も同じ問題を、もっとはっきり書いている

同じ落とし穴は Anthropic 固有のものではありません。openai/codex-action の README は、より明示的に書いています。

  • GitHub-hosted Linux runners: The action enables unprivileged user namespaces during setup and clears Ubuntu's AppArmor gate when present. This avoids the bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted failure seen on newer hosted images, including workflows that use the action once to bootstrap Codex and then call codex in later steps. Self-hosted Linux runners still need equivalent kernel support configured ahead of time.

GitHub: openai/codex-action README

最後の一文が本記事の核心です。「セルフホステッド Linux ランナーでは、同等のカーネルサポートを事前に構成しておく必要がある」。

Codex 側は権限戦略も明示的で、自前ランナー運用者向けの選択肢を用意しています。

  • unprivileged-user — Runs Codex as the user provided via codex-user. Use this if you manage your own runner with a pre-created unprivileged account. Ensure the user can read the repository checkout and any files Codex needs.

デフォルトの drop-sudo はジョブ実行中に sudo 権限を剥奪する戦略なので、GitHub ホステッド向けです。自前ランナーなら、あらかじめ非特権ユーザーを用意して unprivileged-user を選べ、という設計になっています。「事前に用意しておく」という前提が、自前ランナー側の運用に丸ごと乗っているわけです。

3.3 ランタイムの前提すら変わる

もう 1 つ、docs/security.md に見逃せない記述があります。

Note that the runtime executing the tool also reads project config. bunx <tool> runs the tool's script under node when node is on PATH (as it is on GitHub-hosted runners); when only Bun is available, Bun executes the script itself and reads bunfig.toml from the checkout — including preload entries — which comes from the PR head. On such runners, make sure node is on PATH for the hook, and treat bunfig.toml and .npmrc in the checkout as PR-controlled runtime config.

GitHub: claude-code-action docs/security.md

nodePATH にあるかどうか(GitHub ホステッドではある)で、bunx の実行主体が変わります。Bun しか無い環境では Bun 自身がスクリプトを実行し、その際にチェックアウト内の bunfig.toml を、preload エントリを含めて読みます。そしてそのチェックアウトは、PR の head 由来です。

つまり、node の入っていない自前ランナーでは、PR 作成者が bunfig.toml にコードを仕込める経路が開きます。公式ドキュメントが「GitHub ホステッドと自前ランナーで前提が変わる」ことを認めている数少ない箇所であり、その差分が素直に攻撃面になっている例です。


4. 「公式ガイドが存在しない」を一次情報で確認する

ここまでの話は、公式ドキュメントに「セルフホステッドランナーで AI エージェントを動かす」という節があれば、そこにまとめて書かれているはずのものです。筆者はそれを探しました。結果として確認できたのは、以下のとおりです。

  • claude-code-action の README・docs/*.md に、runs-on: self-hosted を前提としたまとまった節はありません。言及は 3.1 で引いた action.yml のコメント 1 行と、カスタム実行ファイルに関する FAQ 程度です。
  • Anthropic 公式ドキュメントの GitHub Actions 節も、コストの説明を含めて GitHub ホステッド前提で書かれています。
  • GitHub 側にも、GPU を積んだセルフホステッドランナーの公式構築手順は見つかりませんでした。公式ドキュメントで GPU が出てくるのは GitHub ホステッドの larger runner(Tesla T4 / VRAM 16 GB)だけです(GitHub Docs: Larger runners reference)。
  • Kubernetes で回す場合の Actions Runner Controller についても、GitHub はサポート範囲を明確に区切っています。Autoscaling Runner Sets モード以外は "legacy" 扱いで、Kubernetes 周りや pod テンプレート、GPU のスケジューリングはサポート対象外と明記されています(GitHub Docs: Support for Actions Runner Controller)。

「無いことの証明」は原理的に難しいので、ここは筆者の探索範囲でそれらしい記述に到達できなかった、という表現に留めます。とはいえ、両社のドキュメントを一次情報で当たった限り、この領域が公式のベストプラクティスの空白地帯であることは、実務上そう扱ってよさそうです。

そして GitHub は、この空白の手前でかなり強い警告を出しています。
GitHub Docs: Secure use reference

「誰でも PR を開けて環境を侵害できるから、パブリックリポジトリではほぼ絶対に使うべきでない」。AI エージェントに PR の内容を読ませて作業させる、という用途は、この警告が想定している攻撃面のど真ん中です。


5. 空白を自分で埋めるときの判断軸

公式ガイドが無い以上、決めるのはこちらです。一次情報で裏の取れる範囲で、筆者なら次の順で潰します。

5.1 まず、パブリックリポジトリでは使わない

これは判断ではなく前提です。4 章で引いた警告のとおりです。

5.2 ランナーをエフェメラルにする

GitHub は永続ランナーでのオートスケールを明確に非推奨としています。

autoscaling with persistent self-hosted runners is not recommended.

GitHub recommends implementing autoscaling with ephemeral self-hosted runners.

(いずれも GitHub Docs: Self-hosted runners reference

--ephemeral フラグか Just-in-Time ランナー(--jitconfig)を使い、1 ジョブごとに破棄する構成にします。Kubernetes なら ARC の Autoscaling Runner Sets モード一択です(GitHub Docs: Actions Runner Controller)。

エージェントは大量のファイルを書き、パッケージを入れ、キャッシュを汚します。ジョブ間で状態が残る構成とは相性が最悪です。

5.3 サンドボックスの前提をランナーイメージ側で満たす

sudo/apt に頼らず、ランナーイメージの時点で必要なものを揃えておきます。

  • bubblewrapsocat をプリインストールしておく
  • 非特権ユーザー名前空間が使える状態にしておく(Ubuntu 24.04 以降なら kernel.apparmor_restrict_unprivileged_userns の扱いを決めておく)
  • nodePATH に置く(3.3 の bunfig.toml 経路を塞ぐため)
  • Codex を併用するなら、非特権ユーザーを作って safety-strategy: unprivileged-user を指定する

そのうえで、隔離が効いているかをジョブの中で検証するステップを足すのが実務的だと筆者は考えます。continue-on-error: true で握り潰される保護を、こちらから明示的に確認しに行く発想です。

- name: Verify sandbox prerequisites
  run: |
    set -euo pipefail
    command -v bwrap >/dev/null || { echo "bubblewrap missing"; exit 1; }
    command -v node  >/dev/null || { echo "node not on PATH"; exit 1; }
    # 非特権ユーザー名前空間が実際に使えるかを確認する
    # --unshare-user を必ず付ける(無いと setuid 版 bwrap で通ってしまい、確認したいことが確認できない)
    bwrap --unshare-user --unshare-pid --ro-bind / / true

--ro-bind にしているのは、検証のためにホストのファイルシステムを書き込み可能でバインドする必要がないからです。ここで見たいのは「名前空間を分離できるか」だけなので、権限は最小にしておきます。

※これは筆者が一次情報の記述から組み立てた例であり、公式が提示している検証手順ではありません。実際に導入する際は自分の環境で挙動を確かめてください。

5.4 権限モードは dontAsk を使う

CI での権限制御について、公式は --dangerously-skip-permissions ではないモードを名指ししています。

To set a baseline for the whole session instead of listing individual tools, pass a permission mode. dontAsk denies anything not in your permissions.allow rules or the read-only command set, which is useful for locked-down CI runs.

Anthropic Docs: Run Claude Code programmatically

さらに、CI では --bare が推奨されています。

--bare is the recommended mode for scripted and SDK calls, and will become the default for -p in a future release.

(同上)

--bare はフック・スキル・プラグイン・MCP サーバ・自動メモリ・CLAUDE.md の自動検出をスキップします。公式は「同僚の ~/.claude にあるフックやプロジェクトの .mcp.json の MCP サーバは実行されない」と説明しています。

自前ランナーの文脈で読み直すと、この効能は一段と大きくなります。 使い回されるランナーの ~/.claude が前のジョブで汚染されている可能性も、PR に含まれる .mcp.json が読まれる可能性も、--bare は断ちます。5.2 のエフェメラル化と合わせて、二重に効かせるべき設定です。

5.5 egress は default-deny にする

Anthropic はセルフホステッド環境の設計原則として、こう書いています。

Session code is model-directed and can attempt connections to arbitrary hosts; default-deny egress at the network layer bounds where those attempts can land.

Anthropic Docs: Deploy self-hosted environments

「セッションのコードはモデルに指示されて動くので、任意のホストへ接続を試みうる。ネットワーク層での default-deny egress が、その試行の着地点を限定する」。モデルが何をするか完全には予測できない以上、ネットワーク層で境界を引くのが筋だ、という主張です。自前ランナーは、この境界を自分で引ける立場にあります。


ハマりどころ

timeout-minutes を書かないと 5 日は手に入らない

1.1 で挙げた「5 日」は、黙って使えるわけではありません。ジョブの timeout-minutes の既定値は 360 分(6 時間)なので、明示的に上書きしない限りジョブは 6 時間で切られます。

The maximum number of minutes to let a job run before GitHub automatically cancels it. Default: 360

GitHub Docs: Workflow syntax for GitHub Actions。原文の製品名は Liquid 変数のため公開ページの表示に合わせています)

jobs:
  claude:
    runs-on: self-hosted
    timeout-minutes: 1440  # 既定は 360。上書きしないと 5 日の恩恵は受けられない

逆に、timeout-minutes をランナーの実行時間上限より大きく設定しても、上限側で打ち切られます。設定値がそのまま効くわけではない点は押さえておいてください。

さらに厄介なのは、ステップ単位の上限 360 分はセルフホステッドでも引き上げられないことです。

The maximum number of minutes to run the step before killing the process. Maximum: 360 for both GitHub-hosted and self-hosted runners.

(同上。原文の製品名は Liquid 変数のため公開ページの表示に合わせています)

長時間タスクを 1 ステップに押し込む設計だと、ランナーを変えても壁は動きません。エージェントの実行を複数ステップに割るか、ステップ内で区切る設計が要ります。

Bedrock/Vertex に寄せても api.anthropic.com は完全には消えない

推論を Bedrock や Vertex AI に寄せれば api.anthropic.com への通信を全部切れる、と考えたくなりますが、そうはなりません。WebFetch のドメイン安全性チェックが残るためで、これは skipWebFetchPreflight: true で無効化できます(Anthropic Docs: Network configuration)。閉域で allowlist を組むときに引っかかりやすいところです。

プロキシ変数は job レベル env: から渡す

claude-code-action は composite action なので、ステップの env: ブロックが呼び出し元のジョブレベル env を隠します。action.yml にもコメントで明記されています。

# MCP configuration — these env vars are read directly from process.env by the
# Claude CLI subprocess. They must be listed explicitly here because this step's
# env: block shadows the calling workflow's job-level env vars (GitHub Actions
# composite action behavior). Set these in your workflow's job-level env: or via
# a prior step that writes to $GITHUB_ENV.

GitHub: claude-code-action action.yml

ANTHROPIC_BASE_URL などはジョブレベルの env:$GITHUB_ENV 経由で渡す必要があります。なお HTTPS_PROXY のような一般的なプロキシ変数は action.yml の env 列挙に含まれていません。ランナープロセスの環境変数として設定すれば継承されそうではありますが、Action 側のドキュメントに明示的な記述は見つけられませんでした。

Docker Hub のレート制限は免除されない

GitHub ホステッドランナーはパブリックイメージの pull についてレート制限が適用されませんが、セルフホステッドは適用されます。

Pulling images from Docker Hub is always subject to the rate limit.

GitHub Docs: GitHub Actions limits

エージェント用に大きなイメージを毎ジョブ引く構成だと、ここで詰まります。社内ミラーやレジストリキャッシュを前提にした方が安全です。


参考

GitHub 公式

Anthropic 公式

OpenAI 公式

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?