ブログ一覧

D1 と R2 を使う Workers サイトは Deno の celld で動くが、今は移すメリットが無い

Deno のチームが Cloudflare に入るという Cloudflare の発表をきっかけに、Deno が 2026年8月に出していた celld を Workers 以外で動かす環境として試し、D1 と R2 を使うこのブログが Cloudflare の外で動くかを確かめてみました。celld は Workers と Durable Objects を自前のマシンで動かすシングルバイナリで、workerd とは別の実装です。

結論から言うと、D1 や R2 を使う Workers のサイトを Deno の celld に移すメリットは今はありません。動くことは動きますが、移すと Workers のキャッシュと TLS を失い、ビルドのたびの手直しとベータ版のツールを抱えることになり、得る物は Cloudflare の外の自分のマシンに置けることだけでした。

Workers のランタイムの workerd は、workerd の設定スキーマを見ると、単体には D1 の実装が無く、KV と R2 もバックエンドを自分で用意する形で、wrangler dev ではそのバックエンドを Miniflare が用意しています。

当サイトは Astro 製の CMS「EmDash」で作っていて、記事は D1、画像は R2、ログインのセッションは KV に置き、予約公開などの定期処理を Cron Triggers で回しています。料金と WordPress からの移行の話はEmDash を本番で半年使った記事に書きました。

設定 5か所とコード 3か所を直すと、このサイトが Cloudflare の外の 2台で動きました。2台のうち D1 のデータを受け持つ 1台を落とすと、もう一方は約 10秒 404 を返してから戻りました。ただし D1 のデータは wrangler d1 export では出せず、移行用の SQL を自分で組んでいます。KV はバインディングがあっても起動はしましたが、当サイトで KV を使うログインをしていないので動くかは試しておらず、本番の D1 からの移行も、元の SQLite ファイルを本番の D1 から取る方法を含めてまだやっていません。

止まった 8か所は、アダプタの設定と Node の API と celld の D1 の内部テーブル

試したのは celld 0.6.2、wrangler 4.125.0、EmDash 1.2.0、Astro 7.2 です。同じビルドを wrangler dev と celld に載せ、トップ、記事、R2 の画像、RSS、サイトマップ、404、管理画面の初期設定など 11 ページを比べました。wrangler dev では最初から全部動きましたが、celld では直す前は 1 ページも出ませんでした。

Astro の Cloudflare アダプタが出した wrangler.json のまま celld deploy すると、受け付けないキーで止まりました。設定で止まったのは次の 5か所で、どれもアダプタが出す wrangler.json か assets ディレクトリの問題でした。

  1. cache、observability、routes など、celld が受け付けないキーがたくさんある(多くは空の値)
  2. rules の ESModule を受け付けない
  3. assets ディレクトリがプロジェクトの外にあると止まる
  4. アダプタが出す .assetsignore があると止まる
  5. no_bundle: true のままだとエントリのファイルしか載らず、チャンクが見つからずに起動しない

1〜4 は celld 用の設定ファイルを別に作って受け付けないキーと rules を消し、assets ディレクトリをプロジェクトの中に置いたうえで、アダプタが出した .assetsignore をどけると解消しました。5 も no_bundle を消して celld に esbuild でバンドルさせると解決し、どれもサイトのソースは変えていません。Astro の Cloudflare アダプタを使うサイトなら、この 5か所には同じように当たるはずです。

設定を直して起動すると、今度は node:module.createRequire is not implemented in celld で落ちました。EmDash が使うライブラリがロード時に createRequire を呼んでいて、require しているのは crypto と util だけだったので、ビルド後のファイルでその 2つだけを返す関数に差し替えました。別のファイルにある import "node:module"; も celld が解決できずに resolve: no stub for specifier で落ちたので、その行を消しました。この 2か所は EmDash が使うライブラリの中の話です。

そこを直すと、EmDash の初回のマイグレーションが D1_ERROR: not authorized で止まりました。sqlite_master を見ると、見覚えのない _litestream_seq と _litestream_lock があります。これは Litestream(SQLite をストレージにレプリケーションするツール)の名前が付いた celld の D1 の内部テーブルで、テーブル一覧には出るのにカラムを見ようとすると拒否されます。

$ celld d1 execute <DB 名> --command "SELECT * FROM pragma_table_info('_litestream_seq');"
Error: D1_EXEC_ERROR: not authorized

EmDash がテーブル一覧を取るのは d1-introspector.ts の 45〜46行目です。

.where("name", "not like", "sqlite_%")
.where("name", "not like", "_cf_%") // Skip Cloudflare internal tables

このように _cf_ で始まる Cloudflare の内部テーブルを除外していますが、_litestream_ は除外していないので、全テーブルのカラムを調べに行ってここで止まります。ビルド後のファイルの同じ条件に _litestream_% を除外する 1行を足すと動きました。D1 のテーブル一覧を自分で取るアプリなら、同じエラーに当たるはずです。

メリットは、ソースを変えずに同じビルドが自前の 2台で動くこと

8か所を直し終えると、celld で 11 ページとも表示され、HTML の差はホスト名だけになりました。書き換えたビルドも wrangler dev では全部動きました。本番の Cloudflare には載せていませんが、celld に合わせて直しても Cloudflare に戻せなくなるわけではありません。

