Skip to main content

Hono 🔥 SQLite CMS AWS deploy

https://zenn.dev/jp/articles/947373b85a3dc1

↑ 日本語版

This article provides a step-by-step guide on deploying an easy-to-setup CMS Resource with SQLite on AWS using Hono, integrated with AWS Amplify.

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

Start your project with the following commands to easily build a CMS based on this article.

npx create-next-app@latest --yes
cd my-app
npm create amplify@latest --yes

1. Deploying SQLite and Hono Endpoints Handling SQLite on AWS Resources

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

Refer to this Zenn article to create the necessary AWS resources for a CMS, and explain the steps to build from Amplify. Run SQLite on Amazon Elastic File System and construct Hono endpoints accessible within a VPC via Lambda.

1.1. Creating aws-cdk Code to Define File System and Lambda in VPC

AWS resources are defined using aws-cdk, which is deployed automatically by Amplify's CI/CD. Save the following three resource files in your project:

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. Updating backend.ts to Construct Custom Resources Running SQLite and Hono

Modify the code as below to ensure the necessary AWS resources for a CMS are deployed via Amplify's CI/CD. AWS Cognito will also be deployed together to implement the authentication later.

// 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. Connecting GitHub Repository to AWS Amplify for Auto-Deployment via CI/CD

Create the handler.ts and save it for now. Once these changes are pushed to GitHub, simply clicking through Amplify's console will auto-deploy:

// 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. Configuring Access to VPC Resources from Deployed Next.js on AWS Amplify

This chapter explains how to connect Amplify to access resources within the VPC from Next.js. Set up Amplify and IAM policies and environmental variables settings.

2.1. Setup IAM to invoke Lambda in VPC from Next.js

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

Follow the same steps as chapter 3 of the recently published article to configure Amplify and IAM policies using information from the deployed Amplify and Lambda's Arn. Retrieve the four environment variables:

# .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. Setting Environment Variables in AWS Amplify and Deploying

Set the acquired four environment variables on the 'Manage environment variables' page of the Amplify console.

2.3. Creating amplify.yml for Passing Environmental Settings to Lambda

Save the following amplify.yml in the project root, adding commands to pass four environment variables to the server side:

# 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. Implementing a SQLite Handler with Hono in a VPC Lambda

Create Hono endpoints to manage SQLite and deploy to Lambda. Developing the API in TypeScript allows sharing API-side specification as type information during client-side development.

3.1. Implementing the handler.ts Code for SQLite Database Operations

Develop endpoints necessary for database initialization and data operations using Hono.Hono's Client feature allows the use of type hints to be specified during POST using zod for validation:

// 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. Unit Testing the SQLite Handler Locally Before Deployment

Use Hono's testClient to share TypeScript types between the server and uni test, and editor completion works during testing, detecting type errors when API endpoints change, and completing corrections through the editor:

3.3. Deploying and Functionally Testing the SQLite Handler from AWS Console

Push changes to GitHub for deployment. You can then test the Lambda from AWS's console, initiating requests to the /init endpoint and verify other operations:

4. Implementing a Lambda Invoker in Next.js Server Components

Discuss methods for invoking Lambda from the Next.js side to display data within the VPC, sharing development ease using Hono's client functionality and type definitions.

4.1. Implementing the Lambda invoker.ts Code to Access Inside the VPC

Define a function to invoke Lambda using stored environment variables; used with Hono's Client, type information on the API side can again be shared.

// 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. Writing Integration Tests for Lambda Invoke Using the Same Code as Unit Tests

Switch the testClient in unit tests to invoker, allowing the same code to be used for integration testing.

4.3. Operate Resources Inside the VPC from Next.js Server-Side

Test accessing SQLite data within the VPC from Next.js Server Components. Just do our best to implement the CURD features on the UI, and the CMS is complete!

// app/page.tsx
export default async function Home() {
const res = await app.index.$get();
const list = await res.json();
return JSON.stringify(list);
}