ブログ記事

エッジコンピューティング入門 - Cloudflare Workersで作るサーバーレスアプリ

エッジコンピューティングは、ユーザーに最も近い場所でコードを実行する新しいパラダイムです。Cloudflare Workersを使って、0msコールドスタートのグローバルアプリケーションを構築する方法を解説します。

Web開発
Cloudflare Workers エッジコンピューティング サーバーレス JavaScript Web開発
エッジコンピューティング入門 - Cloudflare Workersで作るサーバーレスアプリのヒーロー画像

「サーバーレス」の次は「エッジ」です。ユーザーに最も近い場所でコードを実行することで、レイテンシーを劇的に削減し、真のグローバルアプリケーションを実現できます。本記事では、Cloudflare Workers を中心に、エッジコンピューティングの基礎から実践的な開発手法まで詳しく解説します。

この記事で学べること

  • エッジコンピューティングの基本概念と利点 - Cloudflare Workers の開発環境構築 - 実践的なアプリケーション開発パターン - KV、Durable Objects、R2 などの周辺サービス活用 - パフォーマンス最適化とセキュリティ

目次

  1. エッジコンピューティングとは何か
  2. なぜ Cloudflare Workers なのか
  3. 開発環境の構築
  4. 基本的な Worker の作成
  5. KV ストレージの活用
  6. Durable Objects で状態管理
  7. R2 で大容量ファイル管理
  8. 実践的なアプリケーション例
  9. パフォーマンス最適化
  10. セキュリティとベストプラクティス

エッジコンピューティングとは何か

エッジコンピューティングは、データ処理をユーザーに物理的に近い場所で行う分散コンピューティングパラダイムです。

従来のクラウドvs エッジコンピューティング

チャートを読み込み中...

エッジの利点

エッジコンピューティングの性能比較
特徴 従来のクラウド エッジコンピューティング 改善率
レイテンシー 50-200ms 5-20ms 90%削減
可用性 99.9% 99.99% 10倍向上
スケーラビリティ リージョン単位 グローバル自動 無限
コールドスタート 100-500ms 0ms 完全排除
地理的カバレッジ 数リージョン 275+拠点 50倍

なぜCloudflare Workersなのか

数あるエッジプラットフォームの中で、Cloudflare Workers が注目される理由があります。

主要なエッジプラットフォーム比較

主要エッジプラットフォーム比較(2025年)
プラットフォーム 拠点数 言語 料金 特徴
Cloudflare Workers 275+ JS/Rust/C $5~/月 0msコールドスタート
AWS Lambda@Edge 200+ JS/Python $0.50/100万 CloudFront統合
Vercel Edge 20+ JS/TS $20~/月 Next.js統合
Fastly Compute 60+ Rust/JS 要問合せ 高性能CDN

Cloudflare Workersの独自性

  1. V8 Isolates技術: コンテナではなく、軽量な分離環境
  2. 0msコールドスタート: 常に温かい状態を維持
  3. グローバル自動デプロイ: 1 回のデプロイで全世界展開
  4. 豊富なエコシステム: KV、R2、D1 など統合サービス

私たちは、インターネットそのものをプログラム可能にしています。 開発者は地理的な制約から解放され、真にグローバルなアプリケーションを構築できます。

Matthew Prince Cloudflare CEO

開発環境の構築

Cloudflare Workers の開発を始めるのは驚くほど簡単です。

必要なツール

# Wranglerのインストール(CLIツール)
npm install -g wrangler
# または
bun add -g wrangler

# バージョン確認
wrangler --version
# 3.x.x(2025年6月時点)

# Cloudflareアカウントにログイン
wrangler login

プロジェクトの初期化

# 新規プロジェクト作成
wrangler init my-edge-app

# テンプレート選択
? Would you like to use a template?
 Hello World Worker
    Router Worker
    Scheduled Worker
    ChatGPT Plugin

# TypeScript設定
? Would you like to use TypeScript? Yes

プロジェクト構造

