Deploy Sonik application to AWS Lambda with functions URL by AWS CDK
Sonik is a JavaScript meta framework maintained by Yusuke-san, Hono framework maintainer. It allows building websites and web applications with Next.js-like file-based routing and Hono-like REST API. The text provides instructions on how to configure an AWS CDK project and deploy a Sonik app to AWS. It includes code snippets for defining AWS Lambda and Amazon S3 resources, as well as setting up static asset deployment. The text also mentions updating the Sonik app to adapt to AWS Lambda by adding a new entry point file, setting the handler function for mapping requests and responses, and adding a middleware to support static assets deployed to S3. It suggests using CloudFront for distributing static assets in a production environment.
目次
Sonik is a JavaScript meta framework maintained by Yusuke-san, Hono framework maintainer.
https://www.npmjs.com/package/sonik
We can build websites and web applications that are built by Next.js-like file-based routing and also Hono-like REST API.
Configure AWS CDK project
At that time, I will try to deploy the demo to AWS because it looks easy to deploy to Cloudflare.
Let’s create a first project by the AWS CDK CLI.
% mkdir my-sonik-app
% cd my-sonik-app
% cdk init app -l typescript
Update the lib/cdk-stack.ts
file to add the definition of AWS Lambda and Amazon S3 resources.
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
})
}
}
The reason why adding the S3 bucket is to deploy static assets like favicons and images.
So we also need to define the BucketDeployments
construct to deploy the static resources on the Sonik application.
Create Sonik application
Then, let’s set up the first Sonik app.
% npx degit yusukebe/sonik/examples/basic sonik-app
% npm --prefix sonik-app install
% npm install -D @types/aws-lambda
Possibly this way of setting up the Sonik application will be changed after releasing version 1.
Update Sonik app to adapt to AWS Lambda
We need to update several codes to adapt this application to AWS Lambda.
Add the lambda.ts
file to create a new entry point
We need to create a new entry point file for AWS Lambda instead of _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
Set the handler
function for mapping the AWS Lambda’s request and response.
Add a new middleware to support the static assets
Add a new middleware to support the static asset that deployed to Amazon S3.
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
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()
})
For the keeping of simplicity, I choose to redirect the request to the static assets to the S3 bucket directly.
But possibly using GetObject
looks better.
I copied and pasted the getMimeType
function from the Hono repository.
Update Vite configuration
To support running this on the AWS Lambda function, we need to update the vite.config.ts
file.
import { defineConfig } from 'vite'
import sonik from 'sonik/vite'
export default defineConfig({
plugins: [
sonik({
entry: './lambda.ts',
minify: true,
}),
],
// 追加ここから
build: {
rollupOptions: {
output: {
format: 'commonjs'
}
}
}
// 追加ここまで
})
Deploy to AWS
Build the Sonik application
In this way, we need to build the Sonik application at first by manually.
% npm --prefix ./sonik-app run build
Deploy the application by CDK CLI command
Run the cdk delploy
command to deploy the application.
% cdk deploy
We can check the application URL on the AWS Lambda console or AWS CLI command.
What did I build?
I took a screenshot of my first Sonik application on AWS Lambda:
For production use, we need to use CloudFront to distribute the static assets from the same domain.
Possibly we will use a new Hono/Sonik middleware or Lambda@edge / CloudFront Function script.