空雲 Blog

Eye catchPrisma Accelerate の機能を Deno に載せて、Cloudflare WorkersからPrismaでDBへアクセスする

publication: 2023/12/30
update:2024/02/21

Prisma の Edge Runtime での問題点

Prisma は Node.js で記述された API 部分と、DB とやり取りする Rust で記述された Engine 部分に分かれています。Rust の部分は各環境用にネイティブでコンパイルされており、Cloudflare Workers のような Edge 環境で動作させることは出来ません。回避手段としては Prisma Accelerate というサービスを利用して、Rust の Engine 部分だけリモートで実行するという方法があります。しかし、Prisma Accelerate は無料で利用できる範囲が 6 万クエリ/月ということで心もとない数字です。

しかし現在、Prisma は Rust のコードを WebAssembly で出力する作業が進んでいます。WebAssembly を使えば EdgeRuntime でも動作可能となります。ただし問題点があって、現時点でのサイズは圧縮しても 1MB を大幅に超えてしまい、CloudflareWorkers のフリープランでの制限を超えてしまいます。実装状況を見ると、今後も 1MB を切るようにするのはかなり困難ではないかと思われます。

無料で Prisma を EdgeRuntime で動かす方法

Cloudflare Workers のフリープランではサイズの問題から Prisma の WebAssembly を動かすことは出来ません。しかし Deno Deploy ならば動作可能です。Deno Deploy は無料で 1MB 超えの WebAssembly を動かすことが出来ます。ただ、Deno と聞くと、Node.js との互換性の問題から拒否反応を示す人も多いと思います。そのため、Prisma のエンジン部分のみを Deno Deploy に載せて、その他の部分は Cloudflare Workers からアクセスすることで、問題を解決出来ます。Deno Deploy は 100 万アクセス/月まで無料で利用できます。

構成

以下のような構成で動作するシステムを作ります。

{"width":"941px","height":"491px"}

DenoDeploy に載せた Prisma Accelerate に Secret を設定しておきます。そして Secret から DB 接続文字列を JWT の APIKey を作り、CloudflareWorkers から DenoDeploy にアクセスします。これによって DenoDeploy 側は一つサービスを立ち上げてほけ場、複数の DB や複数のクライアントに対して汎用的に動作します。

Deno Deploy に Prisma Accelerate を載せる

prisma-accelerate-local を使って、Deno Deploy に Prisma Accelerate のエミュレートをするコードです。これは PostgraduateSQL 用のコードです。環境変数の SECRET に何らかの文字列を設定しておく必要があります。後ほど APIKey を作る際に使います。

