マルチテナント SaaS のバックエンド設計:Workers + D1 + R2 を活用した高速化とコスト最適化
はじめに
本稿では、自社の BtoB SaaS サービスである請求書管理サービスを、Cloudflare の Workers、D1、R2、KV、Queues を活用して再設計し、従来の AWS Lambda + Aurora Serverless v2 + S3 構成からの移行を通じて得られた知見を紹介します。マルチテナント対応のバックエンド設計において、パフォーマンスの向上とコストの最適化をどのように実現したかを解説します。
技術的背景
請求書管理サービスは月間 5,000 社の MAU を抱える BtoB SaaS です。従来、AWS Lambda + Aurora Serverless v2 + S3 で構成されていましたが、コールドスタートによるレイテンシの問題や Aurora のスケーリング遅延、S3 と CloudFront の冗長な署名付き URL 生成、そしてマルチリージョン展開とテナント分離のための高コスト化が課題でした。
アーキテクチャ解説
新しいアーキテクチャは、Workers、D1、R2、KV、Queues で構成されます。
- Workers: API Gateway とビジネスロジックを担当し、テナントルーティングを含む。
- D1: テナントごとにデータベースを分離し、D1 の API で動的に作成。
- R2: 請求書 PDF や添付ファイルの保存に使用。
- KV: セッション管理とテナント設定のキャッシュに利用。
- Queues: PDF 生成の非同期処理とメール送信キューとして機能。
クライアントからのリクエストは Workers へ届き、認証とテナント判定後、D1 に対してデータの読み書きを行います。R2 はファイルの保存に使用され、KV はセッション管理とテナント設定のキャッシュとして活用されます。PDF 生成は Queues を通じて非同期処理され、生成された PDF は R2 に保存されます。
実装詳細
Hono フレームワークを用いて API を構築し、ミドルウェアでテナント判定を実行しています。D1 のマイグレーションは Drizzle ORM で管理し、テナント作成時に自動実行しています。R2 への PDF アップロードは Workers から直接行い、署名付き URL は Workers で生成します。Queues の Consumer では Puppeteer を用いて PDF のレンダリングを行っています。
// テナントルーティングミドルウェア
const tenantMiddleware = createMiddleware(async (c, next) => {
const tenantId = c.req.header('X-Tenant-ID');
if (!tenantId) return c.json({ error: 'Tenant ID required' }, 400);
// KV からテナント設定を取得(キャッシュ)
const config = await c.env.TENANT_KV.get(`tenant:${tenantId}`, 'json');
if (!config) return c.json({ error: 'Tenant not found' }, 404);
// テナント専用 D1 バインディングを解決
c.set('tenantConfig', config);
c.set('db', c.env[`DB_${tenantId}`]);
await next();
});
// PDF 生成 Queue Consumer
export default {
async queue(batch, env) {
for (const msg of batch.messages) {
const { invoiceId, tenantId } = msg.body;
const html = await renderInvoiceHTML(env, tenantId, invoiceId);
const pdf = await env.BROWSER.pdf(html);
await env.R2_BUCKET.put(`${tenantId}/invoices/${invoiceId}.pdf`, pdf);
msg.ack();
}
}
};
ベストプラクティス
- テナント分離: D1 をテナント単位で分けることでクエリの RLS 不要、パフォーマンス向上。
- コールドスタート対策: Workers は V8 Isolate なのでコールドスタートほぼゼロ。
- コスト管理: D1 は読み取り 500 万行/月無料、書き込みも安い。テナント数が増えてもリニアにスケール。
- エラーハンドリング: Queues の自動リトライ (3 回) + DLQ で PDF 生成失敗を検知。
- 監視: Workers Analytics Engine でテナントごとのリクエスト数・レイテンシを計測。
検証中の苦労
- D1 のトランザクション: batch() API で複数クエリをまとめる必要があり、ORM の transaction() がそのままでは使えなかった → Drizzle の D1 アダプタで batch() をラップ。
- Browser Rendering API のメモリ制限: 請求書が 50 ページ超のテナントで OOM → ページ分割レンダリングに変更。
- KV の eventual consistency: テナント設定更新後に旧値が返るケースがあった → cache TTL を 60 秒に設定し許容。
運用上の課題
- D1 の 10GB 上限: 大口テナントのデータ量が年内に上限に達する見込み → アーカイブ戦略を検討中。
- Workers の CPU 時間制限 (30 秒): 大量データのバッチ処理は Queues に分割する必要。
- Drizzle ORM の D1 対応がまだベータ的で、マイグレーション周りにバグあり。
まとめと今後の展望
本稿では、Cloudflare の Workers、D1、R2、KV、Queues を活用したマルチテナント SaaS のバックエンド設計について解説しました。従来の AWS Lambda + Aurora Serverless v2 + S3 構成からの移行により、パフォーマンスの向上とコストの最適化を実現しました。次に、Vectorize + Workers AI でテナントの請求データから異常検知、Hyperdrive で既存の PostgreSQL (レポーティング DB) への接続を高速化、Durable Objects でリアルタイム共同編集機能を実装、Workers for Platforms でテナントごとのカスタムロジック実行環境を提供することを検討しています。