PrismaのMigrationをAWS Lambdaで実行する

# はじめに
TypeScript用のORMである[Prisma](https://www.prisma.io/)は、DBのスキーマをMigrationという方法で管理することができる。
ActiveRecordのMigrationとよく似ており、`CREATE/ALTER TABLE`のSQLクエリを順次適用していく感じのもの。

Migrationは、DBと疎通している環境から `prisma migrate` というコマンドを打つことで適用できる。
本記事では、このPrismaのMigrationをAWS Lambdaでサーバーレスに実行する方法を紹介する。

# 方法
大まかなアーキテクチャは下図。
なお本記事はDBがAWS上にあることを前提としているが、他のクラウドでも同様の方法は取れるはず。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127392/70ef0478-3cda-92f2-6caa-edab85306975.png)


実際のコードは、[こちらのリポジトリ](https://github.com/aws-samples/prisma-lambda-cdk)を参照。

Migrationを実行するLambdaのコードの抜粋は下記。
Prismaには現状コードからMigrationコマンドを直接呼び出す方法はない([参照](https://github.com/prisma/prisma/issues/4703))。
このため、Prisma CLIを別プロセスで起動することにより、Migrationを実行している。
Migrationプロセスを起動する際に `env` 引数で本体の環境変数(Lambdaに設定したもの)を指定することで、DBの接続情報など必要な情報を渡すことができる。

```ts
export const handler: Handler = async (event, _) => {
  try {
    const exitCode = await new Promise((resolve, _) => {
      execFile(
        path.resolve("./node_modules/prisma/build/index.js"),
        ["migrate", "deploy"],
        {
          env: {
            ...process.env,
          },
        },
        (error, stdout, stderr) => {
          console.log(stdout);
          if (error != null) {
            console.log(`prisma migrate deploy exited with error ${error.message}`);
            resolve(error.code ?? 1);
          } else {
            resolve(0);
          }
        },
      );
    });

    if (exitCode != 0) throw Error(`migration failed with exit code ${exitCode}`);
  } catch (e) {
    console.log(e);
    throw e;
  }
};
```

[AWS CDK](https://github.com/aws/aws-cdk)を利用して、上記のLambdaをデプロイする。
DBの接続情報は、CloudFormationのデプロイ時に[AWS Secrets Managerから読み込み](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-pattern)、Lambdaの環境変数に渡される。

このLambdaを下記コマンドなどで起動することで、Migrationを実行することができる。

```sh
aws lambda invoke --function-name FUNCTION_ARN res.json
```

# 考察
以上が、この記事で紹介するMigrationの方法である。
ここからは、この方法の良し悪しについて考えていく。

## 他にどのような方法があるか
まずは比較のため、他にどのような方法があるか思いつく限り列挙してみる。

1. **CI/CDサーバー上で実行する**
CI/CDのランナーから直接Migrationを実行する方法。
アプリケーションをデプロイするパイプラインの中でMigrationを実行するのは定石なので、実行もCI/CDのサーバーに任せてしまう。
CI/CDサーバーとDBが疎通する必要があるので、[CodeBuildをVPCと疎通させたり](https://docs.aws.amazon.com/codebuild/latest/userguide/vpc-support.html)、SaaSの場合はSelf-hosted runnerを使ったりする。

2. **専用のサーバーを立てて、sshで接続して実行する**
EC2などで作ったサーバーにsshなどで接続し、Migrationを手動で実行する方法。
もちろんサーバーはDBと疎通するネットワークに配置する必要がある。
CI/CDの考え方に染まっていないチームでは、こういう方法を採るところもあるのでは。

3. **ローカル環境から実行する**
各開発者のローカル開発端末からMigrationを実行する方法。
非常に小規模なサービス・チームだと、このような方法を採る場合もあるかもしれない。
VPCと開発環境のネットワークを疎通させる必要があるので、逆に難しいかも。

とりあえず3つほど挙げてみた。
他にこのような方法があるという方はぜひ教えて下さい。

## Lambdaを使う方法のメリット
それでは、上記の方法と比較しながら、今回紹介した方法の良さを考えていく。
メリットは下記のようにいくつかある:

* **DBの認証情報をあちこちに置く必要がない**
認証情報をセキュアに保つためには、できる限り認証情報を直接利用する場所を減らすべきである。
例えば、各開発者のローカル環境それぞれに認証情報が保存されている状態は、開発端末の数だけ漏洩のリスクが増すため、望ましくない。
今回の方法では、DBの認証情報の配置は必要最小限のスコープに抑えられており、またCDK+Secrets Managerによりセキュアな形で一元管理することもできている。


* **CI/CDのサーバーからRDBMSサーバーに疎通可能にする必要がない**
CI/CDからDBにアクセスするためだけにSelf-hosted runnerを立てるのは、明らかに大変だと思う。
また、CodeBuildでVPCとつなぐのも良いが、それほどCodeBuildは使いたくないケースやセキュリティ的に望ましくないケースもあると思われる。
Migration専用のLambdaをVPC上で実行するのは、要件に対して最小限のソリューションに近いので好ましい。

* **サーバーレス**
これは一般的なサーバーレスのメリットと同じだが、EC2などでサーバーを立てるのに比べて管理コストが低い。
サーバーのセキュリティパッチは考えなくてよいし、アクセス権限の管理はIAMで可能。
また、実行ログや監査ログを残すことも容易である。


## Lambdaを使う方法のデメリット
上記のように多くのメリットがあるが、一つ欠点がある。
それは**Lambdaの実行時間の上限が15分**であることである。

Migrationの状況によっては、この制限が問題となる場合がある。
例えば作成するテーブルが非常に多い場合や、運用の長期化によりMigrationファイルが増えてきた場合、データ量が多くALTER TABLEに時間がかかる場合など。
(とはいえ、個人的な感覚で言えば、15分以上かかるのは極稀なケースではないかと思う。)

その場合は、ECS Fargateを利用してMigrationを実行するのが有効と思われる。
ECSクラスタの作成などやや必要な作業は増えるが、実行時間の制限はなくなるためより多くのユースケースに対応可能となる。
また、ECSではLambdaより[セキュアに環境変数で認証情報を渡すことができる](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data-secrets.html)点も優位点である。

ECSでMigrationを実行するには、 `prisma migrate` を実行するDockerfileを書けば良い。
比較的容易に実装できるため、この記事では詳細な手順を割愛する。


# まとめ
AWS LambdaでPrismaのMigrationを実行する方法を紹介した。
他の方法と比べてもメリットの方が多いので、ぜひ試してみてください。