Hono 🔥 爆安SQLite CMS AWS deploy
↑english ver
この記事は、「AWS AmplifyとAWS×フロントエンド #AWSAmplifyJP Advent Calendar 2024」15日目の記事です。
この記事では、AWS Amplifyを利用してAWS上にお手軽にCMSのリソースをSQLiteとHonoでデプロイする方法を紹介します。以下のコマンドでプロジェクトを開始し、この記事を参考に簡単にCMSの構築を試せます🚀
npx create-next-app@latest --yes cd my-app npm create amplify@latest --yes npm i hono sqlite3
1. AWSリソースにSQLiteとHonoのエンドポイントをデプロイ
この記事を参考に、CMSに必要なAWSリソースを作成し、Amplifyから構築する手順を説明します。Amazon Elastic File System上でSQLiteを動かし、LambdaからVPC内にアクセス可能なHonoのエンドポイントを構築します。
1.1. VPC内のFile SystemとLambdaを定義するaws-cdkコードを作成
aws-cdkを使用してAWSリソースを定義します。これはAmplifyのCI/CDから自動的にデプロイされます。以下の3つのリソースファイルをプロジェクトに保存します。
1.2. backend.tsにSQLiteとHonoを動かすカスタムリソースを追加
以下のようにコードを変更して、CMSに必要なAWSリソースがAmplifyのCI/CDを通じてデプロイされるようにします。後々認証機能を実装するため、AWS Cognitoも一緒にデプロイします。
// amplify/backend.ts import { defineBackend } from "@aws-amplify/backend"; import { auth } from "./auth/resource"; import FileSystem from "./custom/FileSystem/resource"; import VpcLambda from "./custom/VpcLambda/resource"; import VpcSubnet from "./custom/VpcSubnet/resource"; const backend = defineBackend({ auth }); const stack = backend.createStack("CustomResources"); const { vpc } = new VpcSubnet(stack, "VpcSubnet"); const { accessPoint } = new FileSystem(stack, "FileSystem", { vpc }); new VpcLambda(stack, "VpcLambda", { vpc, accessPoint });
1.3. GitHubのリポジトリをAmplifyのCI/CDに接続してデプロイ
handler.tsを作成し、一旦保存します。これらの変更をGitHubにプッシュした後、Amplifyのコンソールをポチポチするだけで自動的にデプロイされます。
// handler.ts import sqlite3 from "sqlite3"; const DB_PATH = "/mnt/db/db.sqlite"; const db = new sqlite3.Database(DB_PATH); export const handler = () => { console.log(`Hello SQLite ${sqlite3.VERSION}`); };
2. Next.jsからVPC内のリソースにアクセスする設定
この章では、Next.jsからVPC内のリソースにアクセスするためにAmplifyを接続する方法を説明します。AmplifyとIAMのポリシーと環境変数の設定を行います。
2.1. Next.jsからVPC内のLambdaを呼び出すためのIAMの設定
今週公開したこの記事の3章の手順と同様に、デプロイされたAmplifyとLambdaのArnの情報を使用して、AmplifyとIAMのポリシーを設定します。以下の4つの環境変数を取得します。
# .env.local VPC_AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx VPC_AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx VPC_LAMBDA_AWS_REGION=ap-northeast-1 VPC_LAMBDA_FUNCTION_NAME=amplify-xxxxxxxxxxxxxx-xx-xxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxx
2.2. AWS Amplifyに環境変数を設定してデプロイ
Amplifyコンソールの「Manage environment variables」ページに、取得した4つの環境変数を設定します。
2.3. Lambdaに環境設定を渡すためのamplify.ymlを作成
以下のamplify.ymlをプロジェクトのルートに保存することで、4つの環境変数をサーバーサイドに渡すためのコマンドを追加します。
# amplify.yml version: 1 backend: phases: build: commands: - env | grep -e VPC_AWS_ACCESS_KEY_ID >> .env || true - env | grep -e VPC_AWS_SECRET_ACCESS_KEY >> .env || true - env | grep -e VPC_LAMBDA_AWS_REGION >> .env || true - env | grep -e VPC_LAMBDA_FUNCTION_NAME >> .env || true - npm ci --cache .npm --prefer-offline - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID frontend: phases: build: commands: - npm run build artifacts: baseDirectory: .next files: - '**/*' cache: paths: - .next/cache/**/* - .npm/**/*
3. HonoのVPC Lambdaで動くSQLite Handlerの実装
SQLiteを管理するためのHonoエンドポイントを作成し、Lambdaにデプロイします。TypeScriptでAPIを開発することで、API側の仕様を型情報としてクライアントの開発側に共有できます。
3.1. SQLiteデータベース操作のためのhandler.tsの実装コード
データベースの初期化やデータ操作に必要なエンドポイントをHonoで開発します。HonoのClient機能により、バリデーションにzodを使用してPOST時に型ヒントを指定することができます:
// handler.ts // ... export const app = new Hono(); export const db = new sqlite3.Database(DB_PATH); export const routes = app .get("/init", async (c) => { await run(tableCreationQuery); return c.json({ message: "inited" }); }) .get("/", async (c) => { const page = parseInt(c.req.query("page") || "1"); const q = `SELECT * FROM items ORDER BY created_at DESC LIMIT ? OFFSET ?`; const res = await all<Item[]>(q, 10, 10 * (page - 1)); return c.json(res); }) .post("/", zValidator("json", createSchema), async (c) => { const { title, content } = c.req.valid("json"); const q = `INSERT INTO items (title, content) VALUES (?, ?)`; const id = await run(q, title, content); return c.json({ id }, 201); }) // ... export type AppType = typeof routes; export const handler = handle(app);
3.2. デプロイ前にSQLiteハンドラーをローカルでユニットテスト
HonoのtestClientを使用すると、サーバーとテスト間でTypeScriptの型が共有され、テスト中にエディタ補完が動きます。API側のエンドポイントを変更するとTypeErrorを検出し、エディターの補完によって修正できます。
3.3. SQLiteハンドラーをAWS Consoleからデプロイして機能検証
GitHubに変更をプッシュしてデプロイします。その後、AWSのコンソールからLambdaをテストできます。/init
エンドポイントへリクエストしてから、他の操作を動作確認します。
4. Next.jsのServer Componentで動くLambda Invokerの実装
Next.js側からLambdaを呼び出してVPC内のデータを表示する方法について説明します。Honoのクライアント機能と型定義を利用した開発の容易さも話します。
4.1. VPC内にアクセスするLambda invoker.tsの実装コード
保存した環境変数を使用してLambdaを呼び出す関数を定義します。HonoのClient機能と一緒に使用することで、API側の型情報を再び共有できます。
// invoker.ts import { AppType } from "@/handler"; import { Lambda } from "@aws-sdk/client-lambda"; import { hc } from "hono/client"; const lambda = new Lambda({ region: process.env.VPC_LAMBDA_AWS_REGION!, credentials: { accessKeyId: process.env.VPC_AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.VPC_AWS_SECRET_ACCESS_KEY!, }, }); const customFetch = async (path: RequestInfo | URL, init?: RequestInit) => { const { body, method, headers } = init ?? {}; const payload = { path, body, httpMethod: method, headers: Object.fromEntries(headers as []), isBase64Encoded: false, }; const args = { FunctionName: process.env.VPC_LAMBDA_FUNCTION_NAME, Payload: JSON.stringify(payload), }; const res = await lambda.invoke(args); const buf = Buffer.from(res.Payload!); const obj = JSON.parse(buf.toString()); return new Response(obj.body); }; export const invoker = () => hc<AppType>("", { fetch: customFetch });
4.2. ユニットテストと同じコードでLambda invokeの統合テスト
ユニットテストのtestClient部分をinvokerに変えることで、全く同じコードで統合テストに変えることができます。
4.3. Next.jsのサーバーサイドからVPC内のリソースを操作
Next.jsのServer Component側からLambdaをinvokeしてみて、VPC内のSQLiteのデータを扱えるかをテストしてみます。あとは頑張ってCURDの機能をUI上で実装すれば冒頭のCMSの完成です!
// app/page.tsx
export default async function Home() {
const res = await app.index.$get();
const list = await res.json();
return JSON.stringify(list);
}
Discussion
ここから遊べます!(VPCの数の上限もあるので、今月中にAmplifyから接続を解除するかもです😭)
OpenAI が動かないときのメッセージ表示管理みたいな用途を想定してます(AIサービスを作っているので)