1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

サーバー代ゼロ円で「動くデモ」22本を一斉公開した話 — Node標準httpサーバをそのままWorkersに載せる

1
Posted at

個人開発してきた自治体標準準拠システム21本+署名収集支援SaaSを、サーバー代ゼロ円で「実際に触れるデモ」として一斉公開しました。

ショーケース(全デモへのリンク集): https://civictech-portfolio.pages.dev/

  • 自治体標準準拠システムの参考実装 20業務+市民ポータル(職員ログインはデモ用、市民ポータルはモックのマイナンバーカード認証で体験可)
  • 出前署名(リコール等の法定署名収集を配車モデルで支援。E2EE封緘つき)の動くデモ
  • 紹介LP4本(電子投票プロトタイプ/QuietPass/供養ノート/出前署名)

この記事は、その裏側——Node.js標準ライブラリだけで書いたhttpサーバを、ほぼ無改修でCloudflare Workersに載せる手順と、実際に踏んだ罠のメモです。

構成

  • 静的アセット(SPA): Workersの静的アセット機能(無料・帯域課金なし)
  • API: http.createServer で書いた既存サーバを cloudflare:nodehttpServerHandler でそのまま稼働
  • データ: インメモリ(デモはデータ揮発が前提なのでむしろ好都合)
  • LP: Cloudflare Pages(wrangler pages deploy の直接アップロード)

Node httpサーバをWorkersで動かす

2025年以降のWorkersランタイムは node:http のサーバ側APIをサポートしています。compatibility_date2025-09-01 以降にして nodejs_compat を有効にすると、エントリはこれだけです。

// worker.mjs
import { httpServerHandler } from "cloudflare:node";
import "./server.mjs"; // 中で server.listen(8090) している既存サーバ

export default httpServerHandler({ port: 8090 });

listen のポート番号は実ポートではなく「ルーティングキー」として扱われます。既存コードの server.listen(PORT) をそのまま活かせるのが嬉しいところ。

wrangler設定(JSONC):

{
  "name": "my-demo",
  "main": "apps/api/worker.mjs",
  "compatibility_date": "2025-09-01",
  "compatibility_flags": ["nodejs_compat"],
  "assets": { "directory": "apps/web" },
  "vars": { "MY_STORAGE": "memory" }
}

assets.directory に置いた静的ファイルはCDNから直接配信され、アセットに一致しないリクエストだけがworker(=既存のNode APIサーバ)に届きます。SPA+/api/*の構成ならルーティングを書く必要すらありません。

踏んだ罠と対処

罠1: /tmp はリクエストごとに消える

Workersの node:fs は仮想ファイルシステムで、書き込めるのは /tmp のみ。しかも /tmp の内容はリクエスト間で共有されません。JSONファイル保存のアプリをそのまま載せると「保存したはずのデータが次のリクエストで消えている」状態になります。

対処: ストレージ層に「メモリモード」を足しました。isolate(Workersの実行単位)のモジュールスコープ変数はリクエスト間で保持されるので、デモ用途ならこれで十分です。

const MEM = process.env.MY_STORAGE === "memory" ? new Map() : null;

function readJson(file) {
  try {
    return JSON.parse(MEM ? MEM.get(file) : fs.readFileSync(file, "utf8"));
  } catch { return []; }
}
function writeJson(file, data) {
  const text = JSON.stringify(data, null, 2);
  if (MEM) MEM.set(file, text);
  else fs.writeFileSync(file, text);
}

ファイルパスをそのままMapのキーに流用すると、変更が読み書きの3関数だけで済みます。isolateは不定期に破棄されるためデータは消えますが、デモバナーに「データは不定期に消去されます」と書けば仕様です。

罠2: バンドル後は import.meta.url が undefined

fileURLToPath(import.meta.url)__dirname を作る定番イディオムは、wrangler(esbuild)でバンドルすると import.meta.url が undefined になり起動時に即死します。

const __dirname = import.meta.url
  ? path.dirname(fileURLToPath(import.meta.url))
  : "/app"; // メモリモードではMapのキーとしか使われないので固定値でよい

罠3: オプション依存の動的importがバンドルで死ぬ

await import("pg")(DATABASE_URL設定時のみ実行される行)のような文字列リテラルの動的importは、esbuildが解決を試みて未インストールだとビルドエラーになります。specifierを変数に逃がすと静的解析対象から外れます。

const pgSpecifier = "pg";
pg = await import(pgSpecifier); // この行に到達しなければ実行時エラーも出ない

罠4: 起動時に読む必須ファイル

起動時に await readFile("catalog.json") するコードは、バンドルに同梱されないため落ちます。エントリ側でJSONを静的importしてサーバに注入する形に変えました(esbuildがJSONをバンドルしてくれる)。

import catalog from "../data/catalog.json";
const server = await createServer({ handle, catalog });

デモバナーはデプロイ時に注入する

20本のシステムのソースに手を入れる代わりに、デプロイ用ステージングスクリプトで index.html<body> 直後にバナーを差し込みました。源泉コードは無改修・ローカル開発では何も出ません。

const html = await readFile(indexPath, "utf8");
await writeFile(indexPath, html.replace(/(<body[^>]*>)/i, `$1${BANNER}`), "utf8");

共通コア+業務別の薄い設定という構成(20業務で共通コア1つ)にしてあったので、メモリモードの実装はcoreに1回で全業務に波及し、デモ公開は1業務あたり

node tools/demo_deploy.mjs <SystemDir> <slug>
npx wrangler deploy --config .demo-stage/<slug>/wrangler.jsonc

の2コマンドになりました。18本の一括公開はループを回すだけです。

費用とコスト感

項目 費用
Workers(22デモ) ¥0(無料枠: 10万リクエスト/日)
Pages(LP5本) ¥0(静的配信は無制限)
合計 ¥0/月

デモ用途なら無料枠で全く問題ありません。クレジットカード登録も不要でした。

まとめ

  • httpServerHandler のおかげで、依存ゼロのNode httpサーバはWorkers移植がほぼ無改修
  • ファイル保存だけはメモリモードへの差し替えが必要(デモなら揮発は仕様にできる)
  • バンドル環境特有の罠(import.meta.url/動的import/同梱ファイル)は定型対処で回避可能
  • 「READMEを読んでもらう」より「30秒で触ってもらう」方が圧倒的に伝わる

すべての参考実装は MIT ライセンスです。デモは https://civictech-portfolio.pages.dev/ からどうぞ。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?