この記事は、Prisma公式の「Deploying to AWS Lambda」の和訳(意訳)です。
翻訳元とライセンスについてはページ下部に記載しています。
このハンズオンでは、Serverless Frameworkを使用して、AWS LambdaにサーバレスなNode.jsのREST APIをデプロイする方法をお伝えします。
AWS LambdaはAWSのサービスの一つで、これを使うとサーバ管理不要のサーバレス環境でコードを実行できます。一方で、REST APIをLambdaにデプロイするためには、S3でファイルをホスティングしたり、API GatewayでAPIをHTTPで公開する必要もあります。
Serverless Frameworkを使えば、CLIでワークフローを自動化し、AWSリソースのプロビジョニングも行うことができます。
今回作成するREST APIでは、Prisma Clientを使用してデータベースのレコードの取得・作成・削除を行います。具体的には、それぞれの関数はRESTリソースのエンドポイントを表し、Prisma Clientを用いて(Herokuなどにホスティングされている)PostgreSQLデータベースを操作します。
このハンズオンの目的は、PrismaベースのAPIをAWS Lambdaにデプロイする方法をご紹介することです。いくつかのRESTエンドポイントがサーバレス関数として実装済みのサンプルをダウンロードすることから始めましょう。
きちんと実装できているかの確認は途中のチェックポイントで何度か行いますので、ご安心ください。
GraphQLサーバをLambdaにデプロイする際の注意点
このハンズオンでご紹介するサンプルでは、RESTを使用します。GraphQLサーバを使用する場合でも概ねは同じですが、GraphQL APIでは関数を一つしか必要としない点が異なります。
GraphQLを使用した場合の関数では、下に示すコードのように、Lambdaのコンテキストオブジェクトの context.callbackWaitsForEmptyEventLoop
を false
に設定する必要があります。
exports.server = (event, context, cb) => {
// falseに設定することで、Node.jsのイベントループが空になるのを待たず、コールバック関数が実行されると即座にレスポンスを返します。
context.callbackWaitsForEmptyEventLoop = false
return lambda.graphqlHandler(event, context, cb)
}
イベントループについて
JavaScript イベントループの仕組みをGIFアニメで分かりやすく解説
lambda関数のコールバック関数について
Node.js の AWS Lambda 関数ハンドラー
3 番目の引数 callback は、レスポンスを送信するために non-async ハンドラーで呼び出すことができる関数です。...
呼び出すと、Lambda はイベントループが空になるのを待ってから呼び出し元にレスポンスまたはエラーを返します。
前提
- PostgreSQLデータベースをホスティングし、URLでアクセスできること。例)
postgresql://username:password@your_postgres_db.cloud.com/db_identifier
- AWSアカウントと、それにプログラムからアクセスするためのアクセスキーがあること
- Serverless framework CLIがインストールされていること
- Node.jsがインストールされていること
- PostgreSQL CLI
psql
がインストールされていること
Prismaのワークフロー
Prismaは、既存のデータベースの使用にも、新しいデータベースの新規作成にも対応しています。いずれの場合でも、Prismaは schema.prisma
ファイルのスキーマに依存します。
今回は、素のSQLで空のデータベースを作成しておいて、以下のように進めます。
- SQLでデータベースのスキーマを定義する
- ローカルで
prisma db pull
を実行し、データベースのスキーマをschema.prisma
に反映させます。 -
prisma generate
を実行し、Prismaスキーマを元にPrisma Clientを生成します。
1. サンプルのダウンロード
ターミナルを開いて、適当なディレクトリに移動してください。アプリケーションを格納するディレクトリを作成し、サンプルコードをダウンロードします。
mkdir prisma-aws-lambda
cd prisma-aws-lambda
curl https://codeload.github.com/prisma/prisma-examples/tar.gz/latest | tar -xz --strip=3 prisma-examples-latest/deployment-platforms/aws-lambda/
ダウンロードが終わったら、依存パッケージをダウンロードします。
npm install
チェックポイント1
ls -l
を実行して、下のような出力になることを確認してください。
ls -1
README.md
aws-credentials
handlers
node_modules
package-lock.json
package.json
prisma
schema.sql
serverless.yml
2. ローカルに環境変数 DATABASE_URL をセットする
ローカル環境に環境変数 DATABASE_URL
をセットすることで、作成したデータベーススキーマにPrismaがアクセスできるようにします。
export DATABASE_URL="postgresql://__USER__:__PASSWORD__@__HOST__/__DATABASE__"
この先のステップでも、環境変数 DATABASE_URL
は必要になります。このプロジェクト内でターミナルを開くときは、常にこの環境変数がセットされている状態にしておいてください。
大文字で仮置きしている部分は、あなたのデータベースの情報で置き換えてください。
例)
postgresql://janedoe:randompassword@yourpostgres.compute-1.amazonaws.com:5432/yourdbname
3. DATABASE_URL を .env にセットする
Lambda関数がデータベースにアクセスするには、環境変数DATABASE_URL
が必要です。
.env
ファイルを定義して、serverless-dotenv-plugin
が関数のランタイムに環境変数を注入できるようにしましょう。
ダウンロードしたリポジトリには .env.example
というサンプルファイルが含まれているので、これをコピーしましょう。
cp .env.example .env
それから、.env
ファイルを開いて、STEP 2 で作成した文字列を DATABASE_URL
にセットします。
Git管理している場合、DATABASE_URLのような情報はGitリポジトリに含めないのがベストプラクティスです。そうするためには、.gitignore
に .env
ファイルを除外する行を追加するのが一般的です。このハンズオンでは、リポジトリの作成は行わずにコピーしただけなので、リポジトリを初期化でもしない限り、気にする必要はありません。
4. データベーススキーマの作成
サンプルコードにある schema.sql
を実行して、データベーススキーマを作成します。
psql $DATABASE_URL -f schema.sql
チェックポイント2
psql $DATABASE_URL -c "\dt"
を実行して、下のようなテーブルが出力されることを確認してください。
List of relations
Schema | Name | Type | Owner
--------+---------+-------+-----------
public | Post | table | janedoe
public | Profile | table | janedoe
public | User | table | janedoe
おめでとうございます!
これで、データベースのスキーマを作成することができました。
5. データベース構造を取得する
Prisma CLIを使用して、データベースの構造を取得します。
npx prisma db pull
Prismaスキーマの datasource
ブロックに定義したデータベースが取得され、データベースのテーブルに対応したmodelにスキーマが挿入されます。
チェックポイント3
prisma/schema.prisma
が以下のようになっていることを確認してください。(modelのフィールドは見やすいように並び替えているので、順番が異なっていても大丈夫です。)
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
title String
content String?
published Boolean @default(false)
User User @relation(fields: [authorId], references: [id]) // relation field
authorId Int // relation scalar field
}
model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int @unique // relation scalar field
User User @relation(fields: [userId], references: [id]) // relation field
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
Post Post[] // relation field
Profile Profile? // relation field
}
あとは、prisma generate
を実行するだけでPrisma Clientを生成できます。Prismaスキーマを変更するたびに、prisma generate
を実行してPrisma Clientに反映させる必要があることにご注意ください。
関連するフィールドをリネームして簡単にアクセスできるようにする
User
モデル内に生成された Post
フィールドと Profile
フィールドは仮想的なもので、データベース内の外部キーに紐づいていないため、手動で書き換えても問題ありません。書き換えたとしてもPrisma Clientにしか影響しないので、リレーション名により意味を持たせることができます。
Prismaスキーマを見ると、2種類のリレーションフィールドがあることが分かります。
- リレーションフィールド : Model名を型として利用します。
Post
モデルのUser
フィールドがこれに当たります。名前をUser
からauther
に変更すると良さそうです。 - リレーションスカラーフィールド : 外部キーを保持します。
Post
モデルのauthorId
フィールドがこれに当たります。データベースのフィールド名と一致している必要があるので、この名前は変更できません。
Client内のリレーションフィールド名は、リレーションそのものにアクセスするのに使われます。例えば、特定の Post
とそれに紐づいた User
オブジェクトを取得するには、以下のようにします。
const postAuthor = await prisma.post.findUnique({
where: { id: 1 },
include: { User: true },
})
Post
モデルの User
フィールドを書き換えたとすると、次のようにアクセスできるようになります。
const postAuthor = await prisma.post.findUnique({
where: { id: 1 },
include: { author: true },
})
この命名規則に従いつつ、リレーションフィールドの名前を変更してみましょう。
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id]) // renamed from `User` -> `author`
authorId Int // relation scalar field
}
model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int @unique // relation scalar field
user User @relation(fields: [userId], references: [id]) // renamed from `User` -> `user`
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[] // renamed from `Post` -> `posts`
profile Profile? // renamed from `Profile` -> `profile`
}
6. AWSアクセスキーを環境変数にセットする
AWSリソースの作成とアプリケーションのデプロイをServerless Frameworkが行えるようにするためには、アクセスキーを設定する必要があります。アクセスキーを取得するには、個人のアカウントを使用して作成する方法と、Serverless Framework用のIAMユーザを作成する方法の2通りがあります。後者の方法の方がきめ細かくパーミッションを設定できるため、推奨されています。アクセスキーを取得する詳細な方法については、AWSのガイドなどをご参照ください。
アクセスキーIDとシークレットアクセスキーを取得したら、次のコマンドでセットしましょう。
serverless config credentials --provider aws --key AWS_ACCESS_KEY_ID --secret AWS_SECRET_ACCESS_KEY
7. アプリをデプロイする
デプロイする準備はもう整っています。次のコマンドでデプロイしましょう。
serverless deploy
ServerlessはAWSリソースの作成とコードのアップロードを行い、以下のようにサービスについての情報を出力します。
Service Information
service: prisma-aws-lambda-example
stage: dev
region: us-east-1
stack: prisma-aws-lambda-example-dev
resources: 39
api keys:
None
endpoints:
GET - https://UNIQUE_IDENTIFIER.execute-api.us-east-1.amazonaws.com/dev/
GET - https://UNIQUE_IDENTIFIER.execute-api.us-east-1.amazonaws.com/dev/seed
GET - https://UNIQUE_IDENTIFIER.execute-api.us-east-1.amazonaws.com/dev/users
POST - https://UNIQUE_IDENTIFIER.execute-api.us-east-1.amazonaws.com/dev/users
GET - https://UNIQUE_IDENTIFIER.execute-api.us-east-1.amazonaws.com/dev/posts
functions:
status: prisma-aws-lambda-example-dev-status
seed: prisma-aws-lambda-example-dev-seed
getUsers: prisma-aws-lambda-example-dev-getUsers
createUser: prisma-aws-lambda-example-dev-createUser
getPosts: prisma-aws-lambda-example-dev-getPosts
layers:
None
チェックポイント4
statusエンドポイントを呼び出してみましょう。
curl https://UNIQUE_IDENTIFIER.execute-api.us-east-1.amazonaws.com/dev/
{"up":true}
が返されることを確認してください。
デプロイしたREST APIをテストする
APIのベースURLである https://UNIQUE_IDENTIFIER.execute-api.us-east-1.amazonaws.com/dev/
を使って、APIのエンドポイントをテストすることができます。
エンドポイント | 概要 | 実行 |
---|---|---|
GET / |
ステータス | handlers/status.js |
GET /seed |
データベースの全てのレコードを削除し、users ・profiles ・posts のテストデータを挿入します。作成されたユーザが返されます。 |
handlers/seed.js |
GET /users |
データベース内の全てのusers と、それに紐づくprofiles を取得します。 |
handlers/users.js |
POST /users |
データベースにusers を一つ作成します。 |
handlers/create-user.js |
GET /posts |
データベース内の全てのposts と、それに紐づくauthors を取得します。 |
handlers/posts.js |
APIを呼び出すには、curlを利用します。
curl -v https://UNIQUE_IDENTIFIER.execute-api.us-east-1.amazonaws.com/dev/seed
補足
serverless.ymlについて
serverless.yml
という設定ファイルには、エンドポイントと関数の設定が含まれています。このファイルを更新すれば、エンドポイントを追加したり、設定を変更することができます。より詳細なAWSの設定項目は、Serverless FrameworkのドキュメントのAWSプロバイダの設定についての項目をご参照ください。
schema.prisma内のbinaryTargetsについて
Prismaスキーマのgeneratorブロックには、次の一文が含まれていると思います。
binaryTargets = ["native", "rhel-openssl-1.0.x"]
この設定が必要な理由は、ローカルのランタイム環境がLambdaのランタイム環境と異なるためです。binaryTarget
を設定すると、互換性のあるPrismaエンジンのバイナリが使用されるようになります。
serverless.yml内のパッケージパターンについて
Serverlessの設定ファイルでは、全てのPrismaエンジンのバイナリが除外されるようにパッケージパターンが設定されています。しかし、その内の一つはLambdaのランタイムで必要なものなので、アプリをアップロードするためにServerlessがパッケージ化するとき、1つのバイナリのみが含まれるようにしています。
package:
patterns:
- '!node_modules/.prisma/client/libquery_engine-*'
- 'node_modules/.prisma/client/libquery_engine-rhel-*'
- '!node_modules/prisma/libquery_engine-*'
- '!node_modules/@prisma/engines/**'
これにより、パッケージ化されたアーカイブを小さく保つが可能になります。
ただし、serverless-webpack
を使用する場合は、この方法を利用できないことにご注意ください。
デプロイにserverless-webpackを利用する
serverless-webpack
を使用する場合、特に設定を行わなくても、serverless-webpack-prismaというプラグインが利用可能です。serverless-webpack-prisma
は、以下のような働きをします。
- Webpackプラグイン経由で
schema.prisma
をコピーします。 -
prisma generate
コマンドを実行します。なので、Webpackオプションに個別にコマンドを追加する必要はありません。 - Lambdaで使用されるPrismaエンジンを適切にパッケージ化します。これにより、40MB以上を節約できます。
まだWebpackをインストールしていない場合
npm install -D webpack serverless-webpack webpack-node-externals
TypeScriptを使用している場合は、ts-loader
もインストールします。
npm install -D ts-loader
次に、プロジェクトのルートに webpack.config.js
ファイルを作成し、以下の設定をコピペします。
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path')
const nodeExternals = require('webpack-node-externals')
const slsw = require('serverless-webpack')
const { isLocal } = slsw.lib.webpack
module.exports = {
target: 'node',
stats: 'normal',
entry: slsw.lib.entries,
externals: [nodeExternals()],
mode: isLocal ? 'development' : 'production',
optimization: { concatenateModules: false },
resolve: { extensions: ['.js'] },
output: {
libraryTarget: 'commonjs',
filename: '[name].js',
path: path.resolve(__dirname, '.webpack'),
},
}
TypeScriptの場合には、以下の設定を exports
内に挿入する必要があります。
// ...initial config
module.exports = {
// ...initial config
resolve: { extensions: ['.js', '.ts'] },
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
},
],
},
// ...initial config
}
その他の設定項目については、Serverless Webpackについてのドキュメントをご参照ください。
次のコマンドで、開発環境での依存パッケージもインストールしてください。
npm install -D serverless-webpack-prisma serverless
serverless.yml
のプラグインリストにserverless-webpack
を追加してください。
plugins:
- serverless-webpack
- serverless-webpack-prisma
custom:
webpack:
includeModules: true
serverless-webpack-prisma
は自動的に prisma generate
を実行するので、以下のようにWebpackスクリプトを別途使用する必要はありません。
custom:
webpack:
packagerOptions:
scripts:
- prisma generate
serverless-webpack-prisma
は、LambdaでPrismaエンジンが使用しない依存関係とパッケージを削除します。そのために、serverless.yml
ファイル内の package.patterns
からそれらを削除します。
package:
patterns:
- '!node_modules/.prisma/client/libquery_engine-*'
- 'node_modules/.prisma/client/libquery_engine-rhel-*'
- '!node_modules/prisma/libquery_engine-*'
- '!node_modules/@prisma/engines/**'
serverless.yml
ファイルを変更したら、serverless deploy
でデプロイしてください。デプロイ結果の出力をみると、適切なPrismaエンジンをパッケージ化して配布されたことが分かります。
Serverless: Copy prisma schema for app...
Serverless: Generate prisma client for app...
Serverless: Remove unused prisma engine:
Serverless: - node_modules/.prisma/client/libquery_engine-darwin.dylib.node
Serverless: - node_modules/@prisma/engines/introspection-engine-darwin
Serverless: - node_modules/@prisma/engines/libquery_engine-darwin.dylib.node
Serverless: - node_modules/@prisma/engines/migration-engine-darwin
Serverless: - node_modules/@prisma/engines/prisma-fmt-darwin
Serverless: Copying existing artifacts...
まとめ
お疲れ様でした!APIをAWS Lambdaにデプロイすることができましたね。
Prisma ClientのAPIについてもっと知りたい場合は、handlers/
フォルダ内の関数を見てみてください。
一般的に、データベース連携をFaaS(function as a service)で行う場合、DB接続をプールしておいた方がパフォーマンス上有利です。その理由としては、関数が実行されるたびにデータベースへの接続が発生する可能性があるからです。この問題は、実行が継続されるNode.jsサーバでは発生しません。より詳しい解決策については、サーバレス環境への接続についてのガイドをご参照ください。
翻訳元
翻訳日: 2022/05/21
ライセンス:Apache License 2.0