AWSAWS CDKHono / SonikJavaScript

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.

Referenced blogs

ブックマークや限定記事(予定)など

WP Kyotoサポーター募集中

WordPressやフロントエンドアプリのホスティング、Algolia・AWSなどのサービス利用料を支援する「WP Kyotoサポーター」を募集しています。
月額または年額の有料プランを契約すると、ブックマーク機能などのサポーター限定機能がご利用いただけます。

14日間のトライアルも用意しておりますので、「このサイトよく見るな」という方はぜひご検討ください。

広告ここから
広告ここまで

Related Category posts