1import { PrismaPg } from "npm:@prisma/adapter-pg@5.8.0-dev.42";
2import { getPrismaClient } from "npm:@prisma/client@5.8.0-dev.42/runtime/library.js";
3import pg from "npm:pg";
4import {
5 PrismaAccelerate,
6 ResultError,
7} from "npm:prisma-accelerate-local@0.2.0/lib";
8
9const queryEngineWasmFileBytes = fetch(
10 new URL(
11 "../../node_modules/@prisma/client/runtime/query-engine.wasm",
12 import.meta.url
13 )
14).then((r) => r.arrayBuffer());
15
16const getAdapter = (datasourceUrl: string) => {
17 const url = new URL(datasourceUrl);
18 const schema = url.searchParams.get("schema");
19 const pool = new pg.Pool({
20 connectionString: url.toString(),
21 });
22 return new PrismaPg(pool, {
23 schema: schema ?? undefined,
24 });
25};
26
27export const createServer = ({
28 secret,
29}: {
30 https?: { cert: string; key: string };
31 secret: string;
32}) => {
33 const prismaAccelerate = new PrismaAccelerate({
34 secret,
35 adapter: (datasourceUrl) => getAdapter(datasourceUrl),
36 getQueryEngineWasmModule: async () => {
37 const result = new WebAssembly.Module(await queryEngineWasmFileBytes);
38 return result;
39 },
40 getPrismaClient,
41 });
42
43 return Deno.serve(async (request) => {
44 const url = new URL(request.url);
45 const paths = url.pathname.split("/");
46 const [_, version, hash, command] = paths;
47 const headers = Object.fromEntries(request.headers.entries());
48 const createResponse = (result: Promise<unknown>) =>
49 result
50 .then((r) => {
51 return new Response(JSON.stringify(r), {
52 headers: { "content-type": "application/json" },
53 });
54 })
55 .catch((e) => {
56 if (e instanceof ResultError) {
57 return new Response(JSON.stringify(e.value), {
58 status: e.code,
59 headers: { "content-type": "application/json" },
60 });
61 }
62 return new Response(JSON.stringify(e), {
63 status: 500,
64 headers: { "content-type": "application/json" },
65 });
66 });
67
68 if (request.method === "POST") {
69 const body = await request.text();
70 switch (command) {
71 case "graphql":
72 return createResponse(
73 prismaAccelerate.query({ body, hash, headers })
74 );
75 case "transaction":
76 return createResponse(
77 prismaAccelerate.startTransaction({
78 body,
79 hash,
80 headers,
81 version,
82 })
83 );
84 case "itx": {
85 const id = paths[4];
86 switch (paths[5]) {
87 case "commit":
88 return createResponse(
89 prismaAccelerate.commitTransaction({
90 id,
91 hash,
92 headers,
93 })
94 );
95 case "rollback":
96 return createResponse(
97 prismaAccelerate.rollbackTransaction({
98 id,
99 hash,
100 headers,
101 })
102 );
103 }
104 }
105 }
106 } else if (request.method === "PUT") {
107 const body = await request.text();
108 switch (command) {
109 case "schema":
110 return createResponse(
111 prismaAccelerate.updateSchema({
112 body,
113 hash,
114 headers,
115 })
116 );
117 }
118 }
119 return new Response("Not Found", { status: 404 });
120 });
121};
122
123createServer({
124 secret: Deno.env.get("SECRET")!,
125});

Cloudflare Workers から Deno Deploy にアクセスする

コマンドラインから以下のコマンドを実行して、APIKey を作成します。APIKey は JWT で、Deno Deploy にアクセスする際に使います。

1npx prisma-accelerate-local -s secret -m postgresql://xxxx:yyyy@zzzz:5432/postgres

すると以下のような出力が得られます。

1eyJhbGciOiJIUzI1NiJ9.eyJkYXRhc291cmNlVXJsIjoicG9zdGdyZXNxbDovL3h4eHg6eXl5eUB6enp6OjU0MzIvcG9zdGdyZXMiLCJpYXQiOjE3MDM5MDM1ODEsImlzcyI6InByaXNtYS1hY2NlbGVyYXRlIn0.5tera4SWWm8roDdpMOJBGlXjJwVjiFd3Mg6wZG0wSt4

この時点で JWT の中に DB 接続文字列が含まれています。本家の Prisma Accelerate と違い、サーバー側で DB 接続文字列を保持する必要はありません。
次は Prisma に対して、DATABASE_URL を指定します。指定内容は以下の通りです。

1DATABASE_URL="prisma://[Deno Deployのサービスのアドレス]/?api_key=[API_KEY]"

そして Cloudflare Workers 向けのコードを書く場合は、以下のように PrismaClient を import します。

1import { PrismaClient } from "@prisma/client/edge";

まとめ

Prisma Accelerate のエンジン部分を Deno Deploy に載せて、Cloudflare Workers からアクセスすることで、無料で Prisma を EdgeRuntime で動かすことが出来ました。Deno Deploy は 100 万アクセス/月まで無料で利用できます。ただし、Prisma の WebAssembly の機能は開発中です。そのため、個人の趣味で使う範囲に留めておくことをおすすめします。

Prisma Accelerate のエミュレーションを使わなくても、毛嫌いせず Deno Deploy にバックエンドの処理を全部載せた方が実は良いのではないかという気がしないでもないです。