my-edge-app/
├── src/
│   └── index.ts        # メインのWorkerコード
├── wrangler.toml       # 設定ファイル
├── package.json
├── tsconfig.json
└── node_modules/

基本的なWorkerの作成

最初の Worker を作成してみましょう。

Hello World Worker

// src/index.ts
export interface Env {
  // 環境変数やバインディングの型定義
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === '/') {
      return new Response('Hello from the Edge!', {
        headers: {
          'content-type': 'text/plain',
        },
      });
    }

    if (url.pathname === '/api/time') {
      const now = new Date().toISOString();
      return Response.json({
        time: now,
        region: request.cf?.region || 'unknown',
      });
    }

    return new Response('Not Found', { status: 404 });
  },
};

開発サーバーの起動

# ローカル開発サーバー起動
wrangler dev

# http://localhost:8787 でアクセス可能

APIエンドポイントの実装

// より実践的なAPI実装
import { Router } from 'itty-router';

const router = Router();

// ユーザー情報API
router.get('/api/users/:id', async (request, env) => {
  const { id } = request.params;

  // KVストレージから取得(後述)
  const user = await env.USERS_KV.get(`user:${id}`, 'json');

  if (!user) {
    return new Response('User not found', { status: 404 });
  }

  return Response.json(user);
});

// POST リクエスト処理
router.post('/api/users', async (request, env) => {
  const body = await request.json();
  const id = crypto.randomUUID();

  await env.USERS_KV.put(
    `user:${id}`,
    JSON.stringify({
      id,
      ...body,
      createdAt: new Date().toISOString(),
    })
  );

  return Response.json({ id }, { status: 201 });
});

// デフォルトハンドラー
export default {
  fetch: router.handle,
};

KVストレージの活用

Workers KV は、グローバルに分散された低レイテンシーのキーバリューストレージです。

KVの設定

# wrangler.toml
name = "my-edge-app"
main = "src/index.ts"
compatibility_date = "2025-06-20"

[[kv_namespaces]]
binding = "USERS_KV"
id = "your-kv-namespace-id"

KVの基本操作

export interface Env {
  USERS_KV: KVNamespace;
}

// データの保存
await env.USERS_KV.put('key', 'value');
await env.USERS_KV.put('user:123', JSON.stringify(userData), {
  expirationTtl: 60 * 60 * 24, // 24時間で期限切れ
});

// データの取得
const value = await env.USERS_KV.get('key');
const user = await env.USERS_KV.get('user:123', 'json');

// リスト取得
const list = await env.USERS_KV.list({ prefix: 'user:' });

// 削除
await env.USERS_KV.delete('key');

キャッシュ戦略

// エッジキャッシュの実装
async function getCachedData(key: string, env: Env): Promise<any> {
  // まずKVから取得を試みる
  const cached = await env.USERS_KV.get(key, 'json');

  if (cached && cached.expiry > Date.now()) {
    return cached.data;
  }

  // キャッシュミスの場合、オリジンから取得
  const fresh = await fetchFromOrigin(key);

  // KVに保存(5分間キャッシュ)
  await env.USERS_KV.put(
    key,
    JSON.stringify({
      data: fresh,
      expiry: Date.now() + 5 * 60 * 1000,
    })
  );

  return fresh;
}

KVのベストプラクティス

  • 読み取りは高速(〜10ms)、書き込みは最終的一貫性(〜60 秒)- 値のサイズは最大 25MB - キーは最大 512 バイト - 適切な TTL を設定してストレージコストを最適化

Durable Objectsで状態管理

Durable Objects は、エッジで強い一貫性を持つ状態管理を可能にします。

Durable Objectの定義

// src/counter.ts
export class Counter implements DurableObject {
  private state: DurableObjectState;
  private value: number = 0;

