Serverless frameworkで`prisma migrate`を実行する

## この記事を書こうと思った理由
serverless frameworkとPrismaで開発をすることになったのですが、
スキーマを反映させるためのコマンド`prisma migrate`を
lambdaで実行するのにとても苦労したので、自分のした処理をここにまとめます。  
本当はもっと良い方法があると思うので、知ってましたら教えてください。   

# 実行手順

## 1.Prismaインストール

PrismaとPrisma CLIをインストールします。

```bash
yarn add prisma @prisma/client
```

Prisma CLIをインストールしたら、Prismaの設定を初期化します。

```bash
yarn prisma init
```

このコマンドにより、prismaディレクトリが作成され、schema.prismaファイルが配置されます。

## 2. データベース接続の設定

.envファイルを開き、使用するデータベースに合わせて接続URLを設定します。  
例えば、MySQLを使用する場合は以下のようになります。

```typescript:env
DATABASE_URL="mysql://<USER>:<PASSWORD>@<HOST>:3306/<DATABASE>"
```

[mysqlのURL設定方法](https://www.prisma.io/docs/orm/overview/databases/mysql#connection-url)

[PostgreSQLのURL設定方法](https://www.prisma.io/docs/orm/overview/databases/postgresql)

prismaはDATABASEの中身は操作できますが、直接DATABASEを作成はできないので、  
ない場合は以下のようにコマンドで作成しましょう（今回はmysqlです。）

```bash
ssh -i <キーペアのpath> ec2-user@<踏み台のIP>

mysql -h <HOST> -u <USER> -p

CREATE DATABASE `sample`;
SHOW DATABASES;
```

## 3. Prismaスキーマの定義

`prisma/schema.prisma`ファイルが自動生成されているはずなので、
以下のように設定する

```
datasource db {
  provider = "mysql"  // お好きなものを設定しましょう
  url      = env("DATABASE_URL")
}

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["rhel-openssl-3.0.x", "native"] // lambda環境向けの設定
  output        = "../src/generated/client"
}

// テーブル設定はお好きな内容にしてください
model prefectures {
  prefecture_cd   Int    @id
  prefecture_name String
}
```

**`output        = "../src/generated/client"`について** 
outputをsrcディレクトリの配下にしているのは、デプロイ時に他のコードと一緒にビルドプロセスに含ませるのが簡単になるというメリットがあります。


## 4. serverless.yml設定

```yml
service: sample

# versionが4以降は有料になるので、3を指定してます。
frameworkVersion: '3'

# 環境変数を.envファイルから読み込む設定
useDotenv: true

plugins:
  - serverless-esbuild #ビルド時にファイルの軽量化、パフォーマンス向上が期待できるのでesbuildを使用

custom:
  stackName: 'sample'
  tableName: 'sample'
  defaultStage: dev
  esbuild:
    bundle: true
    minify: true
    exclude: ['aws-sdk', 'src/__test__/*']
    packager: yarn
    packagePath: './package.json'
    target: 'node20'
    external:
      - prisma
  logRetentionInDays: ${env:LOG_RETENTION_IN_DAYS, 14}

provider:
  name: aws
  region: ${opt:region, "ap-northeast-1"}
  runtime: nodejs20.x
  tags:
    sample_tag: sample
  memorySize: 512 #(MB)
  timeout: 300 #(秒)
  iam:
    role:
      name: 'sample-lambdaRole'
      statements:
        - Effect: Allow
          Action:
            - lambda:InvokeFunction
          Resource: "*"
  vpc:
    securityGroupIds:
      - ${env:SECURITY_GROUP_ID}
    subnetIds:
      - ${env:SUBNET_IDS_AZ}
  environment:
    DATABASE_URL: ${env:DATABASE_URL}

# 以下の設定をすることで複数lambdaがある場合、個別にパッケージされます。  
# それによりデプロイ時間短縮、メモリ使用量の削減などのメリットを得られます。
package:
  individually: true

functions:
  prismaMigrate:
    handler: src/prismaMigrate/index.handler
    name: central-link-api-${env:STAGE}-prismaMigrate
    package:
      patterns:
        - 'prisma/**'
        - 'node_modules/prisma/**/*'
        - 'node_modules/@prisma/**/*'
    environment:
      PRISMA_QUERY_ENGINE_LIBRARY_PATH: ${env:PRISMA_QUERY_ENGINE_LIBRARY_PATH}
      PRISMA_CLI_PATH: ${env:PRISMA_CLI_PATH}
      PRISMA_SCHEMA_PATH: ${env:PRISMA_SCHEMA_PATH}
```

serverless frameworkでデプロイした場合にはlambdaには`handler:`で指定したコードしか反映されません。  
ですが、prismaのコマンドを使用するには以下でまとめている要素がlambda内にないと実行できないので  
`package:patterns:`で含めるべきprismaのコードを指定しています。

**prismaコマンドで必要そうな要素**
* prisma cliコマンドファイル群
* prisma/migrations配下のmigration.sqlファイル
  (`npx prisma migrate dev`コマンドで自動生成されます)
* schema.prismaファイル
* prisma クエリエンジン（基本はクエリ実行のためのものだが、一部DBとの対話に使用されるとのこと。自分は実際にmigrationコマンドを実行するとクエリエンジンがないというエラーが発生しました）

※あくまで個人で試行錯誤して思ったことなので、正確性は保証できません。


## 5. lambda処理の設定

```typescript
import { PrismaClient } from '../generated/client';
import { exec } from 'child_process';
import util from 'util';

process.env.PRISMA_QUERY_ENGINE_LIBRARY = process.env.PRISMA_QUERY_ENGINE_LIBRARY_PATH; // PRISMA_QUERY_ENGINE_LIBRARYのパスを明示するために追加
const PRISMA_CLI_PATH = process.env.PRISMA_CLI_PATH;
const PRISMA_SCHEMA_PATH = process.env.PRISMA_SCHEMA_PATH;

const prisma = new PrismaClient();
const execPromise = util.promisify(exec);

export const handler = async () => {
    try {
        await prisma.$connect();

        const { stdout, stderr } = await execPromise(`node ${PRISMA_CLI_PATH} migrate deploy --schema=${PRISMA_SCHEMA_PATH}`);

        // 標準出力と標準エラー出力をログに出力
        console.log('Migration stdout:', stdout);
        console.log('Migration stderr:', stderr);

        return {
            statusCode: 200,
            body: JSON.stringify({
                message: 'Migration completed successfully',
            }),
        };
    } catch (error) {
        console.error('Error during migration:', error as Error);
        return {
            statusCode: 500,
            body: JSON.stringify({
                message: 'Migration failed',
                error: (error as Error).message,
            }),
        };
    } finally {
        await prisma.$disconnect();
    }
};
```

参考までにそれぞれの環境変数の値は以下の通り
```
PRISMA_QUERY_ENGINE_LIBRARY_PATH=/var/task/node_modules/@prisma/engines/libquery_engine-rhel-openssl-3.0.x.so.node
PRISMA_CLI_PATH=/var/task/node_modules/prisma/build/index.js
PRISMA_SCHEMA_PATH=/var/task/prisma/schema.prisma
```

## 6. デプロイしてみる

以下のコマンドを実行
```
npx prisma migrate dev
```

するとprisma/migrationsのディレクトリ配下にmigration.sqlファイルが生成されます。

lambdaをデプロイしてみる
```
sls deploy
```

## 7. 最後にlambdaを実行してみる

実行して"libquery_engine-rhel-openssl-3.0.x.so.node"が存在しないなどのエラーが出た時には以下のコマンドを実行して再度、デプロイいましょう

```
PRISMA_CLI_BINARY_TARGETS="rhel-openssl-3.0.x" npx prisma generate
```


## 番外編：Github Actionsに自動デプロイ

自分がGithubActionsで自動デプロイを実装した時のコードです。

```yml
name: 🚀Deploy to STG (Auto)

on:
  push:
    branches:
      - develop

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22.3.0'
          cache: 'yarn'
      - run: node --version
      - name: Install dependencies
        run: yarn install
      - name: Prisma Generate
        run: npx prisma generate --schema=./prisma/schema.prisma
      - name: Download and Replace Prisma Engine
        run: PRISMA_CLI_BINARY_TARGETS="rhel-openssl-3.0.x" npx prisma generate
      - name: Deploy to AWS
        run: npx serverless@3 deploy --stage stg --config serverless.yml --verbose
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.STG_AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.STG_AWS_SECRET_ACCESS_KEY }}
          DATABASE_URL: ${{ secrets.STG_DATABASE_URL }}
          STAGE: ${{ vars.STG_STAGE }}
          SECURITY_GROUP_ID: ${{ secrets.STG_SECURITY_GROUP_ID }}
          SUBNET_IDS_AZ1: ${{ secrets.STG_SUBNET_IDS_AZ }}
          PRISMA_QUERY_ENGINE_LIBRARY_PATH: ${{ vars.STG_PRISMA_QUERY_ENGINE_LIBRARY_PATH }}
          PRISMA_CLI_PATH: ${{ vars.STG_PRISMA_CLI_PATH }}
          PRISMA_SCHEMA_PATH: ${{ vars.STG_PRISMA_SCHEMA_PATH }}
```


お疲れ様でした！！！