0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PrivateなAuroraをdrizzleでマイグレーションするlambdaを作ったメモ

0
Posted at

概要

privateなAurora MySQLをLambdaとdrizzleを使ってマイグレーションしてみたメモ。
この時点のソース

フォルダ構造

- apps
  - api
    - drizzle # このディレクトリをlambdaにコピー
      + meta
      - 0000_hoge.sql
    - src
      - db
        - migration.ts # lambda本体
    - package.json # drizzle-kit generate でdrizzleフォルダ配下にマイグレーションファイル作成
    - drizzle.config.ts
- infra
  - lib
    - infra-stack.ts
  - package.json
  - scripts
    - db-migrate.mjs # lambdaをローカル起動させる aws コマンドのバッチ

ソースコード

Lambda handler

DBに接続し、drizzleを使ってマイグレーションを行う。

apps/api/src/db/migration.ts
import { Handler } from 'aws-lambda';
import {
  SecretsManagerClient,
  GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
import { createConnection } from 'mysql2/promise';
import type { Connection } from 'mysql2/promise';
import { drizzle } from 'drizzle-orm/mysql2';
import { migrate } from 'drizzle-orm/mysql2/migrator';
import * as path from 'path';

const secretsClient = new SecretsManagerClient({
  region: process.env.AWS_REGION ?? 'ap-northeast-1',
});

interface DbSecret {
  host: string;
  port: number;
  username: string;
  password: string;
  dbname: string;
}

async function getConnection(): Promise<Connection> {
  const secretArn = process.env.DB_SECRET_ARN;
  if (!secretArn)
    throw new Error('DB_SECRET_ARN environment variable is not set');
  const response = await secretsClient.send(
    new GetSecretValueCommand({ SecretId: secretArn }),
  );
  if (!response.SecretString) throw new Error('SecretString is empty');
  const secret = JSON.parse(response.SecretString) as DbSecret;
  return createConnection({
    host: secret.host,
    port: secret.port,
    user: secret.username,
    password: secret.password,
    database: secret.dbname,
    ssl: { rejectUnauthorized: false },
  });
}

export const handler: Handler = async () => {
  const connection = await getConnection();
  try {
    const db = drizzle(connection);
    await migrate(db, { migrationsFolder: path.join(__dirname, 'migrations') });
    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'Migration compoeted success' }),
    };
  } finally {
    await connection.end();
  }
};

cdk

migrationのlambdaを作成。その際、apps/api/drizzleディレクトリをlambdaに同梱する。

infra/lib/infra-stack.ts
    // 省略
    const lambdaDefaults: Omit<lambdaNodejs.NodejsFunctionProps, 'entry'> = {
      runtime: lambda.Runtime.NODEJS_24_X,
      architecture: isLocal ? undefined : lambda.Architecture.ARM_64,
      handler: 'handler',
      timeout: cdk.Duration.seconds(30),
      vpc,
      vpcSubnets: { subnetGroupName: 'lambda' },
      securityGroups: [lambdaSecurityGroup],
      environment: {
        DB_SECRET_ARN: auroraCluster.secret!.secretArn,
      },
      projectRoot: path.join(__dirname, '../..'),
      bundling: {
        minify: true,
        sourceMap: false,
        target: 'node24',
        externalModules: ['@aws-sdk/*'],
      },
    };
    const migrationFunction = new lambdaNodejs.NodejsFunction(
      this,
      'migrationFunction',
      {
        ...lambdaDefaults,
        entry: path.join(__dirname, '../../apps/api/src/db/migration.ts'),
        timeout: cdk.Duration.seconds(60),
        bundling: {
          ...lambdaDefaults.bundling,
          commandHooks: {
            beforeInstall: () => [],
            beforeBundling: () => [],
            afterBundling: (inputDir: string, outputDir: string) => {
              // Windows / Linux の差異吸収
              const src = inputDir.replace(/\\/g, '/');
              const dst = outputDir.replace(/\\/g, '/');
              return [
                `node -e "const {cpSync}=require('fs');const {join}=require('path');cpSync(join('${src}','apps','api','drizzle'),join('${dst}','migrations'),{recursive:true})"`,
              ];
            },
          },
        },
      },
    );
    auroraCluster.secret!.grantRead(migrationFunction);
    new cdk.CfnOutput(this, 'MigrationFunctionName', {
      value: migrationFunction.functionName,
      description: 'Migration Lambda function name',
    });
    // 省略
infra/package.json
{
  // 省略
  "scripts": {
    "deploy": "cdk deploy",
    "db:migrate": "node scripts/db-migrate.mjs",
  },
  // 省略
}

マイグレーションlambdaの起動用スクリプト

aws invoke。関数名を取得して使うようにしている。
コンソールから手で実行したほうがはやいといえば早いかも

infra/scripts/db-migrate.mjs
import { spawnSync, execSync } from 'child_process';
import { readFileSync, unlinkSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';

const cfnResult = spawnSync(
  'aws',
  [
    'cloudformation', 'describe-stacks',
    '--stack-name', 'InfraStack',
    '--query', 'Stacks[0].Outputs[?OutputKey==`MigrationFunctionName`].OutputValue | [0]',
    '--output', 'text',
  ],
  { encoding: 'utf-8' },
);

if (cfnResult.error || cfnResult.status !== 0) {
  console.error(cfnResult.stderr || cfnResult.error);
  process.exit(1);
}

const functionName = cfnResult.stdout.trim();

if (!functionName) {
  console.error(
    'MigrationFunctionNameが取得できませんでした。InfraStackがデプロイ済か確認してください。',
  );
  process.exit(1);
}

console.log(`Invoking Lambda: ${functionName}`);

const outFile = join(tmpdir(), 'lambda-migrate-out.json');
execSync(
  `aws lambda invoke --function-name ${functionName} --payload "{}" --cli-binary-format raw-in-base64-out "${outFile}"`,
  { stdio: 'inherit' },
);

const result = readFileSync(outFile, 'utf-8');
console.log(result);
unlinkSync(outFile);

npm run deployでpermission エラー

今回のCDKのdeployで下記のエラーがでた。

Done in 24ms
[«FailedToBundleAsset» Failed to bundle asset InfraStack/migrationFunction/Code/Stage, bundle output is located at
 Error: EPERM: operation not permitted, rename 
     ...aws-cdk-lib...
    (no user code in 10 frames, use --stack-trace-limit to capture more)
Relates to construct:
    <.> (aws-cdk-lib.App)
     └─ InfraStack (aws-cdk-lib.Stack)
         └─ migrationFunction (aws-cdk-lib.aws_lambda_nodejs.NodejsFunction)
             └─ Code (aws-cdk-lib.aws_s3_assets.Asset)
                 └─ Stage (aws-cdk-lib.AssetStaging)]
npx ts-node --prefer-ts-exts bin/infra.ts: Subprocess exited with error 1

一度、vscodeからcdk.outを削除しようとすると、削除権限を確認される。
これを許可して削除したのち、再度 cdk deployしたところ、このエラーはでなくなった。

image.png
]

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?