  constructor(state: DurableObjectState) {
    this.state = state;
    // 永続化された状態を復元
    this.state.blockConcurrencyWhile(async () => {
      const stored = await this.state.storage.get<number>('value');
      this.value = stored || 0;
    });
  }

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    switch (url.pathname) {
      case '/increment':
        this.value++;
        await this.state.storage.put('value', this.value);
        return Response.json({ value: this.value });

      case '/decrement':
        this.value--;
        await this.state.storage.put('value', this.value);
        return Response.json({ value: this.value });

      case '/value':
        return Response.json({ value: this.value });

      default:
        return new Response('Not Found', { status: 404 });
    }
  }
}

Durable Objectの使用

// メインWorkerから呼び出し
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const id = env.COUNTER.idFromName('global-counter');
    const counter = env.COUNTER.get(id);

    // Durable Objectにリクエストを転送
    return counter.fetch(request);
  },
};

リアルタイムチャットの実装

export class ChatRoom implements DurableObject {
  private connections: Set<WebSocket> = new Set();
  private messages: Array<Message> = [];

  async fetch(request: Request): Promise<Response> {
    if (request.headers.get('Upgrade') === 'websocket') {
      const pair = new WebSocketPair();
      await this.handleWebSocket(pair[1]);

      return new Response(null, {
        status: 101,
        webSocket: pair[0],
      });
    }

    // 通常のHTTPリクエスト
    return Response.json({ messages: this.messages });
  }

  async handleWebSocket(ws: WebSocket) {
    ws.accept();
    this.connections.add(ws);

    // 既存メッセージを送信
    ws.send(
      JSON.stringify({
        type: 'history',
        messages: this.messages,
      })
    );

    ws.addEventListener('message', async (event) => {
      const message = JSON.parse(event.data as string);

      // メッセージを保存
      this.messages.push(message);
      await this.state.storage.put('messages', this.messages);

      // 全接続にブロードキャスト
      const broadcast = JSON.stringify({
        type: 'message',
        data: message,
      });

      for (const conn of this.connections) {
        try {
          conn.send(broadcast);
        } catch {
          this.connections.delete(conn);
        }
      }
    });

    ws.addEventListener('close', () => {
      this.connections.delete(ws);
    });
  }
}

R2で大容量ファイル管理

R2 は、S3 互換のオブジェクトストレージで、エグレス料金が無料です。

R2の設定と使用

# wrangler.toml
[[r2_buckets]]
binding = "FILES_BUCKET"
bucket_name = "my-files"
// ファイルアップロード
router.post('/upload', async (request, env) => {
  const formData = await request.formData();
  const file = formData.get('file') as File;

  if (!file) {
    return new Response('No file uploaded', { status: 400 });
  }

  const key = `uploads/${crypto.randomUUID()}-${file.name}`;

  // R2にアップロード
  await env.FILES_BUCKET.put(key, file.stream(), {
    httpMetadata: {
      contentType: file.type,
    },
    customMetadata: {
      uploadedBy: 'user123',
      uploadedAt: new Date().toISOString(),
    },
  });

  return Response.json({
    key,
    size: file.size,
    type: file.type,
  });
});

// ファイル取得
router.get('/files/:key', async (request, env) => {
  const { key } = request.params;
  const object = await env.FILES_BUCKET.get(key);

  if (!object) {
    return new Response('File not found', { status: 404 });
  }

  const headers = new Headers();
  object.writeHttpMetadata(headers);
  headers.set('etag', object.httpEtag);

  return new Response(object.body, { headers });
});

画像変換サービス

// 画像のリサイズとキャッシュ
router.get('/images/:key', async (request, env) => {
  const { key } = request.params;
  const url = new URL(request.url);

  const width = parseInt(url.searchParams.get('w') || '0');
  const height = parseInt(url.searchParams.get('h') || '0');
  const format = url.searchParams.get('format') || 'webp';

  // キャッシュキー
  const cacheKey = `${key}-${width}x${height}-${format}`;

  // キャッシュチェック
  const cached = await env.FILES_BUCKET.get(cacheKey);
  if (cached) {
    return new Response(cached.body, {
      headers: {
        'content-type': `image/${format}`,
        'cache-control': 'public, max-age=31536000',
      },
    });
  }

  // オリジナル画像取得
  const original = await env.FILES_BUCKET.get(key);
  if (!original) {
    return new Response('Image not found', { status: 404 });
  }

  // Cloudflare Image Resizing API
  const resized = await fetch(
    `https://example.com/cdn-cgi/image/width=${width},height=${height},format=${format}/${key}`,
    {
      cf: {
        image: {
          width,
          height,
          format,
        },
      },
    }
  );

  // リサイズ画像をキャッシュ
  await env.FILES_BUCKET.put(cacheKey, resized.body);

  return new Response(resized.body, {
    headers: {
      'content-type': `image/${format}`,
      'cache-control': 'public, max-age=31536000',
    },
  });
});

