Miniflareの内部構造:プラグインアーキテクチャとworkerdの統合
前提知識
- ›このシリーズの第1回・第3回の記事
- ›Cloudflare Workersランタイムモデルの基本的な理解
- ›Zodスキーマバリデーションの知識
- ›Node.jsにおける子プロセス管理の概念
Miniflareの内部構造:プラグインアーキテクチャとworkerdの統合
Miniflareは、Workersのローカル実行環境シミュレーターです。ローカルモードで wrangler dev を実行したり、テスト内で getPlatformProxy() を呼び出したりすると、KVネームスペース・D1データベース・R2バケット、その他あらゆるCloudflareバインディングを提供しているのがMiniflareです。Cloudflareのネットワークには一切アクセスせず、すべてあなたのマシン上で動作します。
その仕組みはシンプルで、Cloudflareのオープンソース実装である workerd を子プロセスとして起動し、精巧に組み立てた設定を渡します。その設定は28個の独立したプラグインによって生成され、各プラグインがそれぞれ1つのCloudflareサービスを担当しています。本記事では、プラグインの動作原理・workerdの管理方法・設定が頻繁に更新される状況での整合性維持の仕組みを詳しく見ていきます。
Miniflareクラス:プライベートステートの全体像
packages/miniflare/src/index.ts#L910-L965 の Miniflare クラスは、多くのプライベートステートを保持しています。それぞれのフィールドが何を担うかを理解することで、Miniflareが管理するものの全体像が見えてきます。
classDiagram
class Miniflare {
-#runtime: Runtime
-#runtimeMutex: Mutex
-#proxyClient: ProxyClient
-#liveReloadServer: WebSocketServer
-#webSocketServer: WebSocketServer
-#devRegistry: DevRegistry
-#maybeInspectorProxyController: InspectorProxyController
-#hyperdriveProxyController: HyperdriveProxyController
-#sharedOpts: PluginSharedOptions
-#workerOpts: PluginWorkerOptions[]
-#tmpPath: string
-#disposeController: AbortController
-#externalPlugins: Map
-#browserProcesses: Map
}
主要なフィールドをまとめると以下の通りです。
| フィールド | 役割 |
|---|---|
#runtime |
workerd子プロセスを管理する Runtime インスタンス |
#runtimeMutex |
設定更新を直列化し、競合状態を防ぐMutex |
#proxyClient |
Node.jsプロキシバインディングを提供(getPlatformProxy() 用) |
#liveReloadServer |
ブラウザのライブリロード信号を扱うWebSocketサーバー |
#webSocketServer |
ユーザーWorkerのWebSocket接続を扱うWebSocketサーバー |
#devRegistry |
マルチWorkerのサービスディスカバリーレジストリ |
#maybeInspectorProxyController |
Chrome DevTools Protocolプロキシ |
#hyperdriveProxyController |
HyperdriveのTCP接続向けローカルプロキシ |
#tmpPath |
一時ディレクトリ(dispose() 時に削除) |
#disposeController |
dispose() 時にシグナルを送り、実行中の処理をキャンセルするAbortController |
967行目のコンストラクターでは、Zodを使ったオプションの検証(validateOptions())・ループバックサーバーの起動・WebSocketサーバーの初期化・devレジストリの作成・workerdランタイムの起動が行われます。これらはすべてコンストラクター内で同期的に処理され、非同期の初期化処理は #initPromise に格納されます。
28個のプラグインシステム
Cloudflareの各サービスは独立したプラグインとして実装されています。packages/miniflare/src/plugins/index.ts#L48-L77 の PLUGINS マップに、28個すべてが登録されています。
| プラグイン名 | サービス |
|---|---|
| core | Workerの基本設定(スクリプト・モジュール・互換性) |
| cache | Cache API |
| d1 | D1データベース |
| do (durable-objects) | Durable Objects |
| kv | Workers KV |
| queues | Queues |
| r2 | R2オブジェクトストレージ |
| hyperdrive | Hyperdriveデータベースコネクター |
| ratelimit | レート制限 |
| assets | Workers Static Assets |
| workflows | Workflows |
| pipelines | Pipelines |
| secret-store | Secret Store |
| メール送信 | |
| analytics-engine | Analytics Engine |
| ai | Workers AI |
| ai-search | AI Search |
| browser-rendering | ブラウザレンダリング |
| dispatch-namespace | Workers for Platforms |
| images | 画像変換 |
| stream | Stream(メディア) |
| vectorize | Vectorizeベクターデータベース |
| vpc-services | VPC Services |
| mtls | 相互TLS証明書 |
| hello-world | 組み込みテスト用Worker |
| worker-loader | サービスバインディング / マルチWorker |
| media | メディア処理 |
| version-metadata | Workerバージョンメタデータ |
Cloudflareのドキュメントではよくある KV・D1・R2・Durable Objectsを中心に説明していますが、MiniflareがBrowser RenderingやPipelinesといったニッチな機能も含む28個のプラグインを実装している点から、ローカルシミュレーションのカバレッジの広さが伝わります。
ヒント: ローカルで動作しないように見えるCloudflareバインディングがあれば、まず
packages/miniflare/src/plugins/ディレクトリを確認してみましょう。あまり知られていなくても、プラグインが存在している可能性が高いです。
プラグインのインターフェース:options・sharedOptions・getServices()・getBindings()
すべてのプラグインは、packages/miniflare/src/plugins/shared/index.ts#L103-L133 で定義された Plugin インターフェースを実装します。
export interface PluginBase<Options extends z.ZodType, SharedOptions extends z.ZodType | undefined> {
options: Options;
getBindings(options: z.infer<Options>, workerIndex: number): Awaitable<Worker_Binding[] | void>;
getNodeBindings(options: z.infer<Options>): Awaitable<Record<string, unknown>>;
getServices(options: PluginServicesOptions<Options, SharedOptions>): Awaitable<Service[] | ServicesExtensions | void>;
getPersistPath?(sharedOptions, tmpPath): string;
getExtensions?(options): Awaitable<Extension[]>;
}
packages/miniflare/src/plugins/kv/index.ts#L75-L202 のKVプラグインを見ると、このパターンがより具体的に理解できます。
options—kvNamespaces・sitePath・siteInclude・siteExcludeを定義するZodスキーマ(KVOptionsSchema)sharedOptions— Worker間で共有するオプション(kvPersist)のZodスキーマgetBindings()— バインディング名をKVネームスペースサービスにマッピングするWorker_Bindingオブジェクトを生成getNodeBindings()— Node.jsプロキシアクセス用のProxyNodeBindingインスタンスを返すgetServices()— Durable Objectベースのネームスペースワーカー・ディスクストレージサービス・Workers Sitesサービスを含む、workerdのService定義を返す
flowchart TD
OPTIONS["KVOptionsSchema (Zod)"] -->|"validated input"| PLUGIN["KV Plugin"]
PLUGIN -->|"getBindings()"| BINDINGS["Worker_Binding[]<br/>KV namespace bindings"]
PLUGIN -->|"getServices()"| SERVICES["Service[]<br/>namespace DO worker +<br/>disk storage service"]
PLUGIN -->|"getNodeBindings()"| NODE["ProxyNodeBinding<br/>for getPlatformProxy()"]
SERVICES --> CONFIG["workerd config"]
BINDINGS --> CONFIG
このプラグインシステムの優れた点は、コンポーザビリティにあります。Miniflareクラスは PLUGIN_ENTRIES をイテレートして各プラグインのメソッドを呼び出し、workerd設定全体を組み立てます。プラグイン同士は互いを知らず、Durable Objectのクラス名やキューのコンシューマーといった共有状態は PluginServicesOptions パラメーターを通じて渡されます。
共有型:DurableObjects・Queues・Persistence
プラグイン間の連携には、packages/miniflare/src/plugins/shared/index.ts#L29-L71 で定義された共有型が使われます。
DurableObjectClassNames— サービス名とエクスポートされたDurable Objectクラス名のマッピング。マルチWorker構成でどのWorkerがDOクラスを宣言しているかをcoreプラグインが把握するために使われます。QueueProducers/QueueConsumers— キュー名とプロデューサー/コンシューマー設定のマッピング。Worker間のキュールーティングを実現します。PersistenceSchema—boolean | URL | pathのZod unionで、デフォルトの永続化ルートは.mfです。trueの場合はデータが.mf/<plugin-name>/に保存され、falseの場合はdispose()時に削除される一時ディレクトリが使われます。WrappedBindingNames— ラップドバインディングとして使用されているWorker名を追跡し、ルーティング可能なサービスとして公開されないようにします。
classDiagram
class DurableObjectClassNames {
Map~string, Map~string, DOConfig~~
}
class QueueProducers {
Map~string, QueueProducerConfig~
}
class QueueConsumers {
Map~string, QueueConsumerConfig~
}
class PersistenceSchema {
boolean | URL | path
DEFAULT_PERSIST_ROOT = ".mf"
}
PluginServicesOptions インターフェースはいわば「何でも入れる袋」で、これらの横断的な型がすべてのプラグインの getServices() 呼び出しに渡されます。ソースコードのコメントも率直です。// ~~Leaky abstractions~~ "Plugin specific options" :) と書かれています。
workerd子プロセスの管理
packages/miniflare/src/runtime/index.ts#L208-L349 の Runtime クラスが、workerd子プロセスのライフサイクルを管理します。updateConfig() メソッドは次の手順で処理を進めます。
- 停止 — 既存のプロセスがあれば
SIGKILLで強制終了します。SIGTERMではなくSIGKILLを使うのは、Chromeが約10秒間接続を保持することでプロセスの終了がブロックされる場合があるためです。 - 起動 — バイナリCapnP設定モード・実験的フラグ・fd 3上のコントロールパイプを指定して新しいworkerdプロセスを起動します。
- 書き込み — シリアライズされた設定をstdinに書き込んで閉じます。
- 待機 — fd 3から届くコントロールメッセージを解析して起動完了を検知します。
起動完了の検知には、packages/miniflare/src/runtime/index.ts#L21-L31 のZodスキーマが使われます。
const ControlMessageSchema = z.discriminatedUnion("event", [
z.object({ event: z.literal("listen"), socket: z.string(), port: z.number() }),
z.object({ event: z.literal("listen-inspector"), port: z.number() }),
]);
workerdがリッスンを開始すると、コントロールパイプにJSONメッセージを書き出します。Miniflareはこれらのメッセージからポートのアサイン結果を取得します。ポート0を指定した自動割り当ての場合に特に重要です。waitForPorts() 関数はすべての必要なソケットが確認されるまでメッセージを収集し続けます。
sequenceDiagram
participant MF as Miniflare
participant RT as Runtime
participant WD as workerd process
MF->>RT: updateConfig(configBuffer)
RT->>RT: dispose() existing process
RT->>WD: spawn("workerd", ["serve", "--binary", ...])
RT->>WD: stdin.write(configBuffer)
RT->>WD: stdin.end()
WD-->>RT: fd3: {"event":"listen","socket":"entry","port":8787}
WD-->>RT: fd3: {"event":"listen-inspector","port":9229}
RT-->>MF: SocketPorts map
104行目の getRuntimeCommand() 関数は、まず MINIFLARE_WORKERD_PATH 環境変数を確認してから、インストール済みのworkerdバイナリへのフォールバックを試みます。カスタムビルドのworkerdに対してテストを行う際に便利です。
Cap'n Proto設定のシリアライズとランタイムMutex
workerdはJSONやTOMLを読み取りません。設定はCap'n Protoバイナリ形式で渡す必要があります。serializeConfig() 関数(./runtime/config からインポート)がJavaScriptの設定オブジェクトをworkerdが解析できるバイナリバッファに変換します。
Miniflareクラスの945行目にある #runtimeMutex は、正確な動作のために欠かせない存在です。ホットリロードの場面を考えてみましょう。ユーザーがファイルを保存するとウォッチャーが反応し、設定が再計算されて setOptions() が呼ばれます。しかし setOptions() は非同期処理を伴います。Zodバリデーション・プラグインサービスの生成・設定のシリアライズ・プロセスの再起動、これらすべてが非同期です。最初のリロードが完了する前にユーザーが再度ファイルを保存した場合、2回目の setOptions() 呼び出しが1回目の処理と競合してしまいます。
Mutexによってこれらの操作が直列化され、2回目の呼び出しは1回目の完了を待ってから始まります。これにより状態の破損を防ぎ、最終的に動作している設定が常に最後にリクエストされた更新と一致することが保証されます。
ヒント: 急速な設定変更後にMiniflareが古い設定を持っているように見える問題をデバッグしている場合、Mutexが保持中の可能性があります。Miniflareのデバッグログで "Acquiring runtime mutex" というメッセージを確認してみましょう。
Inspectorプロキシとdev Registry
Miniflareの機能をさらに充実させる高度な仕組みが2つあります。
InspectorProxyController は、WorkerをデバッグするためのChrome DevTools Protocol エンドポイントを作成します。設定可能なポート(通常は9229)で動作し、Chrome DevToolsとworkerdの組み込みV8 inspectorの間でWebSocket接続をプロキシします。このプロキシ層が必要な理由は、リロード中にworkerdが再起動する可能性があるためです。プロキシがDevTools向けの安定した接続を維持しつつ、裏側で新しいworkerd inspectorへの再接続を行います。
DevRegistry はマルチWorkerのサービスディスカバリーを実現します。複数のWorkerをローカルで実行する場合(例:フロントエンドWorkerとAPI Worker)、各Workerは自身のローカルアドレスとバインディングをdev registryへ登録します。これにより他のWorkerはサービスバインディングをローカルアドレスとして解決できるようになります。レジストリは packages/miniflare/src/index.ts#L1046 で初期化され、ファイルベースのプロセス間通信によって動作します。
次のステップ
ここまでで、Miniflareがworkerd向けのコンフィグを組み立て、ライフサイクルを管理する仕組みを見てきました。次はMiniflareが動き出す前のステップ、つまりTypeScript製Workerのソースコードをデプロイ可能なJavaScriptモジュールへ変換するバンドル処理に目を向けます。第5回ではWranglerのバンドルパイプライン・カスタムesbuildプラグイン・Cloudflareエッジへのデプロイの流れを解説します。