【CDK】Honoで簡単なCRUD処理のバックエンドAPIを作成してみた(Lambda + API Gateway + RDS(Aurora))
はじめに
コンサルティング部の神野です。
以前にHonoで簡単なCRUDシステムを作って下記記事で紹介させていただきました。
その際にDynamoDBを採用したのですが、RDS(Aurora)を使いたい場合も考慮して実装してみました!
前回の記事
構成するシステム
システム構成図
下記構成をCDKで構築し、Honoの実装含めて全てTypeScriptで実装します。
- Honoの実行環境はLambdaを選択
- LambdaはAPI Gateway経由で実行
- データソースとしてはAurora MySQL Serverlessを選択
補足
今回はRDS Proxyを使用しておりません。理由としてはあくまで接続検証が目的で瞬間的にしか使用しないためです。
RDS Proxyを使用するかどうか検討する記事がございますので、ワークロードに応じてご検討ください。
機能
Todoアプリケーションを作成します。データソースをRDS(Aurora)とする簡単なCRUD処理を行うバックエンドのAPIサーバーを作成していきます。
Todosテーブル
Todosテーブルとしては下記レイアウトは下記想定で実装を進めていきます。シンプルなテーブルを題材としていきます。
テーブル定義
論理名 | 物理名 | 型 | 備考 |
---|---|---|---|
ID | id | VARCHAR | プライマリーキー |
タイトル | title | VARCHAR | NOT NULL |
完了フラグ | completed | BOOLEAN | デフォルト: false |
期日 | dueDate | TIMESTAMP | NULL許容 |
ユーザーID | userId | VARCHAR | |
作成日 | createdAt | TIMESTAMP | デフォルト: 現在日時 |
更新日 | updatedAt | TIMESTAMP | 更新時に自動更新 |
構築
全てのコードはgithubにアップロードしているので、必要に応じてご参照ください。
事前準備
TypeScriptの実行環境としてBun、環境構築にCDKを使って実装を進めていくので事前にライブラリをインストールしておきます。
またMySQLをローカルで起動するためにDockerも併せてインストールしておきます。使用したバージョンは下記となります。
- CDK・・・2.176.0
- Docker・・・27.1.1-rd
- Bun・・・1.1.26
- Node・・・v20.16.0
任意のフォルダでCDKプロジェクトを作成します。
cdk init --language=typescript
作成後はプロジェクトで必要なライブラリをインストールします。
# PrismaとAWS SDKの関連ライブラリ
bun install prisma@6.3.0 @prisma/client@6.3.0 @types/aws-lambda@8.10.147
# Honoとその関連ライブラリ
bun install hono@4.6.2 @hono/zod-validator@0.2.2 zod@3.23.8
# その他の必要なライブラリ
bun install uuid@10.0.0
# 開発用ライブラリ
bun install --save-dev @types/uuid@10.0.0 dotenv-cli@8.0.0
- prisma@6.3.0
- @prisma/client@6.3.0
- @types/aws-lambda@8.10.147
- hono: 4.6.2
- @hono/zod-validator: 0.2.2
- dotenv-cli@8.0.0
- uuid: 10.0.0
- zod: 3.23.8
補足:Bunについて
今回はTypeScriptの実行環境として、Bunを使用しました。使用感は下記が素晴らしく採用しました
-
TypeScriptのままコードを実行できる(トランスパイルが不要!!)
実行例# example bun run --hot server.ts
-
Honoの実行環境としてサポートされている
-
パッケージマネージャーとしても使用可能で、ライブラリインストールも早い
ディレクトリ構成
下記ディレクトリ構成で実装を進めていきます。
.
├── README.md # プロジェクトの説明文書
├── bin
│ └── blog-sample-hono-rds.ts # CDKアプリケーションのエントリーポイント
├── cdk.json # CDKの設定ファイル
├── compose.yaml # ローカル開発用のDocker設定
├── .env # 環境変数設定ファイル(Githubにはコミットしない)
├── lambda # Lambda関数のソースコード
│ ├── index.ts # Lambda関数のエントリーポイント
│ └── src
│ ├── api
│ │ └── todos
│ │ └── todos.ts # Todoに関するAPI実装
│ ├── app.ts # Honoアプリケーションの主要設定
│ ├── .env.local # ローカル開発環境用の環境変数(Githubにはコミットしない)
│ └── prisma # Prisma関連のファイル
│ ├── schema.prisma # Prismaスキーマ定義
│ └── migrations/ # データベースマイグレーションファイル
├── lib
│ └── blog-sample-hono-rds-stack.ts # CDKスタックの定義
├── package.json # プロジェクトの依存関係と設定
└── tsconfig.json # TypeScriptの設定ファイル
Hono部分
ローカルDB
今回はAurora MySQLを使用するので、ローカルでもMySQLを使用します。
compose.yml
ファイルを作成して、環境を作成します。
version: '3.8'
services:
db:
image: mysql:8.0
container_name: mysql_container
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: todo_db
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
作成したらdocker compose
コマンドでMySQLを立ち上げます。
docker compose up -d
[+] Running 1/1
✔ Container mysql_container Started 0.6s
Prisma
今回はORMとしてPrismaを使用します。
使用するに際して、まずinit
コマンドを実行して初期化します。
また、初期化が完了したら元のディレクトリに戻ります。
# ディレクトリ移動
cd lambda/src
bunx prisma init --datasource-provider mysql
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run prisma db pull to turn your database schema into a Prisma schema.
3. Run prisma generate to generate the Prisma Client. You can then start querying your database.
4. Tip: Explore how you can extend the ORM with scalable connection pooling, global caching, and real-time database events. Read: https://pris.ly/cli/beyond-orm
More information in our documentation:
https://pris.ly/d/getting-started
# 初期化が完了したら元のディレクトリへ
cd ../..
実行後はprisma
フォルダおよびフォルダ内にschema.prisma
ファイルが作成されています。
schema
ファイル内にテーブルの定義を追記します。
generator client {
provider = "prisma-client-js"
binaryTargets = ["linux-arm64-openssl-3.0.x", "darwin-arm64"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Todo {
id String @id @default(uuid())
userId String
title String
completed Boolean @default(false)
dueDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
作成したタイミングで、ローカル開発用の環境変数である.env.local
を作成し、ローカル開発用の環境変数DATABASE_URL
を定義します。compose.yml
で定義したユーザーやパスワードなどの情報を元にURLを記載します。
DATABASE_URL=mysql://root:rootpassword@localhost:3306/todo_db
環境変数を作成したタイミングで、開発環境のDBにマイグレーションを実施します。
.env.local
を読みこむために、dotenv
を使用します。
毎回マイグレーションコマンドを実行するためにディレクトリ移動するのは少し手間なので、package.json
にマイグレーションコマンド(migrate:dev
)を追加し実行します。
{
///省略
...
"scripts": {
// 追加
"migrate:dev": "cd lambda/src && bunx dotenv -e .env.local bunx prisma migrate dev"
},
...
}
追加後はコマンドを実行します。
マイグレーションの名前を求められるので、任意の名前でOKです。
今回はinit
としました。
bunx run migrate:dev
✔ Enter a name for the new migration: … init
Applying migration `20250203080509_init`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20250203080509_init/
└─ migration.sql
コマンドが無事完了したら、該当のフォルダにSQLが存在しているか確認します。
-- CreateTable
CREATE TABLE `Todo` (
`id` VARCHAR(191) NOT NULL,
`userId` VARCHAR(191) NOT NULL,
`title` VARCHAR(191) NOT NULL,
`completed` BOOLEAN NOT NULL DEFAULT false,
`dueDate` DATETIME(3) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
問題なくマイグレーションに成功してSQL作成できていますね!
具体的な処理実装に移ります。
Todo処理部分
TodoアプリケーションのCRUD操作を実装しています。
- Zodを使用してリクエストのバリデーションを行っています。
- Prismaのドキュメントクライアントを使用してデータの操作を行っています。
- 各エンドポイント(POST, GET, PUT, DELETE)でTodoの作成、取得、更新、削除の処理を実装しています。
CRUD処理一覧
操作 | HTTPメソッド | エンドポイント | 説明 | リクエストボディ | レスポンス |
---|---|---|---|---|---|
Create | POST | /api/todos | 新しいTodoを作成 | userId, title, completed, dueDate(optional) | 201 Created, 作成されたTodo |
Read (All) | GET | /api/todos/user/:userId | 特定ユーザーの全Todoを取得 | - | 200 OK, Todoの配列 |
Read (Single) | GET | /api/todos/:id | 特定のTodoを取得 | - | 200 OK, Todoオブジェクト or 404 Not Found |
Update | PUT | /api/todos/:id | Todoを更新 | title(optional), completed(optional), dueDate(optional) | 200 OK, 更新されたTodo |
Delete | DELETE | /api/todos/:id | Todoを削除 | - | 200 OK, 削除成功メッセージ |
// lambda/index.ts
import { Hono } from "hono";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient();
// カスタムZodスキーマ for YYYY-MM-DD形式の日付
const dateSchema = z.string().refine(
(val) => {
return /^\d{4}-\d{2}-\d{2}$/.test(val) && !isNaN(Date.parse(val));
},
{
message: "Invalid date format. Use YYYY-MM-DD",
}
);
// Zodスキーマの定義
const TodoSchema = z.object({
userId: z.string().min(1),
title: z.string().min(1).max(100),
completed: z.boolean(),
dueDate: dateSchema.optional(),
});
const TodoUpdateSchema = TodoSchema.partial().omit({ userId: true });
const todos = new Hono()
.post("/", zValidator("json", TodoSchema), async (c) => {
const validatedData = c.req.valid("json");
try {
const todo = await prisma.todos.create({
data: {
...validatedData,
dueDate: validatedData.dueDate ? new Date(validatedData.dueDate) : null,
},
});
return c.json({ message: "Todo created successfully", todo }, 201);
} catch (error) {
console.error(error);
return c.json({ error: "Failed to create todo" }, 500);
}
})
.get("/user/:userId", async (c) => {
const userId = c.req.param("userId");
try {
const todos = await prisma.todos.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
});
return c.json(todos);
} catch (error) {
console.error(error);
return c.json({ error: "Failed to retrieve todos" }, 500);
}
})
.get("/:id", async (c) => {
const id = c.req.param("id");
try {
const todo = await prisma.todos.findUnique({
where: { id },
});
if (!todo) {
return c.json({ error: "Todo not found" }, 404);
}
return c.json(todo);
} catch (error) {
console.error(error);
return c.json({ error: "Failed to retrieve todo" }, 500);
}
})
.put("/:id", zValidator("json", TodoUpdateSchema), async (c) => {
const id = c.req.param("id");
const validatedData = c.req.valid("json");
try {
const todo = await prisma.todos.update({
where: { id },
data: {
...validatedData,
dueDate: validatedData.dueDate ? new Date(validatedData.dueDate) : undefined,
},
});
return c.json(todo);
} catch (error) {
console.error(error);
return c.json({ error: "Failed to update todo" }, 500);
}
})
.delete("/:id", async (c) => {
const id = c.req.param("id");
try {
await prisma.todos.delete({
where: { id },
});
return c.json({ message: "Todo deleted successfully" });
} catch (error) {
console.error(error);
return c.json({ error: "Failed to delete todo" }, 500);
}
});
export { todos };
app.ts
サーバー起動処理部分です。Middlewareを使用して、ログとBasic認証を実装します。Honoは標準で提供されているので簡単で素晴らしいですね!!
import { Hono } from "hono";
import { logger } from "hono/logger";
import { basicAuth } from "hono/basic-auth";
import { todos } from "./api/todos/todos";
const app = new Hono();
// ログの設定
app.use("*", logger());
//Basic認証の設定
app.use(
"*",
basicAuth({
username: process.env.BASIC_USERNAME ? process.env.BASIC_USERNAME : "a",
password: process.env.BASIC_PASSWORD ? process.env.BASIC_PASSWORD : "a",
})
);
app.route("/api/todos", todos);
export default app;
index.ts
Lambda関数のエントリーポイントとなるファイルです。
- HonoアプリケーションをAWS Lambda用のハンドラーでラップしています。
- これにより、HonoアプリケーションをLambda関数として実行できるようになります。
import { handle } from 'hono/aws-lambda'
import app from './src/app'
// index.ts で定義された純粋なHTTPサーバをAWS Lambda用のアダプタでラップしてハンドラとしてエクスポート
export const handler = handle(app)
migration.ts
CDKのカスタムリソースとしてPrismaのマイグレーションを実施するLambda関数用のファイルとなります。
マイグレーションの実装は下記を参考にさせていただいております。(実装はほぼ同一のものです)
import { execSync } from "child_process";
import {
CdkCustomResourceHandler,
CdkCustomResourceResponse,
} from "aws-lambda";
import { v4 as uuidv4 } from 'uuid';
export const handler: CdkCustomResourceHandler = async (event) => {
const physicalResourceId = event.ResourceProperties.physicalResourceId ?? uuidv4();
if (event.RequestType === "Delete") {
return {
PhysicalResourceId: physicalResourceId,
};
}
if (!process.env.DATABASE_URL) {
throw new Error("DB_CONNECTION is not set");
}
return new Promise<CdkCustomResourceResponse>((resolve) => {
setInterval(() => {
try {
const stdout = execSync(`prisma migrate deploy`, {
env: {
...process.env,
DATABASE_URL: process.env.DATABASE_URL,
}
});
console.log(stdout.toString());
resolve({
PhysicalResourceId: physicalResourceId,
});
} catch (error) {
console.error("Migration is failed %s, will be retry...", error);
}
}, 10 * 1000);
});
};
このLambda関数はコンテナで実行するために、Dockerfileを作成します。
FROM node:20 AS builder
WORKDIR /app
RUN npm init -y
RUN npm install -g typescript
RUN npm install --save-dev @types/node @types/aws-lambda
COPY migration.ts ./
RUN tsc migration.ts
FROM public.ecr.aws/lambda/nodejs:20
COPY /app/migration.js ${LAMBDA_TASK_ROOT}/
COPY src/prisma ${LAMBDA_TASK_ROOT}/
RUN npm i -g prisma
CMD [ "migration.handler" ]
package.json
サーバー起動用のコマンドを追加しておきます。設定することで簡単にbun run dev
でサーバーを起動できます。
{
///省略
...
"scripts": {
// 追加
"dev": "bunx dotenv -e ./lambda/src/.env.local bun run lambda/src/app.ts"
},
...
}
また、BASIC認証用にIDとパスワードも.env.local
に追記します。
BASIC_USERNAME=test
BASIC_PASSWORD=test
動作確認
APIサーバを起動します。
bun run dev
サーバーが立ち上がったので各種リクエストを送ってみます。
Authorization
ヘッダーには.env.local
で設定したユーザー情報を設定します。
# Create: 新しいTodoを作成
curl -i -X POST "http://localhost:3000/api/todos" \
-H "Content-Type: application/json" \
-H "Authorization: Basic <your-information>" \
-d '{"userId": "user123", "title": "新しいタスク", "completed": false, "dueDate": "2023-12-31"}'
HTTP/1.1 201 Created
Content-Type: application/json
Date: Mon, 03 Feb 2025 08:29:18 GMT
Content-Length: 282
{"message":"Todo created successfully","todo":{"id":"01JK5EV8SYA8MSE2THNMS2HWXJ","userId":"user123","title":"新しいタスク","completed":false,"dueDate":"2023-12-31T00:00:00.000Z","description":null,"createdAt":"2025-02-03T08:29:19.038Z","updatedAt":"2025-02-03T08:29:19.038Z"}}
# Read
curl -i -X GET "http://localhost:3000/api/todos/user/user123" \
-H "Authorization: Basic <your-information>"
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 03 Feb 2025 08:29:49 GMT
Content-Length: 237
[{"id":"01JK5EV8SYA8MSE2THNMS2HWXJ","userId":"user123","title":"新しいタスク","completed":false,"dueDate":"2023-12-31T00:00:00.000Z","description":null,"createdAt":"2025-02-03T08:29:19.038Z","updatedAt":"2025-02-03T08:29:19.038Z"}]
# Update
curl -i -X PUT "http://localhost:3000/api/todos/01JK5EV8SYA8MSE2THNMS2HWXJ" \
-H "Content-Type: application/json" \
-H "Authorization: Basic <your-information>" \
-d '{"title": "更新されたタスク", "completed": true}'
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 03 Feb 2025 08:30:35 GMT
Content-Length: 240
{"id":"01JK5EV8SYA8MSE2THNMS2HWXJ","userId":"user123","title":"更新されたタスク","completed":true,"dueDate":"2023-12-31T00:00:00.000Z","description":null,"createdAt":"2025-02-03T08:29:19.038Z","updatedAt":"2025-02-03T08:30:36.144Z"}
# Delete
curl -i -X DELETE "http://localhost:3000/api/todos/01JK5EV8SYA8MSE2THNMS2HWXJ" \
-H "Authorization: Basic <your-information>"
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 03 Feb 2025 08:31:29 GMT
Content-Length: 39
{"message":"Todo deleted successfully"}
CRUD処理が適切に動いていて、いい感じですね!
後はバリデーションチェックが機能しているか、認証の失敗パターンもチェックしてみます!
# 認証エラー
curl -i -X GET "http://localhost:3000/api/todos/user/user123" \
-H "Authorization: Basic test"
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Secure Area"
content-type: application/octet-stream
Date: Sat, 01 Feb 2025 16:13:22 GMT
Content-Length: 12
Unauthorized
# 必須フィールドの欠落
curl -i -X POST "http://localhost:3000/api/todos" \
-H "Content-Type: application/json" \
-H "Authorization: Basic <your-information>" \
-d '{"title": "新しいタスク", "completed": false}'
HTTP/1.1 400 Bad Request
Content-Type: application/json
Date: Sat, 01 Feb 2025 16:13:54 GMT
Content-Length: 162
{"success":false,"error":{"issues":[{"code":"invalid_type","expected":"string","received":"undefined","path":["userId"],"message":"Required"}],"name":"ZodError"}}
こちらも問題なく実装できていますね!エラー時や認証失敗時のレスポンスを作り込まずともフレームワーク側で自動で返却してもらえるのは嬉しいですね!!!
一通りローカルで実装が完了したので、AWS上にデプロイしていきます。
環境構築部分
環境構築部分はCDKで実装していきます。環境変数は.env
ファイルをプロジェクト直下に作成し、下記を定義しておきます。
環境変数
- Basic認証のユーザー名とパスワード
- RDSの情報
- データベース名:
todo_db
- ユーザー名:任意のユーザー名
- パスワード:任意のパスワード
- データベース名:
BASIC_USERNAME=XXX
BASIC_PASSWORD=YYY
RDS_DATABASE_NAME=todo_db
RDS_USERNAME=ZZZ
RDS_PASSWORD=DDD
CDKコード
下記リソースを作成します。
- VPC
- RDSとLambdaを配置するため
- セキュリティグループ
- Aurora用セキュリティグループ
- LambdaからのTCP 3306アクセスを許可
- Lambda用セキュリティグループ
- Aurora用セキュリティグループ
- Aurora MySQL
- Serverless v2で動作するAurora MySQLクラスター
- ACU0.5 ~ 1
- Serverless v2で動作するAurora MySQLクラスター
- Lambda関数
- Hono実行用Lambda
- Prismaクライアントを利用
- バンドル時にNative Query Engineやスキーマ定義をコピー
- 環境変数からBasic認証情報とDATABASE_URLを設定
- Prismaクライアントを利用
- マイグレーション用Lambda(Dockerイメージ使用)
- デプロイ時にPrismaのマイグレーションを自動実行
- Hono実行用Lambda
- API Gateway
- Honoアプリを公開するエンドポイント
- Lambda関数をラップ
- Honoアプリを公開するエンドポイント
- カスタムリソース
- マイグレーション用Lambdaをトリガー
- 最新のマイグレーションを実行する仕組み
- マイグレーション用Lambdaをトリガー
コードの全体は長いので省略します。
コード全体
import * as cdk from "aws-cdk-lib";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigw from "aws-cdk-lib/aws-apigateway";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as rds from "aws-cdk-lib/aws-rds";
import * as dotenv from "dotenv";
import path = require("path");
import * as fs from "fs";
import * as cr from "aws-cdk-lib/custom-resources";
// 環境変数の読み込み
dotenv.config();
// 必須の環境変数をチェックする関数
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Environment variable ${name} is required`);
}
return value;
}
export class BlogSampleHonoRdsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 環境変数から値を取得
const databaseName = requireEnv("RDS_DATABASE_NAME");
const username = requireEnv("RDS_USERNAME");
const password = requireEnv("RDS_PASSWORD");
// VPCの作成
const vpc = new ec2.Vpc(this, "TodoAppVpc", {
maxAzs: 2,
natGateways: 0,
});
// RDSのセキュリティグループ
const dbSecurityGroup = new ec2.SecurityGroup(this, "DbSecurityGroup", {
vpc,
description: "Security group for Aurora database",
allowAllOutbound: true,
});
// Aurora MySQL クラスターの作成
const cluster = new rds.DatabaseCluster(this, "TodoDatabase", {
engine: rds.DatabaseClusterEngine.auroraMysql({
version: rds.AuroraMysqlEngineVersion.VER_3_07_1,
}),
serverlessV2MinCapacity: 0.5,
serverlessV2MaxCapacity: 1,
credentials: {
username: username,
password: cdk.SecretValue.unsafePlainText(password),
},
writer: rds.ClusterInstance.serverlessV2("writer", {
autoMinorVersionUpgrade: false,
instanceIdentifier: "hono-db-writer",
publiclyAccessible: false,
}),
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
securityGroups: [dbSecurityGroup],
defaultDatabaseName: databaseName,
// バックアップ保持期間(日数)
backup: {
retention: cdk.Duration.days(7),
},
// 削除保護(本番環境では true にすることを推奨)
removalPolicy: cdk.RemovalPolicy.DESTROY,
enableDataApi: true,
});
// Lambda関数のセキュリティグループ
const lambdaSecurityGroup = new ec2.SecurityGroup(
this,
"LambdaSecurityGroup",
{
vpc,
description: "Security group for Lambda function",
allowAllOutbound: true,
}
);
// セキュリティグループ間の通信許可
dbSecurityGroup.addIngressRule(
lambdaSecurityGroup,
ec2.Port.tcp(3306),
"Allow Lambda to access Aurora MySQL"
);
// Lambda関数の作成
const honoLambda = new NodejsFunction(this, "lambda", {
entry: "lambda/index.ts",
handler: "handler",
runtime: lambda.Runtime.NODEJS_20_X,
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
securityGroups: [lambdaSecurityGroup],
environment: {
BASIC_USERNAME: requireEnv("BASIC_USERNAME"),
BASIC_PASSWORD: requireEnv("BASIC_PASSWORD"),
DATABASE_URL: `mysql://${username}:${password}@${cluster.clusterEndpoint.hostname}:${cluster.clusterEndpoint.port}/${databaseName}`,
},
timeout: cdk.Duration.seconds(30),
memorySize: 1024,
architecture: lambda.Architecture.ARM_64,
bundling: {
// commandHooksでインストール前、バンドル前、後にコマンドを組み込める
commandHooks: {
beforeInstall(inputDir: string, outputDir: string): string[] {
return [``];
},
beforeBundling(inputDir: string, outputDir: string): string[] {
return [``];
},
afterBundling(inputDir: string, outputDir: string): string[] {
return [
// クエリエンジンを追加
`cp ${inputDir}/node_modules/.prisma/client/libquery_engine-linux-arm64-* ${outputDir}`,
// スキーマ定義を追加
`cp ${inputDir}/lambda/src/prisma/schema.prisma ${outputDir}`,
];
},
},
},
});
// API Gatewayの作成
const apiGw = new apigw.LambdaRestApi(this, "honoApi", {
handler: honoLambda,
proxy: true,
});
// マイグレーション用のLambda関数
const migrationLambda = new lambda.DockerImageFunction(
this,
"MigrationLambda",
{
code: lambda.DockerImageCode.fromImageAsset("lambda", {
file: "Dockerfile",
cmd: ["migration.handler"],
}),
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
securityGroups: [lambdaSecurityGroup],
environment: {
DATABASE_URL: `mysql://${username}:${password}@${cluster.clusterEndpoint.hostname}:${cluster.clusterEndpoint.port}/${databaseName}`,
},
timeout: cdk.Duration.minutes(15),
memorySize: 1024,
architecture: lambda.Architecture.ARM_64,
}
);
// 最新のマイグレーションファイル名を取得
const migrationsDir = path.join(
__dirname,
"../lambda/src/prisma/migrations"
);
const latestMigration = fs
.readdirSync(migrationsDir)
.filter((file) => file !== "migration_lock.toml")
.sort((a, b) => b.localeCompare(a))[0];
console.log(latestMigration);
// カスタムリソースプロバイダーの作成
const provider = new cr.Provider(this, "MigrationProvider", {
onEventHandler: migrationLambda,
});
// カスタムリソースの作成
const migrationCustomResource = new cdk.CustomResource(
this,
"MigrationCustomResource",
{
serviceToken: provider.serviceToken,
properties: {
latestMigration: latestMigration,
},
}
);
// 明示的な依存関係の追加
migrationCustomResource.node.addDependency(cluster);
// 出力の設定
new cdk.CfnOutput(this, "ApiEndpoint", {
value: apiGw.url,
description: "API Gateway endpoint URL",
});
}
}
デプロイ
コードも書けたので、デプロイします。
Auroraのデプロイに時間がかかるため、15分ほどかかります。
cdk deploy
✅ BlogSampleHonoRdsStack
✨ Deployment time: 903.1s
Outputs:
BlogSampleHonoRdsStack.ApiEndpoint = https://<your-endpoint>
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:<your-account>:stack/BlogSampleHonoRdsStack/xxx
✨ Total time: 925.12s
問題なくデプロイできたので、出力されたエンドポイントにリクエストを送ってみます。
動作確認
Authorizationヘッダーには.env
に記載した認証情報を設定します。
# Create: 新しいTodoを作成
curl -i -X POST "https://your-endopoint/api/todos" \
-H "Content-Type: application/json" \
-H "Authorization: Basic <your-information>" \
-d '{"userId": "user123", "title": "新しいタスク", "completed": false, "dueDate": "2023-12-31"}'
HTTP/2 201
content-type: application/json
content-length: 282
date: Mon, 03 Feb 2025 07:38:33 GMT
x-amzn-trace-id: Root=1-67a07278-034fea4b36f4d3f100add63f;Parent=35912d6a5ea32718;Sampled=0;Lineage=1:d9c01db3:0
x-amzn-requestid: 3caa63ab-8868-40f5-94ff-87c02707f0d8
x-amz-apigw-id: FZbS6Gc4tjMEZjQ=
x-cache: Miss from cloudfront
via: 1.1 77c20654dd474081d033f27ad1b56e1e.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT51-C4
x-amz-cf-id: T6rcuZ3hzxTGK7qXqsi5H7TrC-Y7Wf-vO_qFWBW7cW2lbveiqc3qxQ==
{"message":"Todo created successfully","todo":{"id":"01JK5BYAQXSM6B74XX338WFBQH","userId":"user123","title":"新しいタスク","completed":false,"dueDate":"2023-12-31T00:00:00.000Z","description":null,"createdAt":"2025-02-03T07:38:33.596Z","updatedAt":"2025-02-03T07:38:33.596Z"}}
curl -i -X GET "https://your-endopoint/api/todos/user/user123" \
-H "Authorization: Basic <your-information>"
HTTP/2 200
content-type: application/json
content-length: 237
date: Mon, 03 Feb 2025 07:40:15 GMT
x-amzn-trace-id: Root=1-67a072de-7287eadb3ca4eeb039b54059;Parent=6e7ab73aca882002;Sampled=0;Lineage=1:d9c01db3:0
x-amzn-requestid: 619b11fc-89d2-44a4-95f7-13e6e23ff51e
x-amz-apigw-id: FZbi5EyztjMESmw=
x-cache: Miss from cloudfront
via: 1.1 70679ce15d5e20423e4b28a0e958e480.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P3
x-amz-cf-id: HfwXF8o9KYi5fEluITrUrNKSlOybB-pMTpOBF067emXTfAIt7N5U8Q==
[{"id":"01JK5BYAQXSM6B74XX338WFBQH","userId":"user123","title":"新しいタスク","completed":false,"dueDate":"2023-12-31T00:00:00.000Z","description":null,"createdAt":"2025-02-03T07:38:33.596Z","updatedAt":"2025-02-03T07:38:33.596Z"}]%
AWS上にデプロイしても問題なく実行できていますね!!
念の為、コンソール上からもレコードが登録されているか確認してみます。
レコード確認
-
コンソール上から
クエリエディタ
を選択し下記情報を入力しデータベースに接続します
ボタンを押下- データーベースインスタンスまたはクラスター:今回作成したクラスター
- データベースユーザー名:
.env
に記載したユーザー名 - データベースパスワード:
.env
に記載したパスワード - データベースまたはスキーマの名前:
todo_db
-
select * from todo_db.todos
をエディタに入力し、実行ボタン
を押下
しっかりと先程登録したレコードが返却されていますね!!
おわりに
RDSを使用したHonoで実装するバックエンド処理はいかがでしたでしょうか?
DynamoDB版よりは少し手間な箇所もありますが、お手軽にバックエンドサーバーを作れるかと思います!
最後までご覧いただきありがとうございました!