0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Lambda で Prisma 使って Migrate する

Posted at

やりたいこと

AWS で RDS 作ったけど、テーブル作ったりするのどうしよー
という事で、今回は Prisma を AWS Lambda に入れて、migrate を実行してみました。

  1. ローカルで prisma migrate dev を実行して migration ファイルを作成
  2. AWS Lambda で prisma migrate deploy を実行して Amazon RDS に適応

ということをやろうと思います。こうすることで、

  • AWS 上にサーバーを立てることなく DB のテーブル作成が可能
  • Prisma の migration ファイルを管理することで、複数人での開発や、他環境への適応も楽

になるはず。
ちなみに、私は Amplify を使って Lambda を作成しました。

Lambda のコード

RDS のユーザ名、パスワードは Secrets Manager にある前提です。
以下の環境変数の設定が必要です。

  • DATABASE_SECRET_ARN: Secrets Manager の ARN
  • DATABASE_ENGINE: データベースエンジン名 (postgresql とか)
  • DATABASE_HOST: RDS のエンドポイント
  • DATABASE_PORT: RDS のポート番号
  • DATABASE_NAME: データベース名
index.ts
import { execFile } from 'child_process';
import * as path from 'path';
import {
  SecretsManagerClient,
  GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
import { type Handler, type Context } from 'aws-lambda';

type Event = {
  typeName: string;
  fieldName: string;
  arguments?: {
    command?: string;  // deploy, reset or resolve
    option?: string;   // resolve の時に migration 名を指定
  };
};

type DatabaseSecrets = {
  username: string;
  password: string;
};

const client = new SecretsManagerClient();

/* SecretsManager から username と password を取得 */
const getDatabaseSecrets = async (
  secretName: string,
): Promise<DatabaseSecrets | undefined> => {
  const response = await client.send(
    new GetSecretValueCommand({
      SecretId: secretName,
      VersionStage: 'AWSCURRENT',
    }),
  );
  if (response.SecretString == null) return;

  const secret = JSON.parse(response.SecretString) as DatabaseSecrets;
  return secret;
};

/* prisma コマンドを実行 */
const execMigrate = async (command: string, options: string[]) => {
  const exitCode = await new Promise<number>((resolve, _reject) => {
    execFile(
      path.resolve('./node_modules/prisma/build/index.js'),
      ['migrate', command].concat(options),
      (error, stdout, _stderr) => {
        console.log(stdout);
        
        if (error != null) {
          console.error(
            `prisma migrate ${command} exited with error ${error.message}`,
          );
          resolve(Number(error.code) ?? 1);
        } else {
          resolve(0);
        }
      },
    );
  });

  if (exitCode !== 0)
    throw Error(`prisma migrate ${command} failed with exit code ${exitCode}`);
};

export const handler: Handler = async (event: Event, _context: Context) => {
  console.log(`EVENT: ${JSON.stringify(event)}`);

  // 環境変数チェック
  if (process.env.DATABASE_SECRET_ARN == null) {
    throw new Error('DATABASE_SECRET_ARN is not defined.');
  }
  if (process.env.DATABASE_ENGINE == null) {
    throw new Error('DATABASE_ENGINE is not defined.');
  }
  if (process.env.DATABASE_HOST == null) {
    throw new Error('DATABASE_HOST is not defined.');
  }
  if (process.env.DATABASE_PORT == null) {
    throw new Error('DATABASE_PORT is not defined.');
  }
  if (process.env.DATABASE_NAME == null) {
    throw new Error('DATABASE_NAME is not defined.');
  }

  // SecretsManager から username, password を取得
  const secretArn = process.env.DATABASE_SECRET_ARN;
  const secrets = await getDatabaseSecrets(secretArn);
  if (secrets == null) {
    throw new Error('Cannot get database secrets.');
  }

  // URL セーフにする
  const username = encodeURIComponent(secrets.username);
  const password = encodeURIComponent(secrets.password);
  
  // Prisma コマンドで使う環境変数を設定
  const engine = process.env.DATABASE_ENGINE;
  const host = process.env.DATABASE_HOST;
  const port = process.env.DATABASE_PORT;
  const dbName = process.env.DATABASE_NAME;
  process.env.DATABASE_URL = `${engine}://${username}:${password}@${host}:${port}/${dbName}`;

  // 引数なしの場合は prisma migrate deploy を実行
  const command: string = event?.arguments?.command ?? 'deploy';
  let options: string[] = [];
  switch (command) {
    case 'reset':
      // prisma reset --force --skip-generate を実行
      options = ['--force', '--skip-generate'];
      break;
    case 'resolve':
      // prisma migrate resolve --rolled-back を実行
      if (event?.arguments?.option == null) {
        throw new Error('migration name is not defined.');
      }
      options = ['--rolled-back', event.arguments.option];
  }

  // Prisma コマンドを実行
  await execMigrate(command, options);
};

Lambda 作成時の注意事項!

npm install 時に環境変数を設定する

Prisma は npm install 時、環境に合わせた BINARY を取得します。
Lambda の実行環境は Amazon Linux なので、ローカルで npm install したものをアップロードしてしまうと BINARY が無いため動きません。
そこで、環境変数 PRISMA_CLI_BINARY_TARGETSrhel-openssl-3.0.x を追加する必要があります。
Apple Silicon の Mac の場合は以下のようにして npm install を実行します。

PRISMA_CLI_BINARY_TARGETS=darwin-arm64,rhel-openssl-3.0.x npm install prisma

使い方

事前に、ローカルで DB を立て、環境変数 DATABASE_URL を立てた DB 向けに設定しておく必要があります。

  1. ローカルで prisma/schema.prisma を作成/編集
  2. ローカルで npx prisma migrate dev を実行
    • prisma/migrations/ に migration ファイルが追加される
  3. 作成された migration ファイルを含めて AWS Lambda を作成
  4. AWS Lambda を実行
    • npx prisma migrate deploy が Amazon RDS 向けに実行される

migration ファイルをアップロードするために、毎回 Lambda を更新するのがちょっと手間だなー

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?