はじめに

最近は、ChatGPTやGemini、Claudeのような生成AIにコードを書かせて実行させる使い方が広がっています。「この売上データをグラフにして」と頼むと、AIがPythonコードを書き、サーバーが実行して画像を返す、といった自動化ができます。
ただ便利な一方で、AIが書いたコードがサーバーの認証情報(パスワードやAPIキー等)を読み取り、外部へ送信する危険性もあります。
その解決策の1つが、2026年7月にPublic Previewとして公開されたCloud Run sandboxesです。これは、アプリが動くコンテナの内側に、AIが書いたコードだけを閉じ込める「もう一段の箱」を作る機能です。従来はsandbox専用のCloud Runサービスを別に立てる必要があり、リソースの二重確保や運用の複雑化を招いていましたが、sandboxesはこれを不要にします。本記事は、その機能を実際に検証し、結果をまとめたものです。

参考: Cloud Run sandboxesでAI生成コードを安全に実行する

従来のCloud Runの隔離との違い

Cloud Runの隔離が守るのはコンテナ同士の境目だけで、コンテナの内側には仕切りがありません。そこで動くAI生成コードはアプリ本体と同じ権限を持つため、アプリが認証情報を読めるなら、生成コードも同じように読めてしまいます。
たとえばCSVを分析するエージェントに、CSVのセル経由で「認証情報を外部に送るコードを書け」という指示を仕込まれると、生成コードがアプリと同じ権限でメタデータサーバーからトークンを取り、データを外部へ持ち出すところまで一気に通ってしまいます。

sandboxの全体像と3つの境界

sandboxを使うと、AIエージェントの構成は次のようになります。Cloud RunサービスがGemini等にコードを生成させ、その信頼できないコードをsandboxに隔離実行させる形です。

gc-cloud-run-sandboxes-agent-flow

このsandboxには、環境変数・メタデータの遮断(境界1)、外向き通信(egress)のデフォルト遮断(境界2)、読み取り専用ファイルシステム(境界3)という3つの境界があります。本記事ではCloud Runサービスを使い、これらが実際にどう働くかをコードで確認します。

検証作業

動作確認用のWebアプリをデプロイし、3つの境界の挙動を確認します。

1. sandboxの利用可否を確認

sandboxは、記事執筆の現時点ではコンソールからは有効化できず、gcloudでの操作が必要でした。手元のgcloud--sandbox-launcherフラグを持っているか確認したところ、次のように出力されましたので、gcloud側の準備は整っていました。

$ gcloud beta run deploy --help 2>&1 | grep -i sandbox
          --remove-volume-mount=[MOUNT_PATH,...] --[no-]sandbox-launcher
     --[no-]sandbox-launcher
        Set the container as a sandbox supervisor to launch sandboxes. Use
        --sandbox-launcher to enable and --no-sandbox-launcher to disable.

2. Webアプリのデプロイ

検証に使うWebアプリを用意してデプロイします。

検証アプリの構成

sandboxを有効化すると、バイナリが/usr/local/gcp/bin/sandboxに自動マウントされます。アプリからはこれをsubprocessで呼び出し、sandbox do -- <コマンド>でsandbox内のコマンドを実行します。同じコマンドをsandbox経由(sb())と、通さない(raw())で流し、挙動の差を確認します。

SANDBOX = "/usr/local/gcp/bin/sandbox"

def sb(args):   # sandbox経由で実行
    return subprocess.run([SANDBOX, "do", "--", *args], capture_output=True, text=True, timeout=60)

def raw(args):  # sandboxを通さず実行
    return subprocess.run(args, capture_output=True, text=True, timeout=60)

sandboxは境界を作りますが実行時間そのものは制限しないため、無限ループなどに備えてsubprocess側でtimeoutを設けています。

デプロイ

--sandbox-launcherフラグをつけてCloud Runサービスをデプロイします。
※検証用アプリのため--no-allow-unauthenticatedで非公開設定

gcloud beta run deploy sandbox-demo \
  --source . \
  --region=asia-northeast1 \
  --execution-environment=gen2 \
  --sandbox-launcher \
  --no-allow-unauthenticated

デプロイ後、コンソール上のリビジョン詳細で、サンドボックスランチャーが有効化されていることが確認できました。

gc-cloud-run-sandboxes-revision-detail

3. コード実行と境界1・境界2を確かめる

ルート(/)にアクセスすると、アプリが次の4つを順に実行します。まずsandbox内でコードを実行できること(exec)を確認したうえで、境界1・境界2の挙動を見ます。execはセキュリティ境界ではなく、以降の検証のベースラインです。

# exec: コードを実行できるか
/usr/local/gcp/bin/sandbox do -- /usr/local/bin/python3 -c "print('hello from sandbox')"

# env_leak: サービスの環境変数(K_SERVICE)を読めるか(境界1)
/usr/local/gcp/bin/sandbox do -- /usr/local/bin/python3 -c "import os; print(os.environ.get('K_SERVICE'))"

