理系学生日記

おまえはいつまで学生気分なのか

Lambda関数でPrismaを利用するときのTips

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

Concepts / Database connectors / PostgreSQL

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

Managing an RDS Proxy

Prismaは全てのクエリでPrepared Statementを使うので、事実上RDS Proxyを利用するメリットはなさそうです。 このため、Prismaを使うLambda関数は、RDSに対して直接接続するのが定石になるのでしょう。

感想

そんなこんなで色々とハマりどころはあるのですが、一度使えるようになると、補完を効かせながらクエリを書けるというのはなかなか良い体験です。 いつもは「このテーブルのカラム名なんだっけ」とER図を眺めながら試行錯誤するのですが、それが曖昧な記憶のままでも補完していけるというのは生産性に強く寄与する気がします。