エッジコンピューティング入門 - Cloudflare Workersで作るサーバーレスアプリ
エッジコンピューティングは、ユーザーに最も近い場所でコードを実行する新しいパラダイムです。Cloudflare Workersを使って、0msコールドスタートのグローバルアプリケーションを構築する方法を解説します。
Cloudflare Workers、Vercel Edge Functions、Deno Deployの最新機能を徹底比較。パフォーマンス最適化、コスト削減、実装パターン、本番運用のベストプラクティスまで、エッジコンピューティングを極める完全ガイドです。
2025 年、エッジコンピューティングは新たな転換点を迎えています。Cloudflare Workers が 6 月にコンテナサポートを追加、Vercel が 300 秒の実行時間制限を導入、Deno Deploy は 35 リージョンから 6 リージョンへ戦略的撤退。この激動の中で、どのプラットフォームを選び、どう活用すべきか?
本記事では、400msのコールドスタートから始まる実装の詳細から、月額5ドルで1000万リクエストを処理する最適化テクニックまで、エッジコンピューティングの全てを解説します。
エッジコンピューティングが解決する 3 つの課題:
主要プラットフォームが本格採用
エッジでのAI推論が可能に
Cloudflare Workersがコンテナ対応
CDN、エッジ、DBの垂直統合
Durable Objects型の状態管理が標準化
機能 | Cloudflare Workers | Vercel Edge | Deno Deploy |
---|---|---|---|
基盤技術 | V8 Isolates | Cloudflare Workers | V8 Isolates |
グローバル展開 | 280+ 都市 | 119 PoPs | 6 リージョン |
コールドスタート | 0ms(常時warm) | 9x高速化 | 400ms中央値 |
実行時間制限 | 30秒(有料:30分) | 300秒 | 60秒 |
メモリ制限 | 128MB | 128MB | 512MB |
料金(無料枠) | 10万req/日 | 100万req/月 | 制限付き無料 |
追加料金 | $5/1000万req | $2/100万req | $2/100万req |
コンテナ対応 | 2025年6月予定 | 非対応 | 非対応 |
状態管理 | Durable Objects | Edge Config | KV Store |
データベース統合 | D1, Hyperdrive | Vercel Postgres | Deno KV |
チャートを読み込み中...
Cloudflare Workersを選ぶべき場合:
Vercel Edge Functionsを選ぶべき場合:
Deno Deployを選ぶべき場合:
// Cloudflare Workers
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// リクエストの解析
const url = new URL(request.url);
// キャッシュの確認
const cacheKey = new Request(url.toString(), request);
const cache = caches.default;
let response = await cache.match(cacheKey);
if (!response) {
// ビジネスロジックの実行
response = await handleRequest(request, env);
// レスポンスをキャッシュ
ctx.waitUntil(cache.put(cacheKey, response.clone()));
}
return response;
}
};
async function handleRequest(request: Request, env: Env) {
// KVストアからデータ取得
const data = await env.MY_KV.get('key', 'json');
// D1データベースクエリ
const result = await env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(1).first();
return Response.json({
data,
user: result,
timestamp: new Date().toISOString()
});
}
// Vercel Edge Functions
import { NextRequest, NextResponse } from 'next/server';
export const config = {
runtime: 'edge',
};
export async function GET(request: NextRequest) {
// Edge Configからフィーチャーフラグ取得
const featureFlags = await getEdgeConfig();
// 地理的情報の取得
const geo = request.geo;
const country = geo?.country || 'unknown';
// パーソナライズされたレスポンス
const content = await personalizeContent(country);
// キャッシュヘッダーの設定
return NextResponse.json(
{
content,
features: featureFlags,
location: country
},
{
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
},
}
);
}
async function personalizeContent(country: string) {
// 国別のコンテンツを返す
const contentMap = {
'JP': '日本向けコンテンツ',
'US': 'Content for US',
'default': 'Default content'
};
return contentMap[country] || contentMap.default;
}
// Deno Deploy
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
serve(async (req: Request) => {
const url = new URL(req.url);
// Deno KVの使用
const kv = await Deno.openKv();
// ルーティング
if (url.pathname === "/api/users") {
return handleUsers(kv);
} else if (url.pathname === "/api/cache") {
return handleCache(req, kv);
}
return new Response("Not Found", { status: 404 });
});
async function handleUsers(kv: Deno.Kv) {
// KVからユーザーリスト取得
const users = [];
const entries = kv.list({ prefix: ["users"] });
for await (const entry of entries) {
users.push(entry.value);
}
return Response.json({ users });
}
async function handleCache(req: Request, kv: Deno.Kv) {
const cacheKey = ["cache", req.url];
// キャッシュの確認
const cached = await kv.get(cacheKey);
if (cached.value) {
return Response.json(cached.value);
}
// データの生成
const data = {
timestamp: Date.now(),
random: Math.random()
};
// 60秒間キャッシュ
await kv.set(cacheKey, data, {
expireIn: 60 * 1000
});
return Response.json(data);
}
// 地理情報に基づくルーティング
export async function geoRoute(request: Request) {
const cf = (request as any).cf;
// 地理情報の取得
const country = cf?.country || 'unknown';
const continent = cf?.continent || 'unknown';
// リージョン別のバックエンドURL
const backends = {
'AS': 'https://api-asia.example.com',
'EU': 'https://api-eu.example.com',
'NA': 'https://api-na.example.com',
'default': 'https://api.example.com'
};
const backendUrl = backends[continent] || backends.default;
// プロキシリクエスト
const backendRequest = new Request(backendUrl + new URL(request.url).pathname, {
method: request.method,
headers: request.headers,
body: request.body
});
return fetch(backendRequest);
}
// A/Bテストの実装
export async function abTest(request: Request) {
const url = new URL(request.url);
// ユーザー識別子の取得または生成
const userId = getCookie(request, 'user_id') || crypto.randomUUID();
// バリアントの決定(永続的)
const variant = hashToVariant(userId);
// バリアント別の処理
const handlers = {
'A': () => handleVariantA(request),
'B': () => handleVariantB(request),
'control': () => handleControl(request)
};
const response = await handlers[variant]();
// クッキーにユーザーIDを保存
response.headers.append('Set-Cookie', `user_id=${userId}; Path=/; Max-Age=31536000`);
// アナリティクスへの送信
trackEvent({
event: 'page_view',
variant,
userId,
timestamp: Date.now()
});
return response;
}
function hashToVariant(userId: string): string {
const hash = Array.from(userId).reduce((acc, char) =>
acc + char.charCodeAt(0), 0
);
const percentage = hash % 100;
if (percentage < 33) return 'A';
if (percentage < 66) return 'B';
return 'control';
}
// クライアントサイドで画像をリクエスト
async function loadImage(url: string) {
const img = new Image();
img.src = url;
// 大きな画像がそのままロード
await img.decode();
document.body.appendChild(img);
}
// エッジで画像を最適化
export async function optimizeImage(request: Request) {
const url = new URL(request.url);
const imageUrl = url.searchParams.get('url');
const width = parseInt(url.searchParams.get('w') || '800');
const quality = parseInt(url.searchParams.get('q') || '85');
// Cloudflare Image Resizing
const cf = {
image: {
width,
quality,
format: 'auto', // WebP/AVIF自動選択
fit: 'scale-down'
}
};
const imageRequest = new Request(imageUrl, {
headers: request.headers,
cf
});
const response = await fetch(imageRequest);
// キャッシュヘッダーの設定
const newResponse = new Response(response.body, response);
newResponse.headers.set('Cache-Control', 'public, max-age=31536000');
return newResponse;
}
// クライアントサイドで画像をリクエスト
async function loadImage(url: string) {
const img = new Image();
img.src = url;
// 大きな画像がそのままロード
await img.decode();
document.body.appendChild(img);
}
// エッジで画像を最適化
export async function optimizeImage(request: Request) {
const url = new URL(request.url);
const imageUrl = url.searchParams.get('url');
const width = parseInt(url.searchParams.get('w') || '800');
const quality = parseInt(url.searchParams.get('q') || '85');
// Cloudflare Image Resizing
const cf = {
image: {
width,
quality,
format: 'auto', // WebP/AVIF自動選択
fit: 'scale-down'
}
};
const imageRequest = new Request(imageUrl, {
headers: request.headers,
cf
});
const response = await fetch(imageRequest);
// キャッシュヘッダーの設定
const newResponse = new Response(response.body, response);
newResponse.headers.set('Cache-Control', 'public, max-age=31536000');
return newResponse;
}
// 1. グローバル変数でのコネクション再利用
let dbConnection: Database | null = null;
async function getDB(): Promise<Database> {
if (!dbConnection) {
dbConnection = await createConnection();
}
return dbConnection;
}
// 2. レイジーローディング
const heavyModule = {
_instance: null as HeavyModule | null,
async get(): Promise<HeavyModule> {
if (!this._instance) {
const { HeavyModule } = await import('./heavy-module');
this._instance = new HeavyModule();
}
return this._instance;
}
};
// 3. プリフェッチとウォーミング
export async function scheduled(event: ScheduledEvent) {
// 定期的にエンドポイントをウォーム
const endpoints = ['/api/hot-path-1', '/api/hot-path-2'];
await Promise.all(
endpoints.map(endpoint =>
fetch(`https://example.com${endpoint}`, {
method: 'HEAD'
})
)
);
}
// ストリーミングレスポンスで大きなデータを処理
export async function streamLargeData(request: Request) {
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
// 非同期でデータをストリーミング
(async () => {
try {
await writer.write('[');
for (let i = 0; i < 1000000; i++) {
const chunk = JSON.stringify({ id: i, data: 'item' });
await writer.write(i > 0 ? ',' + chunk : chunk);
// メモリ圧迫を避けるため定期的にyield
if (i % 1000 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
await writer.write(']');
} finally {
await writer.close();
}
})();
return new Response(readable, {
headers: {
'Content-Type': 'application/json',
'Transfer-Encoding': 'chunked'
}
});
}
キャッシュ戦略 | 用途 | 実装例 |
---|---|---|
Cache API | 動的コンテンツ | caches.default.put() |
KV Store | セッション/設定 | env.KV.put() |
CDNキャッシュ | 静的アセット | Cache-Control: max-age=31536000 |
ブラウザキャッシュ | パーソナライズ | Cache-Control: private |
Stale-While-Revalidate | 準リアルタイム | stale-while-revalidate=300 |
※ 帯域幅やその他のサービス利用料は含まず
// 1. 効率的なキャッシング
export async function costOptimizedHandler(request: Request) {
const cache = caches.default;
const cacheKey = new Request(request.url, {
method: 'GET',
headers: { 'Cache-Version': 'v1' }
});
// キャッシュヒット率を最大化
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// 2. バッチ処理でAPI呼び出しを削減
const batchKey = `batch:${Math.floor(Date.now() / 60000)}`;
const batchData = await env.KV.get(batchKey, 'json');
if (batchData) {
response = new Response(JSON.stringify(batchData));
} else {
// 高コストな処理
const data = await expensiveOperation();
// バッチキャッシュに保存
await env.KV.put(batchKey, JSON.stringify(data), {
expirationTtl: 60
});
response = new Response(JSON.stringify(data));
}
// 3. 適切なキャッシュヘッダー
response.headers.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
// エッジキャッシュに保存
await cache.put(cacheKey, response.clone());
return response;
}
// 認証、レート制限、ルーティングを統合
export async function apiGateway(request: Request) {
// 認証チェック
const authResult = await authenticate(request);
if (!authResult.valid) {
return new Response('Unauthorized', { status: 401 });
}
// レート制限
const rateLimitKey = `rate:${authResult.userId}`;
const requests = await incrementAndCheck(rateLimitKey);
if (requests > 1000) {
return new Response('Rate limit exceeded', {
status: 429,
headers: {
'Retry-After': '3600'
}
});
}
// 動的ルーティング
const route = matchRoute(request.url);
const backend = selectBackend(route, authResult.tier);
// リクエストの転送
return proxyRequest(request, backend, {
headers: {
'X-User-ID': authResult.userId,
'X-User-Tier': authResult.tier
}
});
}
// WebSocket接続のプロキシとスケーリング
export async function websocketProxy(request: Request) {
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader !== 'websocket') {
return new Response('Expected WebSocket', { status: 426 });
}
// 接続先の選択(負荷分散)
const backend = await selectWebSocketBackend();
// WebSocketペアの作成
const { 0: client, 1: server } = new WebSocketPair();
// バックエンドへの接続
const backendWS = new WebSocket(backend);
// メッセージの中継
server.accept();
server.addEventListener('message', event => {
backendWS.send(event.data);
});
backendWS.addEventListener('message', event => {
server.send(event.data);
});
// エラーハンドリング
const closeHandler = () => {
server.close();
backendWS.close();
};
server.addEventListener('close', closeHandler);
backendWS.addEventListener('close', closeHandler);
return new Response(null, {
status: 101,
webSocket: client
});
}
// Cloudflare Workers AIを使用した推論
export async function mlInference(request: Request) {
const formData = await request.formData();
const image = formData.get('image') as File;
// 画像分類
const imageArray = new Uint8Array(await image.arrayBuffer());
const classification = await env.AI.run(
'@cf/microsoft/resnet-50',
{ image: Array.from(imageArray) }
);
// テキスト分析
const text = formData.get('text') as string;
const sentiment = await env.AI.run(
'@cf/huggingface/distilbert-sst-2-int8',
{ text }
);
// 結果の組み合わせ
return Response.json({
image: classification,
sentiment: sentiment,
timestamp: new Date().toISOString()
});
}
// 包括的なロギングとメトリクス
export async function observableHandler(request: Request) {
const startTime = Date.now();
const requestId = crypto.randomUUID();
try {
// リクエストログ
await logEvent({
type: 'request',
requestId,
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers),
geo: (request as any).cf
});
// ビジネスロジック
const response = await processRequest(request);
// レスポンスログ
await logEvent({
type: 'response',
requestId,
status: response.status,
duration: Date.now() - startTime
});
// メトリクスの送信
await sendMetrics({
endpoint: new URL(request.url).pathname,
duration: Date.now() - startTime,
status: response.status
});
return response;
} catch (error) {
// エラーログ
await logEvent({
type: 'error',
requestId,
error: error.message,
stack: error.stack,
duration: Date.now() - startTime
});
return new Response('Internal Server Error', { status: 500 });
}
}
Miniflare/Wrangler Dev
Branch Deployments
5% → 25% → 50% → 100%
即座にロールバック可能
アラート設定とダッシュボード
✅ 環境変数でのシークレット管理 ✅ CORS ヘッダーの適切な設定 ✅ 入力値の検証とサニタイゼーション ✅ レート制限の実装 ✅ CSRF トークンの検証 ✅ セキュリティヘッダーの設定 ✅ 定期的な依存関係の更新
制限事項 | 影響 | 対策 |
---|---|---|
CPU時間制限 | 重い処理が不可 | バックグラウンドジョブと分離 |
メモリ制限 | 大規模データ処理が困難 | ストリーミング処理を活用 |
実行時間制限 | 長時間処理が不可 | 非同期処理とキューイング |
ファイルシステムなし | 一時ファイル作成不可 | オブジェクトストレージ活用 |
ネイティブモジュール非対応 | 一部ライブラリ使用不可 | WebAssembly版を使用 |
コンテナのサポートにより、エッジコンピューティングは 単なる軽量な処理から、本格的なアプリケーション実行環境へと進化します。 2025 年後半には、エッジネイティブなアーキテクチャが主流になるでしょう。
チャートを読み込み中...
エッジコンピューティングは、2025 年において単なるパフォーマンス最適化の手段から、アプリケーションアーキテクチャの中核へと進化しました。
✅ ユースケースがエッジに適しているか評価 ✅ プラットフォームの選定(コスト vs 機能) ✅ 既存システムとの統合方法を設計 ✅ モニタリングとロギングの準備 ✅ 段階的移行計画の策定 ✅ チーム全体でのナレッジ共有
エッジコンピューティングは、適切に活用すればパフォーマンス向上とコスト削減を同時に実現できる強力な技術です。本記事で紹介したベストプラクティスを参考に、あなたのプロジェクトでもエッジファーストな設計を検討してみてください。