03-5211-7750 平日|09:30~18:00

ユーザーコードを安全に実行!Cloudflare Dynamic Workersを徹底検証 〜本番採用を見据えた安全なサンドボックス実装術〜

           

サービス資料や
ホワイトペーパーはこちら

           資料を【無料】ダウンロードFREE

はじめに

Cloudflare Workers に、実行時に別の Worker を起動してコードを安全に実行できる「Dynamic Workers」が追加されました。Dynamic Workers は、信頼しないコードを軽量な sandbox で動かすための仕組みです。

Cloudflare Doc: https://developers.cloudflare.com/dynamic-workers/
Cloudflare Blog: https://blog.cloudflare.com/dynamic-workers/

公式ドキュメントによると以下のような機能になります。

>信頼できないコードを安全にサンドボックス化するための、コンテナに代わる軽量な手段として使用できます。

Workers の中で、別の Workers のコードをサンドボックス環境で安全に実行できます。

Dynamic Workers は V8 isolates ベースで動作します。
そのため、非常に高速に起動してコードの実行ができますし、メモリも非常に少なく済ませることができます。コンテナと比べて起動が 100 倍速く、メモリ効率も 10〜100 倍良いとCloudflare blogで説明されています。

参照URL: https://blog.cloudflare.com/dynamic-workers/#100x-faster

Cloudflare blog では、Dynamic Workers は Workers 基盤上でスケールし、リクエストごとに別の sandbox を起動するような使い方にも対応できると説明されています。毎秒 100 万リクエスト規模でも、そのような並列実行を想定した設計です。

今回は実際に動かして、どのように使うのかを簡単に検証していこうと思います。

【相談無料】Cloudflareの導入や運用について、こちらからご相談いただけます ✉️

動作検証1

Dynamic Workers にそのまま渡せるコードは、現時点(2026/04)では JavaScript と Python です。TypeScript は事前に JavaScript へ変換して渡す必要があります。

Cloudflare Doc: https://developers.cloudflare.com/dynamic-workers/getting-started/#supported-languages

GUIで書いたWorkersのコードをBackend Workersに送信して、Dynamic Workersで処理したものを返す動きを試してみます。

まずは最小構成で、ブラウザから送った JavaScript を Dynamic Workers で実行し、globalOutbound: null によって外部通信が遮断されることを確認します。

構成図




wranglerでCloudflareにログイン



以下のコマンドでCloudflareにログインします。
$ npx wrangler login
$ npx wrangler whoami

Workers(Backend)の作成



まずWorkers(Backend)を以下のコマンドで作成します。

選択した項目は以下の通りです。
$ npm create cloudflare@latest -- dw-backend

├ What would you like to start with?
│ category Hello World example

├ Which template would you like to use?
│ type Worker only

├ Which language do you want to use?
│ lang TypeScript

├ Do you want to add an AGENTS.md file to help AI coding tools understand Cloudflare APIs?
│ yes agents

├ Do you want to use git for version control?
│ yes git

├ Do you want to deploy your application?
│ no deploy via `npm run deploy`

編集するためにディレクトリを移動して型定義ファイルを生成します。
$ cd dw-backend
$ npx wrangler types


wrangler.jsoncの編集


ALLOWED_ORIGINとworker_loadersの部分を追加します。ALLOWED_ORIGIN部分は、frontendで使用するFQDNを指定して下さい。
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "test-dynamic-workers",
"main": "src/index.ts",
"compatibility_date": "2026-04-23",
"observability": {
"enabled": true
},
"upload_source_maps": true,
"compatibility_flags": [
"nodejs_compat"
],
"vars": {
"ALLOWED_ORIGIN": "https://dw-frontend.xxxxx.workers.dev",
},
"worker_loaders": [
{
"binding": "LOADER",
},
],
}


src/index.tsを編集


以下の様に変更します。
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);

if (url.pathname !== '/api/run') {
return new Response('Not Found', { status: 404 });
}

