AWS CDK Pipeline × Prisma でRDSに接続するLambdaを実装する
※本エントリは、AWS CDK Advent Calendar 2022 の21日目の記事となります。
TypeScriptのORMであるPrismaを使っており、CDKでデプロイしようとしたのですが、何点か難しい箇所があったので、構築の流れとポイントをまとめました。
全て書くとトンデモ文量になってしまうため、端折っています。
ソースコード自体はAWSにデプロイすれば動作するため、すべて見たい方はソースコードを見ていただければと思います。
PrismaもCDK Pipelineも触り始めたのが1ヶ月前くらいからなので、変な書き方をしていたらご指摘いただけるとありがたいです。
本記事の概要
今回の記事は、以下の流れで進めていきます。
- ローカルでPrismaを動かせるようにする
- CDKを使ってLambda, RDSをデプロイする
- CDK Pipelineで自動デプロイできるようにする
- 動作確認
- RDSへのマイグレーションを自動化する(また別の記事)
(マイグレーションの自動化は、今回力尽きて諦めました。)
今回の完成系コードは以下に上げています。
ある程度分かる方は、コードだけ見てもらった方が良いかもしれません。
ちなみに、バックエンドのソースは、簡略化したオニオンアーキテクチャの構造にしました。アプリケーション層とかドメイン層のエンティティは省略しています。
リポジトリの構造が好きなので、使いたかっただけです。
なお、なるべく実際の業務でも取り回しできるように、dev, prd環境があるような想定をして書いています。
ローカル環境でPrismaを動かせるようにする
まずは、ローカルで立ち上げたコンテナのDBと接続できる状態にします。
初期設定
詳しくは、公式チュートリアルを参照(https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases-typescript-postgres)。
バックエンドのディレクトリ配下で、以下を実行しました。
npm init -y
npm install prisma typescript ts-node @types/node --save-dev
npx tsc --init
npx prisma
npx prisma init
テーブルの追加とマイグレーション
初期設定にて出来上がったschemaに、task modelを定義します。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model task {
id String @id
text String
}
dockerでDBのコンテナを立ち上げた状態で、マイグレーションを行います。
ソースにはdocker-compose up -d
でDBのコンテナを立ち上げられるように用意してあります。
また、DBの接続情報はデフォルトで.envファイルのDATABASE_URL
で保持しているため、こちらを修正します。
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/cdk_prisma?schema=cdk_prisma"
schema定義に従ったマイグレーションを行うために、以下のコマンドを実行します。
npx prisma migrate dev --name init
primsaディレクトリ配下にマイグレーションのためのSQLコマンドファイルが発行され、ローカルDB側にはtaskテーブルが作成されています。
ちなみに、これをチーム開発で行う場合、他の開発者は出来上がったマイグレーションのSQLを以下のコマンドで自身のローカル環境に流し込みます。
npx prisma migrate deploy
このように、PrismaとローカルDBの設定を行うことが出来ました。
コードからInsertを実行する
ココマデ、実装の話に触れていませんでした。
ココカラ、Prismaのコードについて触れていきます。
詳細はGitHubのコードを参照してください。ここでは重要な点だけ記載します。
PrismaClientのインスタンスを発行するための関数を外出しします。
後でAWS環境で使うためのコードに書き換える際に、外出しした方が色々都合が良いです。
import { PrismaClient } from "@prisma/client";
export async function getPrismaClient() {
const dbClient = new PrismaClient()
return dbClient;
}
そして、insertは以下のようなコードになります。
ポイントは、prismaClient.<テーブル名>.create
のようにしてテーブルのメソッドにアクセスすることです。
async insert(text: string) {
await this.prismaClient.task.create({
data: {
id: uuidv4(),
text: text,
},
});
}
Prismaでは、PrismaClientのモジュールがスキーマ定義に従って動的に生成されます。
モジュールが生成されるタイミングは、prisma migrate
やprisma generate
をしたタイミングになります。
後述しますが、この辺りがAWS環境に上げるうえで少し難しいポイントになってきます。生成されたPrismaClientをAWS環境に上げる必要があるためです。
ここまで書けたら、テストコードの実行によってInsertが出来るか確認します。
※詳細はリポジトリのコードを参照
import { CreateTaskController } from "./create-task-controller";
describe("テスト(実行のみ)", () => {
test("insert", async () => {
const controller = new CreateTaskController();
await controller.execute();
});
});
テストコードの実行
npx jest
実際に、タスクテーブルにデータが入っていることを確認できました。
ココマデの進捗
- ローカルでPrismaを動かせるようにする 🌸
- CDKを使ってLambda, RDSをデプロイする
- CDK Pipelineで自動デプロイできるようにする
- 動作確認
- RDSへのマイグレーションを自動化する
次は、CDKでLambdaとRDSをデプロイしていきます。
CDKを使ってLambdaでPrismaを動かせるようにする
Prisma側の設定
LambdaにPrismaを動作させるためには、Prisma EngineとPrisma Clientの両方をデプロイする必要があります。
以下の記事には大変お世話になりました。
Lambdaで動作できる Prisma Engine を用意するために、schemaに設定を追記し、Prisma Engineを再生成します。
binaryTargets
を追記します。
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-1.0.x"]
}
以下のコマンドで、Prisma Engineを生成します。
npx prisma generate
AWS CDKで VPC, RDS まわりの実装
ここで漸く、AWS CDKの話に入っていきます。
まず、VPC, Aurora RDS, RDS Proxyの環境を作るのですが、この辺りは長くなるのと本題とは関係が無いので割愛します。詳細はコードを参照ください。
なお、ほとんどこちらの記事を参考につくらせて頂きました。
※RDS ProxyはPrismaでは必要ない、と公式に記載がありますが、今回は一般的なLambda,RDS構成で使うように RDS Proxy を利用しています。まだRDS Proxy無しの検証が私の方で出来ていないためです。
1点注意するべき点が、データベースのパスワードです。
AWSのSecrets Managerを利用してパスワードを生成しているのですが、Prismaで使うときにエンコーディングが必要な文字列が含まれてしまう可能性があります。
色々と面倒なので、エンコーディングが必要な文字列の使用を回避するために、以下のように使えない文字を指定します。
const EXCLUDE_CHARACTERS = ":/?#[]@!$&'()*+,;=%\"";
なお、エンコーディングが必要な文字列は以下に記載があります。
AWS CDKで Lambda まわりの実装
この章で一番説明したかったのが、この部分になります。
先ほど生成した Prisma Engine をAWS環境に上げる必要があるのですが、普通にやってもデプロイすることはできません。
NodejsFunction
のbundling
の設定を利用することで、Prisma Engineをデプロイします。
bundling: {
forceDockerBundling: false,
minify: true,
sourceMap: true,
commandHooks: {
beforeBundling: (i, o) => [`cd ${i} && npm ci`],
afterBundling(i: string, o: string): string[] {
return [
// Prismaクエリエンジンを追加
`cp ${i}/node_modules/.prisma/client/libquery_engine-rhel-* ${o}`,
// Prismaスキーマ定義を追加
`cp ${i}/prisma/schema.prisma ${o}`,
];
},
beforeInstall: (i, o) => [],
},
},
AWS でも利用できるように PrismaClient の取得処理の実装
ローカルでもAWS環境でも動作させられるように、PrismaClientのインスタンス取得処理を実装していきます。
完成系は以下のようになりました。
"dev"がAWS環境の想定で、それ以外であればローカル実行、という想定になっています。
import "dotenv/config";
import {
SecretsManagerClient,
GetSecretValueCommand,
GetSecretValueCommandOutput,
} from "@aws-sdk/client-secrets-manager";
import { PrismaClient } from "@prisma/client";
export async function getPrismaClient() {
let dbClient: PrismaClient;
if (process.env.ENV_NAME === "dev") {
const secretsManagerClient = new SecretsManagerClient({
region: process.env.AWS_REGION!,
});
const getSecretValueCommand = new GetSecretValueCommand({
SecretId: process.env.SECRET_ID,
});
const getSecretValueCommandResponse = await secretsManagerClient.send(
getSecretValueCommand
);
const secret = JSON.parse(getSecretValueCommandResponse.SecretString!);
const dbUrl = `postgresql://${secret.username}:${secret.password}@${process.env.PG_HOST}:${secret.port}/${secret.dbname}?schema=cdk_prisma&connection_limit=1&socket_timeout=3`;
dbClient = new PrismaClient({
datasources: {
db: {
url: dbUrl,
},
},
});
} else {
dbClient = new PrismaClient();
}
return dbClient;
}
AWS環境下ではRDSの情報をSecretsManagerから取得します。RDSの情報をSecretsManagerで管理する方法は、CDKのコードを見ていただければと思います。
こちらのコードの中で動的にエンドポイントを組み立てることで、Lambda -> RDSのアクセスを可能にしています。
また、Lambdaの環境変数に、環境名を与えることで、AWS環境/ローカル環境を判断できるようにしています。
ローカルでは、先ほどから引き続き.envのDATABASE_URLを参照していきます。
AWS CDKでデプロイを実行
ここまで出来たら、後はデプロイするだけです。
cdk bootstrap --profile <プロフィール名>
cdk deploy DevAppStack --profile <プロフィール名>
と、ここで私の環境では、以下のdepsLockFilePath
かbundling
の設定があると、エラーが発生してデプロイが出来なくなりました。
Windows, WSL2, Ubuntuの環境です。
※NodejsFunctionsのpropsの中身です。
depsLockFilePath: path.join(__dirname, backendPath, "package-lock.json"),
bundling: {
forceDockerBundling: false,
commandHooks: {
beforeBundling: (i, o) => [`cd ${i} && npm ci`],
afterBundling(i: string, o: string): string[] {
return [
// Prismaクエリエンジンを追加
`cp ${i}/node_modules/.prisma/client/libquery_engine-rhel-* ${o}`,
// Prismaスキーマ定義を追加
`cp ${i}/prisma/schema.prisma ${o}`,
];
},
beforeInstall: (i, o) => [],
},
},
エラー内容
Failed to bundle asset DevAppStack/TaskFunction/Code/Stage
こちら、原因解消に2時間以上かけても分からなかったため、ローカルでのデプロイを一旦諦めました。私が使っている環境がWSL2ということもあり、他で再現するか謎なため、ここに時間をかけるのは勿体ないと思いました。また、WSL2とNodejsFunctionsにまつわるエラーは多少あるようで、それであれば解決できないため、後回しにします。
解明できたら、この記事に追記しようと思います。
後述するCDK Pipelineを利用することで、デプロイできるようにしていきます。
ここでは、先ほどの設定をコメントアウトしてデプロイを成功させました。
ココマデの進捗
- ローカルでPrismaを動かせるようにする 🌸
- CDKを使ってLambda, RDSをデプロイする 🌸
- CDK Pipelineで自動デプロイできるようにする
- 動作確認
- RDSへのマイグレーションを自動化する
bundlingの設定があるとデプロイが出来ない、という問題が発生していますが、そこ以外はデプロイすることが出来ました。
次に、CDK Pipelineを設定し、自動デプロイと先ほどのエラー解消を行っていきます。
CDK Pipelineで自動デプロイできるようにする
CDK Pipelineの詳細な説明は省きます。
CDKコードの修正
CDK pipelineを使うように、スタックを用意します。
パイプラインのスタックの完成系は以下のようになりました。
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import {
CodePipeline,
CodePipelineSource,
ShellStep,
} from "aws-cdk-lib/pipelines";
import * as iam from "aws-cdk-lib/aws-iam";
import { PipelineAppStage } from "./app-stage";
import { CommonProps } from "./settings/common-props";
interface PipelineProps extends CommonProps {
repository: string;
branch: string;
connectionArn: string;
}
export class PipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: PipelineProps) {
super(scope, id, props);
const pipeline = new CodePipeline(this, "Pipeline", {
pipelineName: "BackendPipeline",
synth: new ShellStep("Synth", {
input: CodePipelineSource.connection(props.repository, props.branch, {
connectionArn: props.connectionArn,
}),
commands: [
"cd Backend",
"npm ci",
"npm run prisma:generate",
"cd ..",
"cd Infra",
"npm ci",
"npm run build",
"npx cdk synth",
],
primaryOutputDirectory: "Infra/cdk.out",
}),
codeBuildDefaults: {
rolePolicy: [
new iam.PolicyStatement({
actions: ["*"],
resources: ["*"],
}),
],
},
});
pipeline.addStage(new PipelineAppStage(this, "backendStage", props));
}
}
ポイントは、commandsの部分です。
commands: [
"cd Backend",
"npm ci",
"npx prisma generate",
"cd ..",
"cd Infra",
"npm ci",
"npm run build",
"npx cdk synth",
],
Backend配下でnpx prisma generate
を行うことで、Prisma ClientとPrisma Engineを生成しています。
詳細はリポジトリをご参照ください。
ここまで出来たら、cdk deploy で CodePipelineのデプロイを行います。
cdk deploy --profile <プロフィール名>
bundlingの設定も反映する
先ほど、ローカルで失敗していたbundling部分のコメントアウトを解除し、 CodePipelineのターゲットとなっているブランチにプッシュします。
すると、無事にデプロイ成功していることが確認できました。
ココマデの進捗
- ローカルでPrismaを動かせるようにする 🌸
- CDKを使ってLambda, RDSをデプロイする 🌸
- CDK Pipelineで自動デプロイできるようにする 🌸
- 動作確認
- RDSへのマイグレーションを自動化する
動作確認
ここまで出来たら、動作確認です。
DBにテーブルを作成する
まず、RDSにテーブルが無い状態なので、接続してtaskテーブルを作成します。
今回作成している環境では、踏み台サーバーをパブリックサブネットに置いているので、pemキーを利用してローカルからRDSに接続できます。
今回のコードではCloudFormationのアウトプットに、公開鍵を取得できるコマンドを用意していますので、そちらで取得したpem情報をもとに、ローカルからDBに接続します。
ローカルからDBに接続出来たら、以下のSQLでスキーマの作成とテーブルの作成を行います。
CREATE SCHEMA cdk_prisma;
SET search_path TO cdk_prisma;
-- CreateTable
CREATE TABLE "task" (
"id" TEXT NOT NULL,
"text" TEXT NOT NULL,
CONSTRAINT "task_pkey" PRIMARY KEY ("id")
);
こんな感じで、テーブルが作成されていることがDBクライアントツールから確認できました。
Lambdaの実行をためす
ようやく、Lambdaの実行です。
今回は簡単に、マネジメントコンソールから試しましょう。
そしてテーブルの中身を確認。
しっかり入ってます!
ココマデの進捗
これで、今回やろうとしていたことは全て完了しました。
マイグレーションの自動化は、続編で書こうと思っています。
- ローカルでPrismaを動かせるようにする 🌸
- CDKを使ってLambda, RDSをデプロイする 🌸
- CDK Pipelineで自動デプロイできるようにする 🌸
- 動作確認 🌸
- RDSへのマイグレーションを自動化する(続編を書く予定)
おわりに
感想
ローカルからデプロイが出来なかったり、RDS Proxy無しでの検証が出来ていなかったり、まだまだ検証すべきところはあるのですが、詰まった点は沢山あったので、伝えられたら良いなと思い記事にしました。
また、もっと重要なポイントのみを抜き出して書ければ、読みやすい記事になったかと思いますが、なるべく時系列が分かりやすいように書いてみました。
コードはGitHubに上げていますので、そちらを見ればわかる人にとっては、冗長な記事になったかもしれません。
おそらく、誰かの役に立つ記事が出来たかと思います!
また、CDKとPrisma関連で追加の情報があれば記事にしていく予定です。
AWS CDK Advent Calendar 2022に参加してみて
今回の記事は、AWS CDK Advent Calendar 2022で投稿するために書きました。
初めてAdvent Calenderに参加してみたのですが、いつも書くものより気合が入って長くなってしまい、逆に冗長な記事が出来てしまった感があります。
アドベントカレンダーではポエミーな記事も多くなるので、私もその方向の記事が書けるようになりたいな、と思いました!
その道に精通していないとポエミーな記事は書けないので、もっと詳しくならないと...
また、来年CDKでもほかのカレンダーでも、参加してみようと思います!
Discussion