🔥

Hono 🔥 爆安SQLite CMS AWS deploy

2024/12/15に公開
1

https://github.com/tseijp/next-amplify-with-lambda-test

https://github.com/tseijp/note/blob/main/2024-12-15.md

↑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のエンドポイントをデプロイ

https://zenn.dev/nixieminton/articles/dc041d5bc8c09f

この記事を参考に、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つのリソースファイルをプロジェクトに保存します。

https://github.com/tseijp/next-amplify-with-lambda-test/blob/main/amplify/custom/FileSystem/resource.ts

https://github.com/tseijp/next-amplify-with-lambda-test/blob/main/amplify/custom/VpcLambda/resource.ts

https://github.com/tseijp/next-amplify-with-lambda-test/blob/main/amplify/custom/VpcSubnet/resource.ts

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の設定

https://zenn.dev/jp/articles/6a5f2d90d0c6c8#3.-accessing-vpc-lambda-via-aws-amplify

今週公開したこの記事の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

tseitsei

ここから遊べます!(VPCの数の上限もあるので、今月中にAmplifyから接続を解除するかもです😭)
OpenAI が動かないときのメッセージ表示管理みたいな用途を想定してます(AIサービスを作っているので)

https://main.dnomldebieci3.amplifyapp.com/