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

:::note
この記事は、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エンドポイントがサーバレス関数として実装済みのサンプルをダウンロードすることから始めましょう。

:::note
きちんと実装できているかの確認は途中のチェックポイントで何度か行いますので、ご安心ください。
:::

## GraphQLサーバをLambdaにデプロイする際の注意点

このハンズオンでご紹介するサンプルでは、RESTを使用します。GraphQLサーバを使用する場合でも概ねは同じですが、GraphQL APIでは関数を一つしか必要としない点が異なります。

GraphQLを使用した場合の関数では、下に示すコードのように、Lambdaのコンテキストオブジェクトの `context.callbackWaitsForEmptyEventLoop` を `false` に設定する必要があります。

```ts
exports.server = (event, context, cb) => {
  // falseに設定することで、Node.jsのイベントループが空になるのを待たず、コールバック関数が実行されると即座にレスポンスを返します。
  context.callbackWaitsForEmptyEventLoop = false

  return lambda.graphqlHandler(event, context, cb)
}
```

:::note
イベントループについて
[JavaScript イベントループの仕組みをGIFアニメで分かりやすく解説](https://coliss.com/articles/build-websites/operation/javascript/javascript-visualized-event-loop.html)

lambda関数のコールバック関数について
[Node.js の AWS Lambda 関数ハンドラー](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-handler.html)

> 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でデータベースのスキーマを定義する
1. ローカルで `prisma db pull` を実行し、データベースのスキーマを `schema.prisma` に反映させます。
1. `prisma generate` を実行し、Prismaスキーマを元にPrisma Clientを生成します。



## 1. サンプルのダウンロード

ターミナルを開いて、適当なディレクトリに移動してください。アプリケーションを格納するディレクトリを作成し、サンプルコードをダウンロードします。

```sh
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/
```

ダウンロードが終わったら、依存パッケージをダウンロードします。

```sh
npm install
```



### チェックポイント１

`ls -l` を実行して、下のような出力になることを確認してください。

```sh
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がアクセスできるようにします。

```sh
export DATABASE_URL="postgresql://__USER__:__PASSWORD__@__HOST__/__DATABASE__"
```

:::note warn
この先のステップでも、環境変数 `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` というサンプルファイルが含まれているので、これをコピーしましょう。

```sh
cp .env.example .env
```

それから、`.env` ファイルを開いて、STEP 2 で作成した文字列を `DATABASE_URL` にセットします。

:::note
Git管理している場合、DATABASE_URLのような情報はGitリポジトリに含めないのがベストプラクティスです。そうするためには、`.gitignore` に `.env` ファイルを除外する行を追加するのが一般的です。このハンズオンでは、リポジトリの作成は行わずにコピーしただけなので、リポジトリを初期化でもしない限り、気にする必要はありません。
:::

## 4. データベーススキーマの作成

サンプルコードにある `schema.sql` を実行して、データベーススキーマを作成します。

```sh
psql $DATABASE_URL -f schema.sql
```

### チェックポイント２

`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を使用して、データベースの構造を取得します。

```sh
npx prisma db pull
```

Prismaスキーマの `datasource` ブロックに定義したデータベースが取得され、データベースのテーブルに対応したmodelにスキーマが挿入されます。


### チェックポイント３

`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スキーマを見ると、２種類のリレーションフィールドがあることが分かります。

- リレーションフィールド : Model名を型として利用します。`Post` モデルの `User` フィールドがこれに当たります。名前を `User` から `auther` に変更すると良さそうです。
- リレーションスカラーフィールド : 外部キーを保持します。`Post` モデルの `authorId` フィールドがこれに当たります。データベースのフィールド名と一致している必要があるので、この名前は変更できません。

Client内のリレーションフィールド名は、リレーションそのものにアクセスするのに使われます。例えば、特定の `Post` とそれに紐づいた `User` オブジェクトを取得するには、以下のようにします。

```ts
const postAuthor = await prisma.post.findUnique({
  where: { id: 1 },
  include: { User: true },
})
```

`Post` モデルの `User` フィールドを書き換えたとすると、次のようにアクセスできるようになります。

```ts
const postAuthor = await prisma.post.findUnique({
  where: { id: 1 },
  include: { author: true },
})
```

[この命名規則](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#naming-conventions-1)に従いつつ、リレーションフィールドの名前を変更してみましょう。

```
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ユーザを作成する方法の２通りがあります。後者の方法の方がきめ細かくパーミッションを設定できるため、推奨されています。アクセスキーを取得する詳細な方法については、AWSのガイドなどをご参照ください。

アクセスキーIDとシークレットアクセスキーを取得したら、次のコマンドでセットしましょう。

```sh
serverless config credentials --provider aws --key AWS_ACCESS_KEY_ID  --secret AWS_SECRET_ACCESS_KEY
```


## 7. アプリをデプロイする

デプロイする準備はもう整っています。次のコマンドでデプロイしましょう。

```sh
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
```


## チェックポイント４

statusエンドポイントを呼び出してみましょう。

```sh
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を利用します。

```sh
curl -v https://UNIQUE_IDENTIFIER.execute-api.us-east-1.amazonaws.com/dev/seed
```


## 補足

### serverless.ymlについて

`serverless.yml` という設定ファイルには、エンドポイントと関数の設定が含まれています。このファイルを更新すれば、エンドポイントを追加したり、設定を変更することができます。より詳細なAWSの設定項目は、[Serverless Frameworkのドキュメント](https://serverless.com/framework/docs/providers/aws/guide/functions/)のAWSプロバイダの設定についての項目をご参照ください。


### schema.prisma内のbinaryTargetsについて

Prismaスキーマのgeneratorブロックには、次の一文が含まれていると思います。

```js
binaryTargets = ["native", "rhel-openssl-1.0.x"]
```

この設定が必要な理由は、ローカルのランタイム環境がLambdaのランタイム環境と異なるためです。`binaryTarget` を設定すると、互換性のあるPrismaエンジンのバイナリが使用されるようになります。


### serverless.yml内のパッケージパターンについて

Serverlessの設定ファイルでは、全てのPrismaエンジンのバイナリが除外されるようにパッケージパターンが設定されています。しかし、その内の一つはLambdaのランタイムで必要なものなので、アプリをアップロードするために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-webpack` を使用する場合は、この方法を利用できないことにご注意ください。


### デプロイにserverless-webpackを利用する

`serverless-webpack`を使用する場合、特に設定を行わなくても、[serverless-webpack-prisma](https://github.com/danieluhm2004/serverless-webpack-prisma)というプラグインが利用可能です。`serverless-webpack-prisma`は、以下のような働きをします。

1. Webpackプラグイン経由で `schema.prisma` をコピーします。
1. `prisma generate` コマンドを実行します。なので、Webpackオプションに個別にコマンドを追加する必要はありません。
1. Lambdaで使用されるPrismaエンジンを適切にパッケージ化します。これにより、40MB以上を節約できます。

<details><summary>まだWebpackをインストールしていない場合</summary><div>
依存パッケージをインストールします。

```sh
npm install -D webpack serverless-webpack webpack-node-externals
```

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

```sh
npm install -D ts-loader
```

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

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

```js: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についてのドキュメント](https://www.serverless.com/plugins/serverless-webpack)をご参照ください。
</div></details>


次のコマンドで、開発環境での依存パッケージもインストールしてください。

```sh
npm install -D serverless-webpack-prisma serverless
```

`serverless.yml`のプラグインリストに`serverless-webpack`を追加してください。

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

custom:
  webpack:
    includeModules: true
```

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

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

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

```yml: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サーバでは発生しません。より詳しい解決策については、[サーバレス環境への接続についてのガイド](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#serverless-environments-faas)をご参照ください。


## 翻訳元

翻訳日: `2022/05/21`

[Deploying to AWS Lambda](https://www.prisma.io/docs/guides/deployment/deployment-guides/deploying-to-aws-lambda)

ライセンス：[Apache License 2.0](https://github.com/prisma/docs/blob/main/LICENSE)