if (request.method !== 'POST') {
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: createCorsHeaders(env),
});
}

return new Response('Method Not Allowed', {
status: 405,
headers: {
Allow: 'POST',
...createCorsHeaders(env),
},
});
}

const requestForWorker = request.clone();
const body = (await request.json()) as { code?: string };

if (typeof body.code !== 'string') {
return Response.json({ error: 'code is required' }, { status: 400, headers: createCorsHeaders(env) });
}

let response: Response;

try {
const worker = env.LOADER.load({
compatibilityDate: '2026-04-16',
mainModule: 'src/index.js',
modules: {
'src/index.js': body.code,
},
globalOutbound: null,
});

const dynamicResponse = await worker.getEntrypoint().fetch(requestForWorker);
const responseBodyText = await dynamicResponse.text();

if (dynamicResponse.ok) {
response = Response.json(
{ result: responseBodyText },
{
status: 200,
},
);
} else {
response = Response.json(
{
error: responseBodyText,
status: dynamicResponse.status,
},
{
status: dynamicResponse.status,
},
);
}
} catch (error) {
response = Response.json(
{
error: error instanceof Error ? error.message : String(error),
status: 400,
},
{ status: 400 },
);
}

const headers = new Headers(response.headers);

for (const [key, value] of Object.entries(createCorsHeaders(env))) {
headers.set(key, value);
}

return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
},
};

function createCorsHeaders(env: Env): Record<string, string> {
return {
'Access-Control-Allow-Origin': env.ALLOWED_ORIGIN,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
}

以下の部分でDynamic Workersを呼び出しています。
const worker = env.LOADER.load({
compatibilityDate: '2026-04-16',
mainModule: 'src/index.js',
modules: {
'src/index.js': body.code,
},
globalOutbound: null,
});

const dynamicResponse = await worker.getEntrypoint().fetch(requestForWorker);

POSTされたコードを実行させて結果を返しています。

BackendのDeploy


CloudflareにBackendのDeployを行います。
$ npx wrangler deploy

Cloudflareの画面から確認してデプロイされてることを確認します。

Workers(Frontend)の作成


Workers(Frontend)を以下のコマンドで作成します。今回はAstroで作成していきます。

Cloudflare Doc: https://developers.cloudflare.com/workers/framework-guides/web-apps/astro/

選択した項目は以下の通りです。
$ npm create cloudflare@latest -- dw-frontend --framework=astro

astro Launch sequence initiated.
◼ dir Using dw-frontend2 as project directory
tmpl How would you like to start your new project?
A basic, helpful starter project
◼ No problem! Remember to install dependencies after setup.
◼ Sounds good! You can always run git init manually.
✔ Project initialized!
■ Template copied
...

├ Do you want to use git for version control?
│ yes git

├ Initializing git repo
│ initialized git

├ Committing new files
│ git commit

╰ Application configured

╭ Deploy with Cloudflare Step 3 of 3

├ Do you want to deploy your application?
│ no deploy via `npm run deploy`

╰ Done

編集するためにディレクトリを移動して型定義ファイルを生成します。
$ cd dw-frontend
$ npx wrangler types


wrangler.jsoncの編集


vars.BACKEND_API_FQDN部分は、backendで使用するFQDNを指定して下さい。
{
"compatibility_date": "2026-04-25",
"compatibility_flags": ["global_fetch_strictly_public"],
"name": "dw-frontend",
"main": "@astrojs/cloudflare/entrypoints/server",
"assets": {
"directory": "./dist",
"binding": "ASSETS",
},
"observability": {
"enabled": true,
},
"vars": {
"BACKEND_API_FQDN": "https://dw-backend.xxxxx.workers.dev",
},
}


src/pages/index.astroを編集


以下の様に変更します。
---
import { env } from 'cloudflare:workers';
import Layout from '../layouts/Layout.astro';
import '../styles/index.css';

export const prerender = false;

const backendApiFqdn = env.BACKEND_API_FQDN?.trim() ?? '';
const normalizedBackendApiFqdn = backendApiFqdn.replace(//+$/, '');
const apiUrl = normalizedBackendApiFqdn ? `${normalizedBackendApiFqdn}/api/run` : '';
const defaultCode = "export default { async fetch() { return new Response('Hello from textbox') } }";
---

<Layout>
<div class="container">
<textarea
id="codeInput"
class="code-input"
placeholder="Cloudflare Workers のコードを入力"
>{defaultCode}</textarea>
<div class="center">
<button id="runButton" type="button">Run</button>
</div>
<div id="output" class="output" aria-live="polite">ここにレスポンスを表示</div>
</div>

<script define:vars={{ apiUrl }}>
const codeInput = document.getElementById('codeInput');
const runButton = document.getElementById('runButton');
const output = document.getElementById('output');

if (
!(codeInput instanceof HTMLTextAreaElement) ||
!(runButton instanceof HTMLButtonElement) ||
!(output instanceof HTMLDivElement)
) {
throw new Error('Required UI elements were not found.');
}

runButton.addEventListener('click', async () => {
output.textContent = 'Running...';
runButton.disabled = true;

try {
if (!apiUrl) {
throw new Error('BACKEND_API_FQDN is not set');
}

const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
code: codeInput.value
})
});

if (!response.ok) {
const errorText = await response.text();
const errorMessage = errorText
? `HTTP ${response.status}n${errorText}`
: `HTTP ${response.status}`;
throw new Error(errorMessage);
}

const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
output.textContent = `Error: ${message}`;
} finally {
runButton.disabled = false;
}
});
</script>
</Layout>


