💻

TypedSQLはとても便利だが、ビルド時にDB接続が必要になって困った話

2024/12/03に公開

はじめに

Prismaを使い始めて久しいですが、集計画面などはどうしても複雑なクエリを投げる必要があり苦渋の思いで$queryRawを使いがちです。

ですが、最近以下の記事を見かけました。
https://zenn.dev/tockn/articles/0e6eac6220e072

こんな便利なものがあるのかと使い始めたのですがビルド時にハマり、その後長いこと使用を諦めていたのですが、この度なんとか解決できたので記事にします。途中試行錯誤しながらのチャレンジでしたが、終わってみればあっさりとした解決方法でした。

前提条件

以下のような作りのプロジェクトがありました。

  • モノレポ構成(ここでは簡単のためにpackages/cdkpackages/serverの2つとする)
    • packages/serverはNextでしたが、本筋にはあまり関係ないです
  • packages/cdkをデプロイすることで、AWS ECSにコンテナとしてpackages/serverをデプロイ
  • そのためpackages/serverはDockerfileでビルドしている
  • CDKではDockerImageAssetを使用

コードのイメージ

まずはDockerfileです。だいぶ端折ってますが、prisma generateをしてNextをビルドして起動しています。
TypedSQLを使いたいのでyarn prisma generate --sqlを実行しています。

packages/server/Dockerfile
FROM public.ecr.aws/docker/library/node:lts-slim

# ...割愛するが色々とapt-get intallして作業ディレクトリを指定

# Prismaまわりの処理
RUN yarn prisma generate
# TypeSqlのgenerate
RUN yarn prisma generate --sql

# アプリケーションをビルド
RUN yarn build

# ポート設定
EXPOSE 3000

# 起動
CMD ["yarn", "start"]

続いてCDKです。先ほどのDockerfileにパスを向けてビルドした後、それをタスク定義に追加しています。

import { DockerImageAsset } from "aws-cdk-lib/aws-ecr-assets";

// ...略

const taskDef; // タスク定義(作成部分は省略)

// コンテナイメージのビルド
const nextjsImage = new DockerImageAsset(this, "NextjsImage", {
  directory: "先ほどのDockerfileへのパス",
  buildArgs: {
    // ビルド時に必要そうな環境変数をあれこれ
  },
});

// コンテナをタスク定義に追加
const container = taskDef.addContainer("NextjsContainer", {
  // 先ほどビルドしたイメージを指定
  image: ContainerImage.fromDockerImageAsset(nextjsImage),
  logging: LogDrivers.awsLogs({ streamPrefix: "Nextjs" }),
  environment: {
    // DB接続はビルド時でなくECSにデプロイした後で行うのでここで設定
    DATABASE_URL: "CDKで作成したRDSの接続情報",
  },
  command: [
    "sh",
    "-c",
    "yarn start",
  ],
});

なにが起きたか

Dockerfileにyarn prisma generate --sqlが加わったことで、CDKデプロイ時に下記のエラーが出るようになります。

error: Environment variable not found: DATABASE_URL.
  -->  prisma/schema.prisma:16
| 
|   provider   = "postgresql"
|   url        = env("DATABASE_URL")

これはschema.prismaの中で指定しているDATABASE_URLが参照できないことで発生します。

補足:DockerImageAssetについて

先ほど使用しているDockerImageAssetですが、CDKのデプロイ(Cfnスタックのデプロイ)より先にコンテナイメージをデプロイします。なので処理順としては

  1. cdk deployを実行したプロセス上でDockerfileに基づきコンテナイメージをビルド
  2. CDKの記述に従ってデプロイ

の順番です。なのでDockerImageAssetbuildArgsに「CDKをデプロイしないと確定しない値(eg. リソースARNなど)を含めることはできない」という痛い制約があります。

エラーの原因

DATABASE_URLもCDKで作成されるRDSの接続情報なので、ビルド時に参照できないのですがTypedSQLを用いない場合は このエラーは発生しません

これはprisma generateは実際のDBではなくSchemaだけを参照するのに対して、prisma generate --sqlはDB接続を必要とするためです。
これは公式にも記述があります。
https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/typedsql#active-database-connection-required

TypedSQL requires an active database connection to function properly.
This means you need to have a running database instance that Prisma can connect to when generating the client with the --sql flag.
If a directUrl is provided in your Prisma configuration, TypedSQL will use that for the connection.

要するにprisma generate --sqlを使用する場合は、そのプロセスから参照できるDBが配置されている必要があります。
今回のケースでいうところのRDSがそれにあたるのですが、大概のRDSはVPC内のプライベートサブネットの中に入ってるのでビルドプロセスからは接続できないと思います。何なら、初めてのCDKデプロイの場合はまだRDSが作成されていないと思います。

対策:ダミーのDBを作って、それを参照してもらう

言い換えれば「prisma generate --sqlを実行する時だけDB接続ができればいい。それはダミーDBでも可」ということになるので、今回はその方針を取りました。

CDKのデプロイをローカルで行っている場合はlocalhostでDBを立ててそれを参照させればOKですし、GitHub Actionsなどを使っている場合はymlの中のservicesを使って一時的にDBを立てればOKです。

それを踏まえて先ほどのDockerfileとCDKは以下のように変わりました。

packages/server/Dockerfile
FROM public.ecr.aws/docker/library/node:lts-slim

# ...割愛するが色々とapt-get intallして作業ディレクトリを指定

# ダミーDB用の接続情報
ARG DATABASE_URL

# Prismaまわりの処理
RUN yarn prisma generate
# TypeSqlを使用時に一時的にDBに接続する必要があるのでダミーDBにマイグレーションをかける
RUN DATABASE_URL=$DATABASE_URL yarn prisma migrate deploy
# TypeSqlのgenerate
RUN DATABASE_URL=$DATABASE_URL yarn prisma generate --sql

# アプリケーションをビルド
RUN yarn build

# ポート設定
EXPOSE 3000

# 起動
CMD ["yarn", "start"]
import { DockerImageAsset, NetworkMode } from "aws-cdk-lib/aws-ecr-assets";

// ...略

// コンテナイメージのビルド
const nextjsImage = new DockerImageAsset(this, "NextjsImage", {
  directory: "先ほどのDockerfileへのパス",
  buildArgs: {
    // 一時的にダミーDBの接続情報を渡す
    DATABASE_URL: process.env.DATABASE_URL!,
  },
  // ビルド時に一時的にダミーDBと導通するため、NWはホストのものを利用する
  networkMode: NetworkMode.HOST,
});

// ...略

DockerfileではRUNの中で環境変数を設定することで一時的にその値を利用するようにしています。
CDKはDockerImageAssetnetworkModeをホストに向けて、ホスト(ビルドプロセス)にあるダミーDBと接続できるようにしています。
あとはcdk deploy時に環境変数に設定 or コマンドライン引数で指定、のどちらかを行えばOKです。

まとめ

Prismaは数あるORMの中でも使いやすく気に入っていますが、複雑なロジックを伴う場合はどうしてもSQLを書く必要があります。
その際にいつも泣く泣く$queryRawを使い、実行結果がanyになってしまうのを受け入れるしかなかったですが、TypedSQLで随分と状況が改善しました。

ただ、そのしわ寄せがビルド・デプロイ時に来てしまい、しかも具体的な対処法がググっても見つからなかったので記事にした次第です。

今回の内容が役立ちましたら幸いです。

Discussion