Lambda関数からRDSを利用する必要が生じ、次世代のO/Rマッパと称されるPrismaを利用することにしました。
Lambda関数自体は、Serverless Frameworkで管理しています。
本エントリでは、Serverless Framework + Prisma + Lambdaの構成におけるノウハウを記述していきます。
Prismaのアーキテクチャ
まず前提となるのが、Prismaのアーキテクチャです。 Prismaは以下の3つの要素で構成されますが、今回はPrisma Clientのみを対象とします。
- Prisma Client: 型安全性が担保される形で自動生成されるクエリビルダー
- Prisma Migrate: データベースマイグレーションシステム
- Prisma Studio: DBを参照・編集できるGUI
Prisma Engine
実のところ、Prisma Clientは、DBに対してSQLを実行するようなコアな機能は持っていません。 この機能を有するのは、Prisma Engineと呼ばれるRust製のプロダクトです。
具体的なシーケンスはPrisma enginesに記載されているので、ここに引用します。
この図のように、実際のDBとの通信はPrisma ClientではなくPrisma Engineが行うことになります。
Prisma Schema
また、Prisma ClientはDB上にどのようなテーブルやカラムがあるのか、といった情報をPrisma Schemaと呼ばれるファイルで管理します。一般的には、schema.prisma
というファイル名になります。
このファイル自体は、手書きするか、prisma pull
コマンドによってDB上のテーブルデータから自動生成されます。
実はこのファイルも実行時に必要です。実行時に存在しない場合はPrisma Clientがエラー終了します。
Prismaのデプロイ
前述したアーキテクチャからわかるように、Lambda関数にPrisma ClientをデプロイするだけだとPrismaは動作しません。Lambda関数のリソースとして、以下の2つのデプロイが必要です。
- Prisma Engine
- Prisma Schema
Prisma Engine
Prisma Engineは実行ファイルなので、OS毎に異なります。Lambda関数上で動作させるのに必要なのはlibquery_engine-rhel-openssl-*.so.node
です。
schema.prisma
ファイルに以下のように記述してprisma generate
コマンドを実行することでnode_modules/.prisma/client/
配下にダウンロードされます。
generator client { provider = "prisma-client-js" binaryTargets = ["native", "rhel-openssl-1.0.x"] }
あとは、当該のEngineをLambda関数にバンドルすれば良いでしょう。
Serverless Frameworkでのバンドル
ぼくはServerless Frameworkのバンドラーとしてesbuildを利用しています。
plugins: - serverless-esbuild
esbuildを利用するときにPrisma EngineとPrisma Schemaをバンドルする設定はこちらになります。
package: patterns: # Prisma を動作させるには、Schema ファイルが必要なので同梱する - "node_modules/.prisma/client/schema.prisma" # 実行 Engine として、rhel 用のものを同梱する必要がある - "!node_modules/.prisma/client/libquery_engine-*" - "node_modules/.prisma/client/libquery_engine-rhel-*" - "!node_modules/prisma/libquery_engine-*" - "!node_modules/@prisma/engines/**"
実際にこれらがバンドルされていることは以下のようにして確認できます。
$ npx sls package $ unzip -l .serverless/graphql.zip | grep -e engine -e schema 44517032 01-01-1980 00:00 node_modules/.prisma/client/libquery_engine-rhel-openssl-1.0.x.so.node 1222 01-01-1980 00:00 node_modules/.prisma/client/schema.prisma
あとはServerless Frameworkを使ってLambda関数をデプロイすれば良いでしょう。
$ npx sls deploy function --function yourFunction
データベースアクセス
接続
Prisma Clientは、schema.prisma
ファイルに指定したdatasource
設定を読んでDBに接続します。
datasource postgresql { provider = "postgresql" url = env("DATABASE_URL") }
一方で、実行タイミングで接続先を上書き可能です。ぼくはこちらの方法を使っています。
export const getPrismaClient = (logger: LambdaLog) => { const prismaClient = new PrismaClient({ datasources: { postgresql: { url: process.env.DATABASE_URL, }, }, log: [ { emit: "event", level: "query" }, { emit: "stdout", level: "info" }, { emit: "stdout", level: "warn" }, { emit: "stdout", level: "error" }, ], }); prismaClient.$on("query", (e) => { logger.info(`duration: ${e.duration} ms, query: ${e.query}`); }); return prismaClient; };
コネクションプール
Prisma Clientは、デフォルトでコネクションプールを構築します。一方で、Lambda関数はトラフィックが多ければ一度に多数、並行して起動します。このときに1関数がたくさんコネクションを作ってしまうと、あっという間にデータベースリソースが枯渇してしまうでしょう。
これを避けるには、Lambda関数でコネクションプールを張らない(事実上、プールサイズを1にする)ことが必要になります。
例えばPostgresqlに接続する際、Connection URLのconnection_limit
パラメータで、コネクションプールのサイズを指定できます。
postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=myschema&connection_limit=5&socket_timeout=3
Lambda関数からPrismaを利用する場合は、このconnection_limit
を1にすることが推奨されます。
RDS Proxy
RDSとコネクションプールというと、Amazon RDS Proxyがあります。
しかし、Prismaの公式ドキュメントでは、このRDS Proxyをコネクションプールとして利用するときのメリットは無いと明言しています。
Prisma is compatible with AWS RDS Proxy. However, there is no benefit in using it for connection pooling with Prisma due to the way RDS Proxy pins connections:
Guides / Deployment / Deployment guides / Caveats when deploying to AWS platforms
この理由はいわゆる「コネクションのピン留め」(connection pinning)です。 例えばPostgresqlを使うとき、RDS Proxyがコネクションをピン留めする条件の1つに「Prepared Statementを使うこと」があります。
Conditions that cause pinning for PostgreSQL (略)
- Using prepared statements, setting parameters, or resetting a parameter to its default
Prismaは全てのクエリでPrepared Statementを使うので、事実上RDS Proxyを利用するメリットはなさそうです。 このため、Prismaを使うLambda関数は、RDSに対して直接接続するのが定石になるのでしょう。
感想
そんなこんなで色々とハマりどころはあるのですが、一度使えるようになると、補完を効かせながらクエリを書けるというのはなかなか良い体験です。 いつもは「このテーブルのカラム名なんだっけ」とER図を眺めながら試行錯誤するのですが、それが曖昧な記憶のままでも補完していけるというのは生産性に強く寄与する気がします。