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

Cloudflare Workers入門 — エッジで動くサーバーレスを試した

0
Posted at

はじめに

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に近い感覚で書けた。


参考


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