src/styles/index.cssを作成


見た目を整えるために、以下のcssを作成します。
.container {
display: grid;
grid-template-columns: minmax(0, 1fr) 120px minmax(0, 1fr);
gap: 16px;
height: 100vh;
padding: 16px;
box-sizing: border-box;
}

.code-input,
.output {
width: 100%;
height: 100%;
box-sizing: border-box;
font-size: 14px;
}

.code-input {
resize: none;
padding: 12px;
font-family: monospace;
}

.center {
display: flex;
align-items: center;
justify-content: center;
}

button {
padding: 12px 20px;
cursor: pointer;
}

.output {
border: 1px solid #ccc;
padding: 12px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-family: monospace;
background: #fff;
}

@media (max-width: 900px) {
.container {
grid-template-columns: 1fr;
grid-template-rows: minmax(320px, 1fr) auto minmax(320px, 1fr);
height: auto;
min-height: 100vh;
}
}


FrontendのBuild


astroで書いたコードをbuildします。
$ npm run build


FrontendのDeploy


CloudflareにFrontendのDeployを行います。
$ npx wrangler deploy

Cloudflareの画面から確認してデプロイされてることを確認します。


ブラウザから動作確認


コードの実行


dw-frontendのworkers.devのURLにブラウザからアクセスします。

左側のコードはそのままの状態で、中央にある「Run」ボタンを実行してみます。

右側に左側で入力したコードの実行結果が表示されました。
コードの実行自体はDynamic Workersで行われています。実行速度も速く、特に待たされる感覚はありませんでした。

Dynamic Workersから外部への通信確認


Dynamic Workers設定部分で、「globalOutbound: null」にしているので外部への通信は拒否しています。
const worker = env.LOADER.load({
compatibilityDate: '2026-04-16',
mainModule: 'src/index.js',
modules: {
'src/index.js': body.code,
},
globalOutbound: null,
});

本当に外部に出れないのかを確認してみます。
左側のテキストエリアに以下を入力します。
export default {
async fetch() {
const response = await fetch("https://www.accelia.net");
const text = await response.text();
return new Response(text);
},
}


想定通り、外部へ通信しようとするとエラーとなって外部に出れないことが確認できました。
※エラー抜粋: このワーカーは、fetch() などのグローバル関数を通じてインターネットにアクセスすることは許可されていません。

