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

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

    Random posts

    Home
    Search
    Bookmark