1
Help us understand the problem. What are the problem?

posted at

updated at

Organization

【公式ハンズオン】Node.js+Prismaを、Serverless Frameworkを使用してAWS Lambdaにデプロイする

この記事は、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.callbackWaitsForEmptyEventLoopfalse に設定する必要があります。

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で空のデータベースを作成しておいて、以下のように進めます。

  1. SQLでデータベースのスキーマを定義する
  2. ローカルで prisma db pull を実行し、データベースのスキーマを schema.prisma に反映させます。
  3. 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 データベースの全てのレコードを削除し、usersprofilespostsのテストデータを挿入します。作成されたユーザが返されます。 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は、以下のような働きをします。

  1. Webpackプラグイン経由で schema.prisma をコピーします。
  2. prisma generate コマンドを実行します。なので、Webpackオプションに個別にコマンドを追加する必要はありません。
  3. Lambdaで使用されるPrismaエンジンを適切にパッケージ化します。これにより、40MB以上を節約できます。
まだWebpackをインストールしていない場合
依存パッケージをインストールします。
npm install -D webpack serverless-webpack webpack-node-externals

TypeScriptを使用している場合は、ts-loaderもインストールします。

npm install -D ts-loader

次に、プロジェクトのルートに webpack.config.js ファイルを作成し、以下の設定をコピペします。

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 内に挿入する必要があります。

webpack.config.js
// ...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を追加してください。

serverless.yml
plugins:
  - serverless-webpack
  - serverless-webpack-prisma

custom:
  webpack:
    includeModules: true

serverless-webpack-prisma は自動的に prisma generate を実行するので、以下のようにWebpackスクリプトを別途使用する必要はありません。

serverless.yml
custom:
  webpack:
    packagerOptions:
      scripts:
        - prisma generate

serverless-webpack-prisma は、LambdaでPrismaエンジンが使用しない依存関係とパッケージを削除します。そのために、serverless.yml ファイル内の package.patterns からそれらを削除します。

serverless.yml
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

Deploying to AWS Lambda

ライセンス:Apache License 2.0

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
1
Help us understand the problem. What are the problem?