実践的なアプリケーション例

実際のプロダクションで使えるアプリケーション例を紹介します。

グローバルAPIゲートウェイ

// APIゲートウェイとレート制限
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const clientIP = request.headers.get('CF-Connecting-IP') || '';

    // レート制限チェック
    const rateLimitKey = `rate:${clientIP}`;
    const current = await env.RATE_LIMIT_KV.get(rateLimitKey);

    if (current && parseInt(current) > 100) {
      return new Response('Rate limit exceeded', {
        status: 429,
        headers: {
          'Retry-After': '60',
        },
      });
    }

    // レート制限カウンター更新
    await env.RATE_LIMIT_KV.put(rateLimitKey, String(parseInt(current || '0') + 1), {
      expirationTtl: 60,
    });

    // 認証チェック
    const authHeader = request.headers.get('Authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return new Response('Unauthorized', { status: 401 });
    }

    const token = authHeader.substring(7);
    const user = await validateToken(token, env);

    if (!user) {
      return new Response('Invalid token', { status: 403 });
    }

    // バックエンドAPIへプロキシ
    const backendUrl = new URL(request.url);
    backendUrl.hostname = 'api.backend.com';

    const backendRequest = new Request(backendUrl, request);
    backendRequest.headers.set('X-User-ID', user.id);
    backendRequest.headers.set('X-Edge-Region', request.cf?.region || 'unknown');

    return fetch(backendRequest);
  },
};

エッジでのA/Bテスト

// A/Bテストの実装
router.get('/', async (request, env) => {
  const cookie = parseCookie(request.headers.get('Cookie') || '');
  let variant = cookie.variant;

  if (!variant) {
    // ランダムに振り分け
    variant = Math.random() < 0.5 ? 'A' : 'B';
  }

  // メトリクス記録
  await env.ANALYTICS_DO.get(env.ANALYTICS_DO.idFromName('ab-test')).fetch(
    new Request('https://internal/record', {
      method: 'POST',
      body: JSON.stringify({
        variant,
        timestamp: Date.now(),
        country: request.cf?.country,
      }),
    })
  );

  // バリアントに応じたコンテンツ返却
  const html = variant === 'A' ? await generateVariantA() : await generateVariantB();

  return new Response(html, {
    headers: {
      'content-type': 'text/html',
      'set-cookie': `variant=${variant}; Path=/; Max-Age=2592000`,
    },
  });
});

エッジで処理開始

最寄りのエッジで即座に処理

エッジで検証

JWTトークンをエッジで検証

KVストレージ参照

高速なキャッシュレイヤー

エッジで実行

必要な処理をエッジで完結

低レイテンシー返却

5-20msで応答

パフォーマンス最適化

エッジでのパフォーマンスを最大化するテクニックを紹介します。

キャッシュ戦略

// スマートキャッシュの実装
const cache = caches.default;

router.get('/api/data/:id', async (request, env) => {
  const cacheKey = new Request(request.url, request);
  const cached = await cache.match(cacheKey);

  if (cached) {
    // キャッシュヒット
    return cached;
  }

  // データ取得
  const data = await fetchData(request.params.id, env);
  const response = Response.json(data);

  // キャッシュヘッダー設定
  response.headers.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
  response.headers.set('CDN-Cache-Control', 'max-age=3600');

  // エッジキャッシュに保存
  ctx.waitUntil(cache.put(cacheKey, response.clone()));

  return response;
});