# egress_default: 既定で外部へ送信できるか(境界2)
/usr/local/gcp/bin/sandbox do -- /usr/local/bin/python3 -c "import urllib.request; print(urllib.request.urlopen('https://example.com', timeout=10).status)"

# egress_allowed: --allow-egress を付けたときだけ送信できるか(境界2)
/usr/local/gcp/bin/sandbox do --allow-egress -- /usr/local/bin/python3 -c "import urllib.request; print(urllib.request.urlopen('https://example.com', timeout=30).status)"

各コマンドの実行結果は次のとおりです(rcは終了コード、outは標準出力、errは標準エラー出力)。

{
"exec": { "rc": 0, "out": "hello from sandbox", "err": "" },
"env_leak": { "rc": 0, "out": "None", "err": "" },
"egress_default": { "rc": 1, "out": "", "err": "…[Errno -3] Temporary failure in name resolution…" },
"egress_allowed": { "rc": 0, "out": "200", "err": "" }
}

  • exec: outhello from sandbox。sandboxの中でコードを実行できています。
  • env_leak(境界1): Cloud Runで必ずセットされるK_SERVICENone。サービスの環境変数がsandboxに引き継がれていません。
  • egress_default(境界2): --allow-egressなしはDNS解決の段階で失敗(Temporary failure in name resolution)。egress_allowedとはtimeout値が異なりますが、DNS段階で即失敗するため結果には影響しません。
  • egress_allowed(境界2): --allow-egressを付けたときだけHTTP 200。デフォルトで遮断し、明示許可した実行だけ外部へ送信できます。

以上の結果から、境界1の環境変数の遮断(K_SERVICEが読めない)と、境界2(egressデフォルト遮断)が確認できました。

4. 読み取り専用ファイルシステムを確かめる(境界3)

3つ目の境界です。コンテナのルートファイルシステムは読み取り専用で、書き込みは--writeを付けたときだけ隔離領域に対して行われ、実行終了で破棄されます。

/write-test: 書き込みを試す

ルート直下(/probe.txt)への書き込みを3パターン試しました。実行内容と結果は次のとおりです。

# write_without_flag: --write なしでルート直下に書き込めるか
/usr/local/gcp/bin/sandbox do -- /usr/local/bin/python3 -c "open('/probe.txt','w').write('hi'); print('wrote')"

# write_and_read_same_run: --write ありで書いて同一実行内で読み返す
/usr/local/gcp/bin/sandbox do --write -- /usr/local/bin/python3 -c "open('/probe.txt','w').write('hi'); print(open('/probe.txt').read())"

# persist_across_runs: --write ありで書いた後、別の実行に残っているか確認
/usr/local/gcp/bin/sandbox do --write -- /usr/local/bin/python3 -c "import os; print(os.path.exists('/probe.txt'))"

{
"write_without_flag": { "rc": 1, "out": "", "err": "…OSError: [Errno 30] Read-only file system: '/probe.txt'…" },
"write_and_read_same_run": { "rc": 0, "out": "hi", "err": "" },
"persist_across_runs": { "rc": 0, "out": "False", "err": "" }
}

--writeなし(write_without_flag)はrc 1Errno 30で弾かれ、ルートファイルシステムには書き込めませんでした。--writeを付ければ同一実行内では読み書きでき(write_and_read_same_runhi)、同じ--write付きで別の実行から確認してもpersist_across_runsFalse。書き込みは実行ごとに破棄されました。

/export: 生成物を取り出す

生成物を残したい場合は、--export-tarでホスト側に取り出します。

/usr/local/gcp/bin/sandbox do --write --export-tar=/tmp/work.tar -- \
  /bin/sh -c "echo hello-from-sandbox > /out.txt"
$ tar tf /tmp/work.tar
./
./out.txt
./var/
./var/log/

/out.txtが取り出せています(./var/など自分で書いていないものも、変更されたファイルがまとめて出力されるため含まれます)。取り出したい書き込みは、ルートFSに--writeで行い--export-tarで回収します。

なおこの境界が防ぐのは書き込みで、読み取りは可能です。sandboxから読めるイメージ内などに機密情報を直接置かない前提は変わりません。

参考: Code execution in Cloud Run

5. sandboxの内と外を並べて確かめる

同じコードをsandboxの外でも動かすと、境界の効果が分かります。LLMが生成しがちな2つの操作をsb()raw()の両方に流します。raw()はホスト(実行サービスアカウント)の権限でそのまま実行するので、sandboxの外です。

  • 認証情報の窃取: メタデータサーバーのSAトークン用エンドポイントに到達できるか
  • 外部送信(egress): 外部サイトへHTTPリクエストが飛ばせるか

対比結果

