NestJSとPrismaのイメージをCloudRunにデプロイしたらクラッシュしてコンテナが起動しなくなったので調べたこと
はじめに
NestJS と Prisma のイメージを CloudRun にデプロイしたら、クラッシュして起動しなくなったので、原因について調べたことまとめです。
事象
Segmentation fault になって、NestJS が起動してくれない。(Cloud Run のログ)
環境
Dockerfile
#==================================================
# Build Layer
FROM --platform=linux/amd64 node:18 as build
WORKDIR /app
COPY package.json yarn.lock ./
COPY prisma ./prisma
RUN yarn install --non-interactive --frozen-lockfile
RUN yarn prisma generate
COPY . .
RUN yarn build
#==================================================
# Package install Layer
FROM --platform=linux/amd64 node:18 as node_modules
WORKDIR /app
COPY package.json yarn.lock ./
COPY prisma ./prisma
RUN yarn install --non-interactive --frozen-lockfile --prod
RUN yarn prisma generate
#==================================================
# Run Layer
FROM --platform=linux/amd64 node:18-slim as node
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist /app/dist
COPY --from=build /app/prisma /app/prisma
COPY --from=node_modules /app/package.json /app/yarn.lock ./
COPY --from=node_modules /app/node_modules /app/node_modules
CMD ["/usr/local/bin/yarn", "start:prod"]
まずは、Segmentation fault について調べてみます。
Segmentation fault とは
- プログラムが OS によって設定された基本的な規則に違反したときに発生するエラーです。
- このエラーが起きたとき、OS はプロセスにシグナルを送信し、プロセスはシャットダウンします。
- 基本的には、低レベルの問題(ポインタやメモリ管理など)なので、JavaScript を書くときは気にする必要はありませんが、Node.js で segfault が発生するパターンはいくつかあるようです。
- Nodejs のネイティブのアドオン実装で、そのアドオン自体にバグがあるか、Node のバージョンと互換性がない場合
- Node 内部の状態の操作で、前提の状態が崩れ、Node の組み込みネイティブ コードが間違った動作をして、セグメンテーション違反が発生する場合
- Node.js 自体にバグがある場合
原因
ログには、Segmentation fault となっているものの、なぜ Segmentation fault になっているかの原因がわからず、社内 Slack で困り果てていたところ、同僚に以下の Issue を教えてもらいました。
この Issue に原因とワークアラウンドが書かれてた(大感謝!!!)
直接的な原因は、NodeJS 自体にバンドルされた OpenSSLとシステムで使われてる OpenSSLのバージョンが異なることに起因してるようです。
Node.js 17.0.0 以降、Node.js にバンドルされてる OpenSSL のバージョンは 3 なのですが、システムが使っている OpenSSL のバージョンが 1.1.x の場合、バージョンがずれて本事象が起こるようでした。
ワークアラウンド
回避策は以下の3つがあるようで、どちらかの Openssl のバージョンに合わせる、もしくはクエリエンジンタイプにバイナリエンジンを指定することで回避できるようです。
- Node のバージョンを 16.x に下げ、Node.js にバンドルされてるバージョンが 1.1.x になるようにして、Node とシステムで使われてる OpenSSL のバージョンを合わせる。
- システムで使われてる OpenSSL のバージョンを 3 に上げて、Node とシステムで使われてる OpenSSL のバージョンを合わせる。
- デフォルトの設定では、Prisma クライアントはクエリエンジンを NodeAPI として利用しますが(
engineType = library
)、クエリエンジンを利用する別の方法として、実行可能なバイナリーをサイドカープロセスとして実行するという方法もあり(engineType = binary
)、デフォルトの設定から、engineType = binary に切り替えると、システムで使っている OpenSSL と Node で使ってる OpenSSL のバージョンが異なっていても、この問題を回避できます。
クエリエンジン
クエリエンジンのタイプをバイナリーエンジン(engineType = binary
)に切り替えると、Node.js に含まれてる OpenSSL とシステムで使われてる OpenSSL のバージョンが異なっても Segmentation fault のエラーが回避できるのがなぜか気になったので、クエリエンジンについてのドキュメントをあらためて読んでみた。
クエリエンジンとは
- prisma client から db への connection が呼び出された時に、db とのコネクションを管理する
- prisma client からの query を SQL に変換、DB に送信する
- rust 実装で OS に依存してる
以下、ドキュメント
クエリエンジンのタイプ
クエリエンジンのタイプは以下の2パターンから選択することができます。
Prisma クライアントとクエリ エンジン間の通信オーバーヘッドが削減されるため、Node-API ライブラリ アプローチがおすすめのようです。(デフォルトの設定)
- Prisma Client にロードされる Node-API ライブラリとしてクエリエンジンを利用する方法(デフォルトの設定)
- 独自のプロセスで実行される実行可能バイナリとしてクエリエンジンを利用する方法
generator client {
provider = "prisma-client-js"
engineType = "binary"
}
気になっていたところとして、engineType = binary
の場合は、Node-API ではなく独自のプロセスでクエリエンジンが実行されるので、Node.js が内包してる OpenSSL が使われず、バージョンが異なっていても動くって感じっぽさそうと思ったのですが、詳細はあってるか自信ないのでもう少し調べてみます。
おわりに
エラーを調べてて初めて知ったことが多かったので勉強になった。Prisma のクエリエンジンのタイプや環境に依存があったりなどそのあたりあまり意識せず使っていたので、ドキュメントを改めて読んで色々発見があってよかった。
参考
- Node.js OpenSSL Strategy
Discussion