並列処理の活用

// 複数のAPIを並列で呼び出し
router.get('/dashboard', async (request, env) => {
  const userId = getUserId(request);

  // 並列でデータ取得
  const [user, posts, analytics] = await Promise.all([
    env.USERS_KV.get(`user:${userId}`, 'json'),
    fetchUserPosts(userId),
    fetchAnalytics(userId),
  ]);

  return Response.json({
    user,
    posts,
    analytics,
  });
});

ストリーミングレスポンス

// 大量データのストリーミング
router.get('/export/users', async (request, env) => {
  const { readable, writable } = new TransformStream();
  const writer = writable.getWriter();

  // 非同期でデータをストリーミング
  ctx.waitUntil(
    (async () => {
      await writer.write('[');

      let first = true;
      const list = await env.USERS_KV.list();

      for (const key of list.keys) {
        const user = await env.USERS_KV.get(key.name, 'json');

        if (!first) await writer.write(',');
        await writer.write(JSON.stringify(user));
        first = false;
      }

      await writer.write(']');
      await writer.close();
    })()
  );

  return new Response(readable, {
    headers: {
      'content-type': 'application/json',
      'content-disposition': 'attachment; filename="users.json"',
    },
  });
});
レスポンス速度向上 95 %
グローバルカバレッジ 90 %
コスト削減 85 %

従来のサーバーレスと比較した改善率

セキュリティとベストプラクティス

エッジ環境でのセキュリティは特に重要です。

セキュリティ対策

// CSPヘッダーの設定
function addSecurityHeaders(response: Response): Response {
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
  );
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-XSS-Protection', '1; mode=block');

  return response;
}

// CORS設定
function handleCORS(request: Request, response: Response): Response {
  const origin = request.headers.get('Origin');

  if (origin && isAllowedOrigin(origin)) {
    response.headers.set('Access-Control-Allow-Origin', origin);
    response.headers.set('Access-Control-Allow-Credentials', 'true');
  }

  if (request.method === 'OPTIONS') {
    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    response.headers.set('Access-Control-Max-Age', '86400');
  }

  return response;
}

エラーハンドリング

// グローバルエラーハンドラー
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    try {
      return await router.handle(request, env, ctx);
    } catch (error) {
      // エラーログ記録
      ctx.waitUntil(logError(error, request, env));

      // 本番環境では詳細を隠す
      if (env.ENVIRONMENT === 'production') {
        return new Response('Internal Server Error', {
          status: 500,
          headers: {
            'X-Error-Id': crypto.randomUUID(),
          },
        });
      }

      // 開発環境では詳細表示
      return new Response(error.stack || error.message, {
        status: 500,
        headers: {
          'content-type': 'text/plain',
        },
      });
    }
  },
};

セキュリティチェックリスト

  • 環境変数に機密情報を保存(ハードコーディング禁止)-[]入力値の検証とサニタイゼーション - []適切な CORS ポリシーの設定 -[]レート制限の実装 -[]セキュリティヘッダーの設定 -[] エラー情報の適切な隠蔽

まとめ

エッジコンピューティングは、web 開発の新しいパラダイムです。

エッジコンピューティングが適している場面

グローバルアプリケーション: 世界中のユーザーに低レイテンシーで提供 ✅ APIゲートウェイ: 認証、レート制限、ルーティング ✅ 静的サイト生成: エッジでの動的レンダリング ✅ リアルタイム処理: WebSocket、Server-Sent Events ✅ 画像・動画処理: リサイズ、フォーマット変換

今後の展望

  • Ai on Edge: エッジでの機械学習推論
  • より多くの言語サポート: Go、Python 対応
  • エッジデータベース: 分散 sql データベース
  • エッジコンテナ: より複雑なアプリケーション対応

Cloudflare Workers を導入してから、api のレスポンス時間が 80%短縮され、 インフラコストも 50%削減できました。 エッジコンピューティングは、もはや選択肢ではなく必須です。

開発チーム Daily Hack編集部

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。