動作検証2

バインディングを使用することでDynamic Workersが接続できるリソースを指定することができます。

最初は通常のWorkersのバインディングだと思っていました。
※通常のバインディング: wrangler.jsoncに記載してCloudflareのリソース(KV、R2、Imagesなど)への接続

Cloudflare Doc: https://developers.cloudflare.com/workers/runtime-apis/bindings/

今回は通常の KV や R2 の binding ではなく、ctx.exports を使って親 Workers のメソッドを RPC として渡す形を使います。

Cloudflare Doc: https://developers.cloudflare.com/workers/runtime-apis/rpc/visibility/

secret などの機密情報を Dynamic Workers に直接渡さずに、必要な処理だけを実行させることができます。
※機密情報は親Workersが保持している

次に、API token を Dynamic Workers に渡さず、親 Workers が保持したまま DNS Records API を呼べることを確認します。child に渡すのは listDnsRecords() を呼ぶための RPC stub だけです。

構成図




API Tokenの作成


ログインユーザのプロフィール -> APIトークン -> 「トークンを作成する」をクリックします。

カスタムトークンを作成します。

以下の内容でTokenを発行します。

発行されたTokenは保存しておいて下さい。

Workers(Backend)の編集


src/index.tsを以下の様に編集します。
import { WorkerEntrypoint } from 'cloudflare:workers';

type EnvWithSecrets = Env & {
CF_API_TOKEN: string;
CF_ZONE_ID: string;
};

const dynamicWorkerCode = `
export default {
async fetch(_request, env) {
const dnsResponse = await env.CF_API.listDnsRecords();

return Response.json({
envKeys: Object.keys(env),
hasApi: !!env.CF_API,
hasToken: "CF_API_TOKEN" in env,
tokenValueType: typeof env.CF_API_TOKEN,
zoneIdValueType: typeof env.CF_ZONE_ID,
dnsResponse,
});
},
};
`;

export class CloudflareApi extends WorkerEntrypoint<EnvWithSecrets> {
async listDnsRecords() {
return fetchDnsRecords(this.env.CF_API_TOKEN, this.env.CF_ZONE_ID);
}
}

export async function fetchDnsRecords(apiToken: string, zoneId: string, fetchImpl: typeof fetch = fetch) {
const response = await fetchImpl(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, {
headers: {
Authorization: `Bearer ${apiToken}`,
},
});

return response.json();
}

export default {
async fetch(request: Request, env: EnvWithSecrets, ctx: ExecutionContext): Promise<Response> {
const worker = env.LOADER.load({
compatibilityDate: '2026-04-22',
mainModule: 'src/index.js',
modules: {
'src/index.js': dynamicWorkerCode,
},
env: {
CF_API: ctx.exports.CloudflareApi({}),
},
globalOutbound: null,
});

return worker.getEntrypoint().fetch(request);
},
};

wrangler.jsoncのvarsに以下を追記します。環境に合わせてIDの値を指定して下さい。
$ npx wrangler secret put CF_API_TOKEN
✔️ Enter a secret value: … *****************************************************
🌀 Creating the secret for the Worker "dw-backend"
✨ Success! Uploaded secret CF_API_TOKEN


BackendのDeploy


CloudflareにBackendのDeployを行います。
$ npx wrangler deploy


secret、環境変数の確認


dw-backendのworkers.devのURLに対して以下のコマンドを実行します。
% curl -ks https://dw-backend.xxxxxxx.workers.dev | jq .

