Nest+Prisma を Lambda で動かす

# Nest+Prisma を Lambda で動かす

既に作っていたNestJSのアプリケーションをAWS Lambdaで動かすことにしました。  
備忘録としてNestJS + Prisma + Lambda + Terraform での構築方法もまとめます。  

## 結論
### 詰まったところ
- Prisma のエンジンのパスが Lambda 上で見つからない
- 容量が大きすぎて Lambda にデプロイできない

### 解決方法 
- Prisma のエンジンは Lambda ようにビルドされたものを使う
- Prisma のエンジンは環境変数で指定する
- バンドルファイルを作成して Lambda にデプロイする

## 目次
- [Nest+Prisma を Lambda で動かす](#nestprisma-を-lambda-で動かす)
  - [結論](#結論)
    - [詰まったところ](#詰まったところ)
    - [解決方法](#解決方法)
  - [目次](#目次)
  - [概要](#概要)
  - [前提条件](#前提条件)
  - [ツールのインストールと設定](#ツールのインストールと設定)
    - [AWS CLIのインストール \& 設定](#aws-cliのインストール--設定)
    - [Terraformのインストール](#terraformのインストール)
  - [NestJSアプリケーションの準備](#nestjsアプリケーションの準備)
      - [Serverless Frameworkの設定](#serverless-frameworkの設定)
      - [serverless.ymlの作成](#serverlessymlの作成)
      - [ローカルでの動作確認](#ローカルでの動作確認)
  - [Terraformによるインフラ構築](#terraformによるインフラ構築)
    - [プロバイダーの設定](#プロバイダーの設定)
    - [IAMロールの設定](#iamロールの設定)
    - [Lambda関数の設定](#lambda関数の設定)
    - [API Gatewayの設定](#api-gatewayの設定)
    - [Terraformの実行](#terraformの実行)
    - [デプロイと動作確認](#デプロイと動作確認)
    - [動作確認](#動作確認)
    - [Prismaの対応](#prismaの対応)
  - [まとめ](#まとめ)

## 概要
以下の図は、今回構築するアーキテクチャの概要です。  

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918370/a64095b5-09ad-bbf9-34ad-08bd10c2b1a7.png)


AWS S3: Lambda関数で使用するNestJSのバンドルファイルを保存します。  
AWS Lambda: NestJSアプリケーションがデプロイされている実行環境です。リクエストを処理し、レスポンスを返します。  
AWS API Gateway: クライアントからのリクエストを受け取り、対応するLambda関数にルーティングします。  
AWS IAMロール: Lambda関数に必要な権限を付与します。  
クライアント（ユーザー）: ブラウザやアプリケーションからHTTPリクエストを送信します。  
Terraform: 上記のAWSリソースをコードで定義・管理し、インフラのプロビジョニングや更新を自動化します。  

## 前提条件
- AWSアカウントを持っていること
- AWS CLIとTerraformがインストールされていること

## ツールのインストールと設定
### AWS CLIのインストール & 設定

[【AWS】aws cliの設定方法](https://zenn.dev/akkie1030/articles/aws-cli-setup-tutorial)  
↑ こちらの記事がとてもわかりやすいです。
``` bash
# AWS CLIのインストール
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

# AWS CLIのインストール確認
aws --version

# AWS CLIの設定
aws configure
```


### Terraformのインストール
[公式：Terraform のインストール](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli)

```bash
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

# インストール確認 (terraform)
terraform --version
```

## NestJSアプリケーションの準備
こちらは、既に用意されているものを使用します。  

```bash
### main.tsの編集
main.tsを以下のように編集します。

```typescript
# コードの例
import serverlessExpress from '@codegenie/serverless-express';
import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Callback, Context, Handler } from 'aws-lambda';
import { AppModule } from './app.module';

let server: Handler;

// 共通のブートストラップ関数
async function bootstrap(): Promise<Handler | INestApplication> {
  const app = await NestFactory.create(AppModule);
  await app.init();
  const expressApp = app.getHttpAdapter().getInstance();
  return serverlessExpress({ app: expressApp });
}

// Lambda ハンドラー
export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) => {
  context.callbackWaitsForEmptyEventLoop = false;
  server = server ?? (await bootstrap());
  return server(event, context, callback);
};

```

#### Serverless Frameworkの設定
ローカル環境で動かすために、Serverless Frameworkをインストールします。

```bash
npm install serverless serverless-offline @codegenie/serverless-express
```
#### serverless.ymlの作成
プロジェクトのルートディレクトリにserverless.ymlを作成し、以下の内容を記述します。
serverless-offlineプラグインを使用して、ローカルでの動作をサポートします。 

```yaml
service: nestjs-lambda-example

plugins:
  - serverless-offline

provider:
  name: aws
  runtime: nodejs22.x
  region: ap-northeast-1
  environment:
    IS_LAMBDA: 'true'

custom:
  serverless-offline:
    httpPort: 3000
    lambdaPort: 3002
    host: localhost
    debug: true
    useChildProcesses: true
    allowCache: true

functions:
  main:
    handler: dist/main.handler
    timeout: 30
    events:
      - httpApi:
          method: '*'
          path: '/{proxy+}'
          payloadFormatVersion: '2.0'
```

#### ローカルでの動作確認
以下のコマンドでローカルサーバーを起動し、動作を確認します。

```bash
serverless offline
```

ブラウザで http://localhost:3000 にアクセスし、正常に動作していることを確認します。

```bash
curl http://localhost:3000
```

こちらは使用している NestJS の挙動を確認します。

## Terraformによるインフラ構築
Terraform の設定ファイルを作成し、以下の内容を記述します。



### プロバイダーの設定
```provider.tf
provider "aws" {
  region = "ap-northeast-1"
}
```

### IAMロールの設定
```iam.tf
resource "aws_iam_role" "lambda_exec" {
  name = "lambda_exec_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_policy_attachment" "lambda_exec_attach" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  roles      = [aws_iam_role.lambda_exec.name]
}
```

### Lambda関数の設定
```lambda.tf
resource "aws_lambda_function" "nestjs_lambda" {
  function_name    = "nestjs-function"
  role             = aws_iam_role.lambda_exec.arn
  handler          = "dist/main.handler"
  runtime          = "nodejs20.x"
  filename         = "path/to/your/nestjs/lambda.zip"
  source_code_hash = filebase64sha256("path/to/your/nestjs/lambda.zip")

  environment {
    variables = {
      NODE_ENV         = "production"
      IS_LAMBDA        = "true"
      PRISMA_QUERY_ENGINE_LIBRARY = "/var/task/node_modules/@prisma/engines/libquery_engine-rhel-openssl-1.0.x.so.node"
    }
  }
}
```

### API Gatewayの設定
```api_gateway.tf
resource "aws_apigatewayv2_api" "nestjs_api" {
  name          = "nestjs-api"
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_integration" "nestjs_integration" {
  api_id           = aws_apigatewayv2_api.nestjs_api.id
  integration_type = "AWS_PROXY"
  integration_uri  = aws_lambda_function.nestjs_lambda.invoke_arn
  payload_format_version = "2.0"
}

resource "aws_apigatewayv2_route" "nestjs_route" {
  api_id    = aws_apigatewayv2_api.nestjs_api.id
  route_key = "$default"
  target    = "integrations/${aws_apigatewayv2_integration.nestjs_integration.id}"
}

resource "aws_lambda_permission" "api_gw" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.nestjs_lambda.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.nestjs_api.execution_arn}/*/*"
}
```
### Terraformの実行
Terraformの初期化、プラン作成、適用を行います。

```bash
terraform init
terraform plan
terraform apply
```
### デプロイと動作確認
Lambda関数へのデプロイ
NestJSアプリケーションをビルドし、Lambda関数にデプロイします。


package.jsonに以下のスクリプトを追加します。
```json
// package.json

  "scripts": {
    "build": "nest build",
    "start": "nest start",
    "esbuild": "node esbuild.config.js",
    "zip": "zip -r lambda.zip build"
  },
```

esbuild.config.jsを作成し、以下の内容を記述します。
```javascript
const { build } = require('esbuild');
const { copy } = require('esbuild-plugin-copy');
const { esbuildDecorators  } = require('esbuild-decorators');

build({
  entryPoints: ['src/main.ts'], // エントリーポイントを指定
  bundle: true,
  platform: 'node',
  target: 'node20',
  outfile: 'build/main.js',
  external: ['aws-sdk',
    'class-transformer',
    'class-validator',
    '@nestjs/websockets',
    '@nestjs/microservices',
    '@nestjs/websockets/socket-module',
    '@nestjs/microservices/microservices-module'
  ],
  plugins: [
    copy({
      // prismのエンジンをコピー
      assets: [
        {
          from: './node_modules/.prisma/**',
          to: './node_modules/.prisma', // ビルド先にコピー
        },
        {
          from: './node_modules/@prisma/**',
          to: './node_modules/@prisma', // ビルド先にコピー
        },
      ],
      verbose: true,
    }),
    esbuildDecorators(),
  ],
}).catch(() => process.exit(1));

```

```bash
npm run esbuild
npm run build
zip -r lambda.zip .
```
lambda.zipファイルをTerraform設定ファイルで指定したパスに配置します。

### 動作確認
API Gatewayのエンドポイントにリクエストを送信し、正常にレスポンスが返ってくることを確認します。

```bash
# コードをコピーする
curl https://{api_id}.execute-api.ap-northeast-1.amazonaws.com/
```
### Prismaの対応
Prismaを使用している場合、Lambda環境でのPrismaエンジンのパスを指定する必要があります。環境変数に以下を追加します。

```hcl
environment {
  variables = {
    PRISMA_QUERY_ENGINE_LIBRARY = "/var/task/node_modules/@prisma/engines/libquery_engine-rhel-openssl-1.0.x.so.node"
  }
}
```

## まとめ
以上で、NestJSアプリケーションをAWS Lambda上で動作させ、Terraformを用いてインフラを構築・管理する方法を記載しました。  
当初、Prismaのエンジンのパスが見つからないという問題に頭を抱えました。
いろんな解決方法がありそうでしたが、私がうまく行ったのは、環境変数に値をいれることでした。

今回の実装のついでに、Terraform でのAWSリソースの作成について知見を深めることもできたので良かったと思います。

参考資料
- NestJS公式ドキュメント - サーバーレス
- Terraform公式ドキュメント
- AWS Lambda - Prisma対応