はじめに
AWSのLambdaを触ってからサーバーレスの概念は理解できた。
次に試したのがCloudflare Workers。「エッジで動く」という言葉が気になっていたので調べた。LambdaとWorkersは同じサーバーレスでも設計思想がかなり違って、その違いを理解するのに時間がかかった。
CloudflareとCloudflare Workersとは
Cloudflare:
世界中にエッジサーバーを持つCDN/セキュリティ企業
DNSサーバー・CDN・DDoS対策が主力サービス
Cloudflare Workers:
Cloudflareのエッジサーバー上で動くサーバーレス実行環境
世界200以上のデータセンターで実行される
JavaScriptまたはWASM(WebAssembly)で動く
LambdaとWorkersの設計の違い
AWS Lambda:
Node.jsランタイム → V8エンジン + Node.js API
コールドスタートがある(数百ms〜数秒)
最大15分実行可能
リージョン単位でデプロイ
Cloudflare Workers:
V8 Isolate → V8エンジンのみ(Node.js APIはない)
コールドスタートがほぼない(5ms以下)
最大CPU時間30秒(Paid)、リクエストはタイムアウト
全エッジで自動デプロイ(世界同時)
一番の違いは「V8 Isolate」という実行モデル。Lambdaはプロセス単位で隔離するが、WorkersはV8のIsolate(軽量なサンドボックス)で隔離する。これによってコールドスタートがほぼなくなる。
一方でV8のみで動くためNode.jsのAPIが使えない。fs(ファイルシステム)、child_process(プロセス起動)、ほとんどのNode.jsネイティブモジュールが使えない。
セットアップ
# Wranglerをインストール(Workers CLIツール)
npm install -g wrangler
# ログイン
wrangler login
# プロジェクトを作成(JavaScriptテンプレート)
npm create cloudflare@latest my-worker
テンプレート選択:
Hello World Worker → シンプルなWorker
Hello World Durable Object → 状態を持つWorker
Worker with Hono → Honoフレームワーク
...
生成されるディレクトリ:
my-worker/
├── src/
│ └── index.ts # Workerのメインファイル
├── wrangler.toml # 設定ファイル
├── package.json
└── tsconfig.json
最初のWorker
// src/index.ts
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
const url = new URL(request.url);
const pathname = url.pathname;
// ルーティング
if (pathname === "/") {
return new Response("Hello from Cloudflare Workers!", {
headers: { "Content-Type": "text/plain" },
});
}
if (pathname === "/json") {
return Response.json({ message: "Hello", timestamp: Date.now() });
}
if (pathname.startsWith("/users/")) {
const id = pathname.split("/")[2];
return Response.json({ id, name: "田中" });
}
return new Response("Not Found", { status: 404 });
},
};
// 型定義(wrangler.tomlのbindingsに対応)
interface Env {
// KV名前空間
MY_KV: KVNamespace;
// 環境変数
API_KEY: string;
// D1データベース
MY_DB: D1Database;
}
# ローカルで開発
wrangler dev
# デプロイ
wrangler deploy
wrangler.toml — 設定ファイル
# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"] # Node.js互換フラグ(一部API)
# 環境変数(シークレット以外)
[vars]
API_URL = "https://api.example.com"
# KV名前空間のバインディング
[[kv_namespaces]]
binding = "MY_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# D1データベース
[[d1_databases]]
binding = "MY_DB"
database_name = "my-database"
database_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# R2バケット
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"
# 本番環境の設定
[env.production]
name = "my-worker-production"
[env.production.vars]
API_URL = "https://api.example.com"
# シークレットの設定(ダッシュボードに保存される)
wrangler secret put API_KEY
# → プロンプトで値を入力
Honoフレームワーク — Express的なルーティング
素のWorkersで手書きルーティングを書くのは面倒なのでHonoというフレームワークを使う。
npm install hono
// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { jwt } from "hono/jwt";
type Bindings = {
MY_KV: KVNamespace;
API_KEY: string;
JWT_SECRET: string;
};
const app = new Hono<{ Bindings: Bindings }>();
// ミドルウェア
app.use("*", logger());
app.use("*", cors({
origin: ["https://myapp.example.com", "http://localhost:3000"],
allowMethods: ["GET", "POST", "PUT", "DELETE"],
allowHeaders: ["Content-Type", "Authorization"],
}));
// ルーティング
app.get("/", (c) => {
return c.json({ message: "Hello from Hono!" });
});
app.get("/users", async (c) => {
// KVからデータを取得
const users = await c.env.MY_KV.get("users", "json");
return c.json(users ?? []);
});
app.post("/users", async (c) => {
const body: { name: string; email: string } = await c.req.json();
if (!body.name || !body.email) {
return c.json({ error: "nameとemailは必須です" }, 422);
}
// KVに保存
const users = (await c.env.MY_KV.get("users", "json") as any[]) ?? [];
const newUser = { id: Date.now(), ...body };
users.push(newUser);
await c.env.MY_KV.put("users", JSON.stringify(users));
return c.json(newUser, 201);
});
// JWT認証が必要なルート
app.use("/protected/*", jwt({ secret: (c) => c.env.JWT_SECRET }));
app.get("/protected/data", (c) => {
const payload = c.get("jwtPayload");
return c.json({ userId: payload.sub, data: "secret data" });
});
export default app;
HonoはExpress/FastAPIに近い感覚でルーティングを書ける。Workersだけでなく、Node.js・Deno・Bunでも動く。
KV(Key-Value Store)
Workersで使えるシンプルなKey-Valueストレージ。
// KVの基本操作
app.get("/cache/:key", async (c) => {
const key = c.req.param("key");
const value = await c.env.MY_KV.get(key);
if (!value) {
return c.json({ error: "Not found" }, 404);
}
return c.json({ key, value });
});
app.put("/cache/:key", async (c) => {
const key = c.req.param("key");
const body = await c.req.json();
// TTL(有効期限)を設定
await c.env.MY_KV.put(
key,
JSON.stringify(body.value),
{ expirationTtl: 3600 } // 1時間後に自動削除
);
return c.json({ key, stored: true });
});
app.delete("/cache/:key", async (c) => {
const key = c.req.param("key");
await c.env.MY_KV.delete(key);
return c.json({ deleted: true });
});
KVの特性:
✓ グローバルに分散(エッジから高速アクセス)
✓ 結果整合性(更新が全エッジに反映されるのに最大60秒)
✗ 強い一貫性は保証されない
✗ 大量の書き込みには向かない
→ キャッシュ・セッション・設定値の保存に向いている
D1 — エッジで動くSQLite
# D1データベースの作成
wrangler d1 create my-database
# マイグレーションファイルの作成
mkdir migrations
-- migrations/0001_create_users.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TEXT DEFAULT (datetime('now'))
);
# マイグレーションを実行(ローカル)
wrangler d1 execute my-database --local --file=migrations/0001_create_users.sql
# マイグレーションを実行(本番)
wrangler d1 execute my-database --file=migrations/0001_create_users.sql
// D1を使ったCRUD
app.get("/users", async (c) => {
const result = await c.env.MY_DB.prepare(
"SELECT * FROM users ORDER BY created_at DESC LIMIT 50"
).all();
return c.json(result.results);
});
app.get("/users/:id", async (c) => {
const id = c.req.param("id");
const user = await c.env.MY_DB.prepare(
"SELECT * FROM users WHERE id = ?"
).bind(id).first();
if (!user) {
return c.json({ error: "Not found" }, 404);
}
return c.json(user);
});
app.post("/users", async (c) => {
const { name, email }: { name: string; email: string } = await c.req.json();
const result = await c.env.MY_DB.prepare(
"INSERT INTO users (name, email) VALUES (?, ?) RETURNING *"
).bind(name, email).first();
return c.json(result, 201);
});
D1はSQLiteベースなのでPostgreSQLとは方言が少し違うが、基本的なSQLは同じ感覚で書ける。
R2 — オブジェクトストレージ
AWS S3互換のオブジェクトストレージ。データ転送費用がゼロ。
// R2を使ったファイル操作
app.post("/upload/:filename", async (c) => {
const filename = c.req.param("filename");
const body = await c.req.arrayBuffer();
await c.env.MY_BUCKET.put(filename, body, {
httpMetadata: {
contentType: c.req.header("Content-Type") ?? "application/octet-stream",
},
});
return c.json({ uploaded: filename });
});
app.get("/files/:filename", async (c) => {
const filename = c.req.param("filename");
const object = await c.env.MY_BUCKET.get(filename);
if (!object) {
return c.json({ error: "Not found" }, 404);
}
return new Response(object.body, {
headers: {
"Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream",
"Cache-Control": "public, max-age=31536000",
"ETag": object.etag,
},
});
});
app.delete("/files/:filename", async (c) => {
const filename = c.req.param("filename");
await c.env.MY_BUCKET.delete(filename);
return c.json({ deleted: true });
});
Next.jsからWorkersにリクエストする
// Next.js側(Route Handler)
// app/api/edge-data/route.ts
export async function GET() {
const res = await fetch("https://my-worker.my-subdomain.workers.dev/users");
const data = await res.json();
return Response.json(data);
}
FastAPIとの使い分け
Cloudflare Workers(Hono)が向いているとき:
✓ グローバルに低レイテンシが必要(エッジに近い処理)
✓ シンプルなAPIプロキシ・変換
✓ 認証トークンの検証
✓ A/Bテストの振り分け
✓ レート制限の実装
✓ CDNと統合したキャッシング
FastAPIが向いているとき:
✓ 重い計算処理
✓ Python固有のライブラリが必要(pandas, scikit-learn等)
✓ DBへの複雑なクエリ
✓ 長時間実行のタスク
両者を組み合わせた構成もよく使われる。
[ブラウザ]
↓
[Cloudflare Workers] ← 認証・キャッシュ・レート制限
↓(キャッシュミスまたは書き込み)
[FastAPI on ECS] ← 重い処理・DB操作
↓
[RDS / S3 / Snowflake]
デプロイとCI/CD
# 本番デプロイ
wrangler deploy --env production
# ログをリアルタイムで確認
wrangler tail
# デプロイ履歴
wrangler deployments list
# .github/workflows/deploy-worker.yml
name: Deploy Worker
on:
push:
branches: [main]
paths:
- 'worker/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: |
cd worker
npm ci
- name: Run tests
run: |
cd worker
npm test
- name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
workingDirectory: worker
command: deploy --env production
まとめ
- Cloudflare WorkersはV8 Isolateで動くのでコールドスタートがほぼない
- Node.js APIは使えない(
fs、child_process等) - HonoでFastAPI/Express的なルーティングが書ける
- KV・D1・R2でストレージが揃っている
- エッジ処理(認証・キャッシュ・振り分け)に向いている
- 重い処理・Pythonライブラリが必要な処理はFastAPIに任せる
AWSのLambdaと比べてコールドスタートがないのは体感でわかるくらい速い。JavaScript/TypeScriptで書く必要があるのでPython一本でやっていると少し慣れが必要だが、HonoのAPIはFastAPIに近い感覚で書けた。