{
"envKeys": [
"CF_API"
],
"hasApi": true,
"hasToken": false,
"tokenValueType": "undefined",
"zoneIdValueType": "undefined",
"dnsResponse": {
"result": [
{
"id": "xxxxxxxx",
"name": "www.xxxxx.xxx",
"type": "A",
"content": "xxx.xxx.xxx.xxx",
"proxiable": true,
"proxied": true,
"ttl": 1,
"settings": {},
"meta": {},
"comment": null,
"tags": [],
"created_on": "2021-06-16T07:02:01.773563Z",
"modified_on": "2021-06-16T07:02:01.773563Z"
},
{
.......
}

 ・"envKeys": ["CF_API"], -> child の env にあるキーは CF_API だけ
 ・"hasApi": true, -> CF_API binding は渡されている
 ・"hasToken": false,-> CF_API_TOKEN というキーは存在しない
 ・"tokenValueType": "undefined", -> Token の値は見えていない
 ・"zoneIdValueType": "undefined" -> zone id の値は見えていない

少なくとも今回の実装では、child の env に CF_API_TOKEN や CF_ZONE_ID は直接入っておらず、Dynamic Worker から参照できないことが確認できました。

 ・上記コードでは、Dynamic WorkersのenvにCF_API_TOKEN や CF_ZONE_ID を直接入れていません。
 ・Dynamic Worker が受け取るのは ctx.exports.CloudflareApi({}) が返す RPC stub だけで、実際に Cloudflare API を叩くのは親 Worker 側の listDnsRecords() です。
 ・さらに globalOutbound: null により、child 自身の fetchは禁止されています。

Cloudflare の docs でも、Dynamic Workers には必要な bindings だけを渡せること、また RPC は明示的に受け取った stub 経由の object / function だけを呼べると説明されています。

サンプルコード

Cloudflare公式のサンプルコードも以下にあります。
いろいろな書き方ができるので以下も参考にしてみて下さい。

・load(実行のたびに新しいDynamic Workersを作成)
https://github.com/cloudflare/agents/tree/main/examples/dynamic-workers

・get(Dynamic WorkersをIDでキャッシュ。今回は未検証)
https://github.com/cloudflare/agents/tree/main/examples/dynamic-workers-playground

最後に

簡単ですがDynamic Workersの機能を検証してみました。

今の時代、AIにコードを書かせるのは一般的だと思いますが、そのコードが本当に安全なのかはどうかは確認しないといけません。
実行させて確認する場合には、サンドボックス環境などの安全な場所で実行させることで不審な動きがあっても安心して実行させることができます。

個人的には実行できる言語がまだ少ないので今後もっと増えてくれると嬉しいです。


そしてCloudflareなら上記以外でも、

 ・パフォーマンスの向上
 ・サイトの信頼性の向上
 ・セキュリティの向上

が、一つのサービスで実現できますので非常におすすめです。

Cloudflareに、アクセリアの運用サポートをプラスしたCDNサービスを提供しています

アクセリア自社CDNの開発と運用は、20年以上にわたります。それらの経験とノウハウを駆使したプロフェッショナルサポートをパッケージしたサービスが、[Solution CDN]と[Solution GateCore]です。
移行支援によるスムーズな導入とともに、お客様の運用負担を最小限に​とどめながら、WEBサイトのパフォーマンスとセキュリティを最大限に高めます。運用サポートはフルアウトソーシングからミニマムサポートまで、ご要望に合わせてご提供します。

Cloudflare(クラウドフレア)の導入や運用について、またそれ以外のことでもなにか気になることがございましたらお気軽にご相談下さい。

Cloudflareの導入・運用について ご相談いただけます。 導入に関するご相談だけでなく、運用についてもご相談ください。

杉木 俊文

技術本部
プラットフォーム部
Contact usお問い合わせ

サービスにご興味をお持ちの方は
お気軽にお問い合わせください。

Webからお問い合わせ

お問い合わせ

お電話からお問い合わせ

03-5211-7750

平日09:30 〜 18:00

Download資料ダウンロード

製品紹介やお役立ち資料を無料でご活用いただけます。

Magazineメルマガ登録

最新の製品情報などタイムリーな情報を配信しています。

Free Service

PageSpeed Insights シミュレータ

CDNによるコンテンツの最適化を行った場合のPageSpeed Insightsのスコアをシミュレートしてレポートします。