こんにちは、X(クロス)イノベーション本部 ソフトウェアデザインセンター・セキュリティグループの大西です。現在、DockerとTypeScriptを使ってシステムを開発中です。DockerのDistrolessイメージの中で、ORMのPrismaを使おうとするとエラーが出てハマってしまったので、エラー解消の方法についてお話ししたいと思います。 まずは少し、DistrolessイメージとPrismaについて説明します。
Distrolessイメージとは
Googleが公開しているDistrolessイメージとは、アプリケーションの実行に必要な最小限のファイルのみが入っている超軽量なDockerイメージです。それゆえ、普通のOSに入っているようなパッケージマネージャーやシェルなどは入っていません。最も小さいサイズのものでgcr.io/distroless/static-debian11
はたったの2MiBほどしかなく、alpine
(5Mib)の約半分、debian
(124MiB)の2%のサイズしかありません。メリットとしては、不要なファイルを含まないことで攻撃対象領域(Attack Surface)を最小限に抑えており、不要なバグや脆弱性を埋め込みにくいという点が挙げられます。また、イメージが軽いことでリポジトリの容量を抑えられ、起動時の読み込みが軽いのでスケーリングにかかる時間が減るという利点もあります。
Prismaとは
Prismaとは、Node.jsとTypeScriptの環境下で動くオープンソースのORMです。MySQL、PostgreSQL、SQLite、SQL Serverなどに対応しており、NextJS、NestJS、GraphQLなど多くのフレームワークにも対応しています。新しいリリースは約2週間に一度行われ、先日v4.0.0がリリースされました。Prismaでよく使われる機能は以下の2つです。
- Prisma Client: 型安全なデータベースクライアントでアプリに合わせたタイプのPrismaスキーマから自動生成される
- Prisma Migrate: カスタマイズ可能なSQLデータベースマイグレーションを自動的に生成し、移行ファイルを生成せずにデータベースに変更を加えることができる
PrismaとTypeORMの比較
今回、開発を進める中でPrismaとTypeORMどちらも使用してみましたが、Prismaの方が直感的にDBを操作できるなと感じました。書くコードの量もTypeORMよりも少なくてすみ、使いやすい印象です。個人的に便利だと思ったのは、スキーマの自動生成機能です。npx prisma db pull
コマンドを実行するだけで、すでにあるDBからスキーマを読み出しPrisma用のスキーマ(schema.prisma)を自動で作成してくれます。最初はTypeORMでモデルを実装していたので、TypeORMから出力されるデータベースのスキーマがすでに存在していました。そのため、Prismaがスキーマを自動生成する機能によってTypeORMからPrismaに変更するときも割と簡単に移行できました。既存のプロジェクトにPrismaを導入するときの手順はこちらにあります。また、PrismaとTypeORMの比較はこちらです。今回は使用していませんが、TypeORMからのマイグレーションの方法もこちらで紹介されています。
今回ハマったところ
まず背景として、DockerのDistrolessイメージを使いたいという開発者の気持ちがありました。その理由は、やはりセキュアなイメージだからです。それゆえ、AlpineイメージでPrismaは動くけれど、頑張ってDistrolessで動かしたいという思いがありました。今回ハマったところは、Distroless上でPrismaを使ってDB操作を実施しようとするとき、PrismaClientInitializationError: Unable to load Node-API Library from /usr/app/node_modules/.prisma/client/libquery_engine-debian-openssl-1.1.x.so.node, Library may be corrupt
というエラーが出てPrismaを使えなくなったところです。ライブラリが壊れているかもしれない、と言われても・・・。このエラーメッセージで検索しても解決策は見つからず、どこに原因要素があるのか調べるためいろいろ試したところ、alpineイメージではPrismaが動くことが分かりました。
解決策
alpineイメージで動くことは分かったものの、alpineイメージにはあり、Distrolessイメージにはない何かを見つけることができません。そんなとき、同じ部の先輩が、Distrolessコンテナの中に入るためシェルのあるdistroless-debug
イメージに変えることを思いつきました。そして、もう一度GitHubのPrismaのコードをよく見ると、Library may be corrupt
のエラーメッセージが出ている箇所のエラーがサプレスされていることに気づきました。
このe
を見れば何かわかるかもしれないということで、next start
された後のシェルに入ってトランスパイルされたJSを直接書き換えてみましたが、e
の内容は表示されません。つまり、next start
する前にソースコードを書き換えないといけないので、コンテナを作る時に書き換えるためDockerfile上でsedすることを思いつきます。
RUN sed -i -e '41147i console.log(e);' node_modules/@prisma/client/runtime/index.js
するとError: libz.so.1: cannot open shared object file: No such file or directory
というエラーが出ていることが分かりました!libzをDistrolessイメージに入れるため、マルチステージングビルドを使うことにしました。以下のように、libzの入っているalpineイメージからDistrolessイメージにlibzをコピーしてみるとエラーが変わり、Error: libc.musl-x86_64.so.1: cannot open shared object file: No such file or directory
となったのでzlibと同様にlibc.musl-x86_64.so.1というCのライブラリもコピーしました。最終的なDockerfileは以下のようになりました。
# libzの入ったalpineイメージを作成 FROM alpine as lib # 上記のalpineイメージからdistrolessにzlibとmusl clibを提供する FROM gcr.io/distroless/nodejs:16 COPY --from=lib /lib/libz.so.1 /lib/libz.so.1 COPY --from=lib /lib/libc.musl-x86_64.so.1 /lib/libc.musl-x86_64.so.1
まとめ
セキュアで軽量なイメージだからこそ、必要なものは自分で入れていかないといけないDistrolessイメージ。その必要なものがなかなか分からず、今回は時間がかかってしまいました。エラーに遭遇したとき、とりあえずそのエラーを検索することはしますが、それでも方法が見つからないこともあります。そんな時はソースが公開されていればソースを眺めて、ここにはどんな値が入っているのだろう、ここはどんなエラー文が出ているのだろうと、自分でソースを開拓していくことも技の一つだと知りました。先輩に感謝です!
私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。
- セキュリティエンジニア(セキュリティ設計)
執筆:@onishi.mayu、レビュー:@handa.kenta (Shodoで執筆されました)