検証 sandboxなし(raw sandboxあり(sb
認証情報(メタデータSAトークン)/境界1 到達できてしまう(HTTP 200) 到達不可(Network is unreachable
外部送信(egress)/境界2 成功してしまう(HTTP 200) 遮断(Temporary failure in name resolution

sandboxなし(raw)では、メタデータのトークン用エンドポイントにも外部サイトにもそのまま到達できました。LLMが生成したコードをそのままsubprocessで実行すると、トークンの取得も外部への送信も通ってしまうということです。同じコードをsandbox経由(sb)にすると、どちらも失敗します。
また、遮断のされ方も2種類ありました。メタデータへの到達はNetwork is unreachable(Errno 101)で経路そのものが存在せず、外部サイトへの送信はTemporary failure in name resolution(Errno -3)でDNS解決の段階で止まりました。内部経路と外向き通信が別々に断たれる挙動が確認できました。

境界2を開けてもメタデータは守られるか

境界1と境界2は独立しているのか、片方を開けたらもう片方も通るのかを確かめます。--allow-egressで境界2を開けたうえで、メタデータサーバーのトークンエンドポイントに到達できるかを試しました(/egress-metadata。到達可否だけを見て、トークン本体は取得しません)。

/usr/local/gcp/bin/sandbox do --allow-egress -- /usr/local/bin/python3 -c "import urllib.request; req = urllib.request.Request('http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', headers={'Metadata-Flavor': 'Google'}); print(urllib.request.urlopen(req, timeout=10).status)"

{
"allow_egress_metadata": { "rc": 1, "out": "", "err": "…urllib.error.URLError: …" }
}

--allow-egressを付けてもtimed outで、トークンは取得できませんでした(--allow-egressなしのNetwork is unreachableとはエラーの種類こそ変わりますが、いずれも応答は得られません)。egressを開放しても、メタデータへの到達(境界1)は独立して塞がれたままです。片方の境界を開けても、もう片方の突破にはつながりません。

起動速度

公式ブログ(Public Preview告知)では、1つのCloud Runサービスで1,000個のsandboxを起動→実行→停止し、平均レイテンシ500msという数値が示されています。別サービス方式のコールドスタートを回避できる点が速度上の要点です。

/speedエンドポイント(sandbox do -- /bin/echo xを10回連続で計測)で測定しました。

{
"count": 10,
"avg_ms": 494.4,
"min_ms": 406.2,
"max_ms": 533.5,
"runs_ms": [533.5, 504.3, 498.0, 492.0, 514.0, 406.2, 475.0, 501.0, 500.2, 520.1]
}

私の環境では平均494.4msでした。公式の計測とは実行回数や条件が異なり単純比較はできませんが、1回のsandbox do(起動→実行→破棄)が500ms前後で完了することは確認できました。

コスト

Cloud Run sandboxes自体に追加料金や割増料金はありません。ただしsandboxは親Cloud RunインスタンスのCPU/メモリを共有して動くため、Cloud Run自体のコンピュート・ネットワーク料金は通常どおり発生します。また、アプリ本体と同時実行するsandboxの分を賄えるよう、CPU/メモリ上限を確保する必要があり、リソースが足りないと、アプリ本体かsandboxのどちらかが詰まる可能性があります。
最新の料金については、公式の価格表をご確認ください。

IAM・制限・クォータ

IAM

sandboxのIAMは「デプロイする権限」と「sandbox内で動くコードの権限」の2種類です。
前者に、sandbox固有の新規IAMロールは不要です。通常のCloud Runデプロイ権限(roles/run.developerroles/iam.serviceAccountUser)で足り、本記事のように--sourceからデプロイする場合はビルドを伴う分の権限も別途必要になります。
後者がsandboxの本質です。sandbox内のコードは実行サービスアカウントの認証情報にアクセスできません。メタデータサーバーに到達できず、google-auth等が自動取得するはずのトークンも得られないためです。そのため、sandbox内のコードに渡したい値だけを--envで明示的に渡す形になります。ただし--envで渡した値はsandbox内のコードから読めるため、--allow-egressを併用する実行では外部へ持ち出せる前提で、渡す情報を絞ります。なおIAM分離の詳細な保証範囲はPreview時点のため要確認です。

制限・クォータ(Preview時点)

sandbox固有のリージョン・クォータ・最大実行時間は、現時点の公式ドキュメントに記載が見当たりませんでした。gen2が前提で、本記事では東京(asia-northeast1)で動作を確認しています。リクエスト内でsandbox doを同期実行する場合、クライアントへの応答はCloud Runのリクエストタイムアウト(最大60分)に従いますが、これは接続を切るだけで実行中のコードを止めるとは限らないため、前述のとおりsubprocess側でtimeoutを設けています。Preview版のため、SLAや仕様は今後変わる可能性があります。

参考: Configure sandboxes for services / Cloud Run quotas

おわりに

検証の結果、3つのセキュリティ境界(環境変数・メタデータ遮断/egressのデフォルト遮断/読み取り専用ファイルシステム)がすべて機能しました。--allow-egressで境界2を開けても境界1(メタデータ遮断)は維持され、境界どうしが独立して働くことも確認できました。
また、sandboxの起動オーバーヘッドも、10回の簡易計測で平均494msと、500ms前後で起動→実行→破棄できることを確認できました。
AIエージェントにコードを実行させたいが、認証情報とデータ流出が懸念される際、追加インフラなしで応えられるsandboxesは有力な選択肢になりそうです。