SonikをAWS Lambda(+ Functions URL)で動かしてみる
Sonikは、Honoのメンテナであるyusukeさんが開発しているメタフレームワークであり、Next.jsのようなファイルベースのルーティングやHonoを使用したAPIの実装が可能です。AWS CDKを使用してSonikアプリをデプロイする方法を紹介しています。SonikのセットアップやAWS Lambda向けの調整、S3の利用方法なども解説されています。ただし、LambdaからS3の画像を配信する部分には問題があり、今回はS3にリダイレクトさせた形でデプロイされています。
目次
Sonikは、Honoのメンテナのyusukeさんが現在開発中のメタフレームワークです。
https://www.npmjs.com/package/sonik
Next.jsのようなファイルベースルーティングや、HonoをつかったAPIの実装なども可能とのことで、Next.jsがToo muchなケースに使えるのではないかと思い、試してみました。
AWS CDKのセットアップ
Cloudflareへのデプロイはシンプルに終わりそうな予感がしたので、あえてAWSにデプロイします。
今回もCDKでリソース定義を行いましょう。
% mkdir my-sonik-app
% cd my-sonik-app
% cdk init app -l typescript
lib/cdk-stack.ts
にLambdaとS3を定義します。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';
import { Function, Runtime, Code, FunctionUrlAuthType } from 'aws-cdk-lib/aws-lambda';
import { BlockPublicAccess, Bucket } from 'aws-cdk-lib/aws-s3';
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import path = require('path');
export class CdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const websiteBucket = new Bucket(this, 'SonikStaticAssets', {
websiteIndexDocument: 'index.html',
publicReadAccess: true,
blockPublicAccess: BlockPublicAccess.BLOCK_ACLS,
});
new BucketDeployment(this, 'DeployWebsite', {
sources: [Source.asset('./sonik-app/public')],
destinationBucket: websiteBucket,
});
const fn = new Function(this, 'MyFunction', {
runtime: Runtime.NODEJS_18_X,
handler: 'lambda.handler',
code: Code.fromAsset(path.join(__dirname, '../sonik-app/dist')),
environment: {
S3_BUCKET_URL: websiteBucket.bucketWebsiteUrl,
},
});
fn.addFunctionUrl({
authType: FunctionUrlAuthType.NONE
})
}
}
S3が出てくるのは、faviconや画像・CSSファイルなどの保存と配信に利用するためです。
そのため、BucketDeployments
も定義し、デプロイ時にSonik内の静的ファイルをアップロードします。
Sonikアプリのセットアップ
インフラの準備ができたので、Sonikをセットアップします。
% npx degit yusukebe/sonik/examples/basic sonik-app
% npm --prefix sonik-app install
% npm install -D @types/aws-lambda
この方法は、v1リリース時に変わる可能性がありますので、2024年以降に読まれる方はご注意ください。
[Checkpoint] ディレクトリ構造
ここまでで、プロジェクトのディレクトリ構造はだいたいこんな感じになります。
% tree -L 2 -I node_modules -I cdk.out
.
├── README.md
├── bin
│ └── cdk.ts
├── cdk.json
├── jest.config.js
├── lib
│ └── cdk-stack.ts
├── package-lock.json
├── package.json
├── sonik-app
│ ├── _worker.ts
│ ├── app
│ ├── dist
│ ├── lambda.ts
│ ├── package.json
│ ├── public
│ ├── tsconfig.json
│ ├── vite.config.ts
│ └── yarn.lock
├── test
│ └── cdk.test.ts
└── tsconfig.json
7 directories, 16 files
SonikをAWS Lambda向けに調整する
続いてSonikのコードをAWS Lambda向けに編集しましょう。
エントリーポイントlambda.ts
の作成
エントリーポイントを_worker.ts
のままにしても良いのですが、今回は別途作成します。
import { createApp } from 'sonik'
import { handle } from "hono/aws-lambda"
import { Handler } from 'aws-lambda'
const app = createApp()
export const handler: Handler = handle(app)
export default app
export
の方法が少し変わっているくらいですね。
画像などをS3から配信するためのmiddlewareを用意
続いてmiddlewareを追加します。これは静的ファイルへのアクセスを処理するためのものです。
const getMimeType = (filename: string): string | undefined => {
const regexp = /\.([a-zA-Z0-9]+?)$/
const match = filename.match(regexp)
if (!match) return
let mimeType = mimes[match[1]]
if ((mimeType && mimeType.startsWith('text')) || mimeType === 'application/json') {
mimeType += '; charset=utf-8'
}
return mimeType
}
const mimes: Record<string, string> = {
aac: 'audio/aac',
abw: 'application/x-abiword',
arc: 'application/x-freearc',
avi: 'video/x-msvideo',
azw: 'application/vnd.amazon.ebook',
bin: 'application/octet-stream',
bmp: 'image/bmp',
bz: 'application/x-bzip',
bz2: 'application/x-bzip2',
csh: 'application/x-csh',
css: 'text/css',
csv: 'text/csv',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
eot: 'application/vnd.ms-fontobject',
epub: 'application/epub+zip',
gz: 'application/gzip',
gif: 'image/gif',
htm: 'text/html',
html: 'text/html',
ico: 'image/x-icon',
ics: 'text/calendar',
jar: 'application/java-archive',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
js: 'text/javascript',
json: 'application/json',
jsonld: 'application/ld+json',
map: 'application/json',
mid: 'audio/x-midi',
midi: 'audio/x-midi',
mjs: 'text/javascript',
mp3: 'audio/mpeg',
mpeg: 'video/mpeg',
mpkg: 'application/vnd.apple.installer+xml',
odp: 'application/vnd.oasis.opendocument.presentation',
ods: 'application/vnd.oasis.opendocument.spreadsheet',
odt: 'application/vnd.oasis.opendocument.text',
oga: 'audio/ogg',
ogv: 'video/ogg',
ogx: 'application/ogg',
opus: 'audio/opus',
otf: 'font/otf',
png: 'image/png',
pdf: 'application/pdf',
php: 'application/php',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
rar: 'application/vnd.rar',
rtf: 'application/rtf',
sh: 'application/x-sh',
svg: 'image/svg+xml',
swf: 'application/x-shockwave-flash',
tar: 'application/x-tar',
tif: 'image/tiff',
tiff: 'image/tiff',
ts: 'video/mp2t',
ttf: 'font/ttf',
txt: 'text/plain',
vsd: 'application/vnd.visio',
wav: 'audio/wav',
weba: 'audio/webm',
webm: 'video/webm',
webp: 'image/webp',
woff: 'font/woff',
woff2: 'font/woff2',
xhtml: 'application/xhtml+xml',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
xml: 'application/xml',
xul: 'application/vnd.mozilla.xul+xml',
zip: 'application/zip',
'3gp': 'video/3gpp',
'3g2': 'video/3gpp2',
'7z': 'application/x-7z-compressed',
}
app.use('/static/*', async (_c, next) => {
if (_c.finalized) {
await next()
}
const url = new URL(_c.req.url)
const bucketURL = process?.env?.S3_BUCKET_URL
//const bucketName = process?.env?.S3_BUCKET_NAME
if (bucketURL) {
const mimeType = getMimeType(url.pathname)
if (mimeType && mimeType.startsWith('image')) {
return _c.redirect(`${bucketURL}${url.pathname}`, 302)
}
const response = await fetch(`${bucketURL}${url.pathname}`)
if (response.ok) {
const body = await response.text()
const mimeType = getMimeType(url.pathname)
if (mimeType) {
_c.header('Content-Type', mimeType)
}
return _c.body(body)
}
}
await next()
})
画像などもS3からGetObject
して返す方がよい気はしています。
が、今回はとりあえず動かすことを目標として、簡単に済むリダイレクトを選びました。
また、getMimeType
はHonoのソースコードにあったものを持ってきました。
Viteの設定を変更
最後にViteの設定を変更します。
import { defineConfig } from 'vite'
import sonik from 'sonik/vite'
export default defineConfig({
plugins: [
sonik({
entry: './lambda.ts',
minify: true,
}),
],
// 追加ここから
build: {
rollupOptions: {
output: {
format: 'commonjs'
}
}
}
// 追加ここまで
})
Lambda側でcommonjs
でないとエラーになるケースがあったので追加しています。
今後のLambda側のアップデートなどで、対応が不要になることもあるかもしれません。
デプロイ方法
実際にデプロイしてみましょう。
Sonikアプリをビルドする
今回の方法では、CDK側でアプリのビルドを行いません。
そのため。前もってSonik側でビルドが必要です。
% npm --prefix ./sonik-app run build
ViteのビルドをCDKから実行する方法があるらしいので、分かり次第紹介します。
AWS CDKでデプロイする
デプロイするアプリの準備ができれば、AWSにデプロイします。
% cdk deploy
CloudFormationまたはLambdaのコンソールやAPIから、デプロイしたLambdaのfunctions URLを取得しましょう。
試してみて
実際にデプロイしてみたものがこちらです。
LambdaからS3の画像を配信する部分がどうも上手くいかず、今回はS3にリダイレクトさせる形にしています。
実際のアプリで使う場合は、CloudFrontのbehaviorを上手く使って同一ドメインで配信できるようにする必要がありそうです。
その場合、middlewareの処理をCloudFrontまたはLambda@edgeやCloudFront function側でやってもよいかもしれません。