Next.jsとPrismaをCloudflareにデプロイして月300万のDBクエリに無料で耐える
はじめに
Next.js を Cloudflare にホスティングしようとすると、必然的に Edge Runtime 環境になります。しかし、Edge Runtime 環境では、Node.js Runtime と異なり、Prisma がそのまま使えません。
最初に思い浮かぶ解決策は Prisma Accelerate です。Prisma Accelerate は公式のサービスで、接続プールイングやグローバルキャッシュ機能を備えており、Edge Runtime でも Prisma を使えるようにします。
しかし、無料プランだと月に 6 万クエリの制限があり、本番運用には不安が残ります。
そこで、今回は Prisma Accelerate を自前で Cloudflare Workers 上に構築し、本番運用に耐えうるサービスを無料で開発する方法を紹介します。この方法なら、無料プランでも 月に 300 万クエリに耐えることができます。
なお、実装については後述のパッケージの開発者のこちらの記事並びにサンプルコードを参考にしています。
prisma-accelerate-local
自前で Prisma Accelerate を構築するには、 prisma-accelerate-local というパッケージを使用します。
本来はローカル開発環境用ですが、これを Cloudflare Workers 上で動かすことで、Prisma Accelerate のセルフホスティングが可能です。
Cloudflare Workers を準備する
まず、Cloudflare Workers の開発環境を準備します。以下のリポジトリを clone して、README.md に従って進めるだけで簡単に構築ができます。
また、prisma-accelerate-local の開発者様のサンプルリポジトリも参考にしてください。
以下の内容は、上記のリポジトリを 0 から構築する手順です。
Cloudflare Workers プロジェクトの作成
まず初めにプロジェクトを作成します。Cloudflare Wrangler を使って、CLI から作成します。
npx wrangler init prisma-accelerate-pg-workers
全ての質問に対してy
またはyes
で大丈夫です。
途中でプロジェクトのタイプの選択肢が出てきたら、 "Hello World" Worker
を選択します(スペースキーで選択できます)。
最後の質問に yes
と回答すると、Cloudflare Workers に自動でデプロイされます。
What type of application do you want to create?
● "Hello World" Worker
○ "Hello World" Worker (Python)
○ "Hello World" Durable Object
○ Website or web app
○ Example router & proxy Worker
○ Scheduled Worker (Cron Trigger)
○ Queue consumer & producer Worker
○ API starter (OpenAPI compliant)
○ Worker built from a template hosted in a git repository
パッケージのインストール
prisma-accelerate-local パッケージとその他 prisma 関連パッケージをインストールします。
なお、今回は PostgreSQL を使用する想定で進めます。
npm install @prisma/client prisma-accelerate-local @prisma/adapter-pg @prisma/adapter-pg-worker @prisma/pg-worker
.dev.vars
ファイルに環境変数を設定
ルートディレクトリに .dev.vars
ファイルを作成し、以下のように環境変数を設定します。
xxx
には任意のランダムな文字列を設定して下さい。この値は後に API_KEY
の生成の種として使います。
PRISMA_ACCELERATE_SECRET=xxx
デプロイ先にも反映するために以下のコマンドを実行します。
入力が求められるので、上と同じ値を入力してください。
npx wrangler secret put PRISMA_ACCELERATE_SECRET
Cloudflare KV を作成
Cloudflare のダッシュボードに移動します。
左メニューから Workers & Pages
を選択し、配下の KV
を選択します。
このような画面に遷移するので、右側の Create Namespace
をクリックします。
Namespace Name
には識別しやすい名前を設定します。ここでは、prisma-accelerate-pg-workers
とします。入力したら Add
をクリックします。
作成された KV の ID を控えておきます。
wrangler.toml
の設定
続いて、wrangler.toml
ファイルを編集します。
wrangler.toml
では Cloudflare Workers のデプロイメントに関する設定をします。
コメントアウトされた設定から、必要な行のコメントを外して編集します。
kv_namespaces
の id には先ほど作成した KV の ID を設定します。
#:schema node_modules/wrangler/config-schema.json
name = "prisma-accelerate-pg-workers"
main = "src/index.ts"
compatibility_date = "2024-05-29"
compatibility_flags = ["nodejs_compat"]
+ minify = true
# Automatically place your workloads in an optimal location to minimize latency.
# If you are running back-end logic in a Worker, running it closer to your back-end infrastructure
# rather than the end user may result in better performance.
# Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
+ [placement]
+ mode = "smart"
...
# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.
# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces
+ [[kv_namespaces]]
+ binding = "KV"
+ id = "xxxxxx"
...
型定義
まず、Wrangler を使って、環境変数を読み込むための型を自動生成します。
npm run cf-typegen
worker-configuration.d.ts
に型が生成されます。
interface Env {
KV: KVNamespace;
PRISMA_ACCELERATE_SECRET: string;
}
つづいて types
ディレクトリを作成し、prisma と wasm に関する型を定義します。
declare module '@prisma/client/runtime/wasm.js' {
export * from '@prisma/client/runtime/library';
}
declare module '*.wasm' {
const content: any;
export default content;
}
polyfill の追加
export * from 'node:util';
実装
最後に、src/index.ts
に Prisma Accelerate 相当の処理を実装します。
import { Pool } from '@prisma/pg-worker';
import { PrismaPg } from '@prisma/adapter-pg-worker';
import WASM from '@prisma/client/runtime/query_engine_bg.postgresql.wasm';
import { PrismaAccelerate, PrismaAccelerateConfig, ResultError } from 'prisma-accelerate-local/lib';
import { getPrismaClient } from '@prisma/client/runtime/wasm.js';
const getAdapter = (datasourceUrl: string) => {
const url = new URL(datasourceUrl);
const schema = url.searchParams.get('schema') ?? undefined;
const pool = new Pool({
connectionString: url.toString() ?? undefined,
});
return new PrismaPg(pool, { schema });
};
let prismaAccelerate: PrismaAccelerate;
const getPrismaAccelerate = async ({
secret,
onRequestSchema,
onChangeSchema,
}: {
secret: string;
onRequestSchema: PrismaAccelerateConfig['onRequestSchema'];
onChangeSchema: PrismaAccelerateConfig['onChangeSchema'];
}) => {
if (prismaAccelerate) {
return prismaAccelerate;
}
prismaAccelerate = new PrismaAccelerate({
singleInstance: true,
secret,
adapter: getAdapter,
getRuntime: () => require('@prisma/client/runtime/query_engine_bg.postgresql.js'),
getQueryEngineWasmModule: async () => WASM,
getPrismaClient,
onRequestSchema,
onChangeSchema,
});
return prismaAccelerate;
};
const createResponse = async (result: Promise<unknown>) => {
try {
const response = await result;
return new Response(JSON.stringify(response), {
headers: { 'content-type': 'application/json' },
});
} catch (e) {
if (e instanceof ResultError) {
console.error(e.value);
return new Response(JSON.stringify(e.value), {
status: e.code,
headers: { 'content-type': 'application/json' },
});
}
return new Response(JSON.stringify(e), {
status: 500,
headers: { 'content-type': 'application/json' },
});
}
};
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const prismaAccelerate = await getPrismaAccelerate({
secret: env.PRISMA_ACCELERATE_SECRET,
onRequestSchema: ({ engineVersion, hash, datasourceUrl }) => env.KV.get(`schema-${engineVersion}:${hash}:${datasourceUrl}`),
onChangeSchema: ({ inlineSchema, engineVersion, hash, datasourceUrl }) =>
env.KV.put(`schema-${engineVersion}:${hash}:${datasourceUrl}`, inlineSchema, { expirationTtl: 60 * 60 * 24 * 7 }),
});
const url = new URL(request.url);
const paths = url.pathname.split('/');
const command = paths[3];
const headers = Object.fromEntries(request.headers.entries());
if (request.method === 'POST') {
const body = await request.text();
if (command === 'graphql') {
return createResponse(prismaAccelerate.query({ body, hash: paths[2], headers }));
}
if (command === 'transaction') {
return createResponse(prismaAccelerate.startTransaction({ body, hash: paths[2], headers, version: paths[1] }));
}
if (command === 'itx') {
const id = paths[4];
const subCommand = paths[5];
if (subCommand === 'commit') {
return createResponse(prismaAccelerate.commitTransaction({ id, hash: paths[2], headers }));
}
if (subCommand === 'rollback') {
return createResponse(prismaAccelerate.rollbackTransaction({ id, hash: paths[2], headers }));
}
}
} else if (request.method === 'PUT' && command === 'schema') {
const body = await request.text();
return createResponse(prismaAccelerate.updateSchema({ body, hash: paths[2], headers }));
}
return new Response('Not Found', { status: 404 });
},
};
デプロイ
ここまで実装できたら、Cloudflare Workers にデプロイします。
npm run deploy
Next.js 側の修正
Next.js プロジェクトでは次の 2 点について設定する必要があります。
- Prisma クライアントを edge 用に変更
-
DATABASE_URL
を Prisma Accelerate 用に修正
それぞれ順に解説していきます。
1. Prisma クライアントを edge 用に変更
まず、Prisma クライアントを edge 用に変更します。
具体的には、 PrismaClient
の import 先を @prisma/client
から @prisma/client/edge
に変更します。
また、PrismaClient を作成する際に withAccelerate
を呼び出すようにします。
import { PrismaClient } from "@prisma/client/edge";
import { withAccelerate } from "@prisma/extension-accelerate";
const prismaClientSingleton = () => {
return new PrismaClient().$extends(withAccelerate());
};
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
export default prisma;
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
DATABASE_URL
を Prisma Accelerate 用に修正
2. 次に、環境変数の DATABASE_URL
を Cloudflare Workers のものに設定します。
API_KEY
を生成
まず初めに次のコマンドを実行します。
--secret
には、 .dev.vars
で指定した PRISMA_ACCELERATE_SECRET
と同じ値を指定します。
--make
には、supabase などの DATABASE_URL
を指定します。
npx prisma-accelerate-local --secret enter_your_secret --make postgres://xxx
実行すると、ey
から始まる API_KEY
が出力されるのでコピーしておきます。
環境変数の設定
.env.local
などに以下のように設定します。
DATABASE_URL=prisma://xxxx.workers.dev?api_key=your_api_key
注意点として、URL のスキーマが prisma
であること、ホストが xxxx.workers.dev
であること、クエリパラメータに api_key
が含まれていることを確認してください。
おわりに
Cloudflare Workers 上に自前で Prisma Accelerate を構築し、Next.js と Prisma を無料で本番運用する方法を紹介しました。
Edge Runtime 環境での制約を克服し、高いパフォーマンスを維持しながらもコストを抑えることが可能です。特に、月間 300 万アクセスにも耐えうる堅牢なサービスを構築できる点が大きな魅力です。
ぜひ、この方法を試してみて、皆さんのプロジェクトに役立ててください。
エラーハンドリング
library
or binary
PrismaClientValidationError: Invalid client engine type, please use prisma generate の引数に --no-engine
をつけて再度実行して下さい。
Unauthorized, check your connection string: {"type":"UnknownJsonError","body":{"Unauthorized":{"reason":"InvalidKey"}}}
API_KEY
を正しく設定できていません。 API_KEY を生成 の手順を確認してください。
参考
Discussion