2台にするのも、同じ S3 互換のバケットを指定するだけでした。同じバケットを見るノードどうしが自動でクラスタになり、D1 のデータは 1台のノードが受け持って、書き込みを他のノードとバケットにレプリケーションします。どちらのノードにリクエストしても同じ結果が返りました。

その D1 を受け持つノードを kill -9 すると、もう一方のノードは記事のページにボディが空の 404 を約 10秒返してから戻り、落とす直前に書いたレコードも残っていました。D1 を受け持つノードを別のノードに切り替えるまでの目安が約 10秒と celld の README にあり、見た時間と合っています。この間の 404 は、EmDash の 404 ログに「無いページ」として 1件残っていました。kill -TERM で止めたときは、もう一方は一度も失敗しませんでした。

予約公開などの処理を回す Cron も celld の上で設定どおりに走り、D1 を更新しました。ただし Cron の設定だけを変えてデプロイし直したときは、動いていたノードは再起動するまで新しい設定を使いませんでした。

デメリットは、ビルドのたびの手直しと D1 の移行、キャッシュと TLS

Cloudflare で動かすのと比べると、celld では次の手間を自分で抱えます。

  • ビルドの出力を、毎回 celld の受け付ける形に直す(上の 8か所)
  • D1 のデータは wrangler d1 export で出せず、SQL を自分で作って移す
  • Workers のキャッシュ(Cache API)が効かない
  • TLS が無くノード間の通信も暗号化しないので、リバースプロキシを前に置き、閉じたネットワークで動かす

D1 のデータを移そうと wrangler d1 export を流すと、cannot export databases with Virtual Tables (fts5) で止まりました。EmDash は記事の全文検索に FTS5 の仮想テーブルを作るので、EmDash のサイトの D1 は丸ごとはエクスポートできません。D1 のインポートとエクスポートのドキュメントは仮想テーブルを消してエクスポートし、後で作り直すよう書いていますが、本番でやるとエクスポートの間はサイト内検索のテーブルが無くなります。--table で FTS5 以外のテーブルだけを出すとインデックスとトリガーが欠け、データだけを移す方法は、EmDash が最初に作るコレクションの ID が移行元と移行先で違うので記事の INSERT で止まりました。

そこで、Python の sqlite3 で移行用の SQL を作る 30行ほどのスクリプトを書きました。sqlite_master から Cloudflare の内部テーブルと FTS5 のシャドウテーブルを除いたテーブルの CREATE TABLE を取り、FTS5 の仮想テーブルは CREATE VIRTUAL TABLE で作ってデータを普通の INSERT で入れ直します。残りのテーブルのデータも INSERT にして、最後にインデックスとトリガーの CREATE を足します。

wrangler dev が手元に持つ D1 の SQLite ファイルから作った SQL を、空の celld の D1 に celld d1 execute --file で流すと全部通り、11 ページとも動いて、サイト内検索も同じ結果を返しました。R2 の画像は celld r2 put で 1つずつ入れられますが、まとめて移すコマンドは celld にありません。

キャッシュは、このサイトでは記事のページを Workers のキャッシュに 30日持たせていますが、celld と Cloudflare の違いの一覧にあるとおり、celld では Cache API が常にミスします。celld の上では記事を 1本表示するたびにレンダリングし、D1 にクエリを 3本投げていました。同じ一覧によると Workers AI・Vectorize・Hyperdrive・Browser Rendering・Email も celld には無く、ほかに BEGIN/SAVEPOINT と D1 の REST API も使えません。

自分のマシンに置ける代わりに、キャッシュと TLS と手直しを抱える

celld で得るのは Cloudflare の外の自分のマシンにサイトを置けることで、その代わりにエッジのキャッシュと TLS、ビルドのたびの手直し、ベータ版のツールを自分で抱えます。celld は制限の一覧で自分でベータと書いていて、Deno は Deno 本体の開発を 1年で終えるとしています。celld のコードと考え方は workerd に取り込むと Cloudflare の発表にありますが、時期と形はまだ発表されていません。

この得る物と抱える物から、私は celld が選択肢になる人とならない人を次のように考えています。

  • 向く人: D1・R2・KV・Cron くらいしか使っていない Workers のサイトを持っていて、Cloudflare から出られる道を手元で確かめておきたい人、自前のサーバーと TLS の終端を自分で用意できる人
  • 向かない人: Workers AI・Vectorize・Hyperdrive・Queues・Workflows に頼っている人、エッジのキャッシュで速さを出している人、今すぐ本番を Cloudflare から移したい人(celld はベータで、workerd に取り込まれた後の形も未発表)

もし自分のサイトで確かめたい場合、この記事に紹介した次の 5つを見ると良いです。

  1. celld に載せて、設定のエラーを潰す
  2. 起動ログで not implemented(celld に無い Node の API)と no stub(celld が解決できない import)を探す
  3. テーブル一覧を自分で取るコードがあれば、_litestream_ で止まらないかを見る
  4. wrangler d1 export が FTS5 で止まるかを見る
  5. 2台にして片方を kill -9 し、もう片方が何を返し、何秒で戻るかを見る

この記事をシェア