🙋‍♂️

AWSユーザのためのHono入門

2023/12/20に公開

これ何

この記事で話そうとした、AWS Lambda/Lambda@EdgeにおけるHonoの利用方法を紹介します。

https://zenn.dev/watany/articles/183d40f8e31a45

Lambdaでwebフレームワークを扱う是非は、前の記事もあわせて読んでください。

対象読者

  • Lambdaにフレームワークを載せる運用に興味があるAWSユーザ
  • AWS CDKでTypescriptを利用していて、バックエンドをサクッと書く方法を知りたいAWSユーザ

他のプラットフォームで既にHonoを利用している方は、公式ドキュメントと重複する点が多いことにご留意ください。

あなた誰

  • 主にAWS界隈で活動しています。
  • HonoのContributerとして、AWS LambdaのAdaptorをメンテナンスしています。
  • HonoのContributerとして、Lambda@EdgeのAdaptorを提案し、以降もメンテナンスしています。

というわけで、少なくともAdaptorの実装に関しては比較的詳しいです。利用者が増えると嬉しいので、布教目的もあってこの記事を書いています。

Adaptorの基本的な考え方

HonoはWeb標準APIを積極的に採用しているため、リクエストやレスポンスを以下の形式で扱います。

https://developer.mozilla.org/ja/docs/Web/API/Request

https://developer.mozilla.org/ja/docs/Web/API/Response

ところでLambdaはイベントドリブンで動作するため、API GatewayやALBから渡ってくるリクエストも一度このようなEventに変換されてLambdaへ渡ります。

{
      "resource": "/",
      "path": "/",
      "httpMethod": "GET",
      "requestContext": {
          "resourcePath": "/",
          "httpMethod": "GET",
          "path": "/Prod/",
          ...
      },
      "headers": {
          "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
          "accept-encoding": "gzip, deflate, br",
          "Host": "70ixmpl4fl.execute-api.us-east-2.amazonaws.com",
          "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36",
          "X-Amzn-Trace-Id": "Root=1-5e66d96f-7491f09xmpl79d18acf3d050",
          ...
      },
      "multiValueHeaders": {
          "accept": [
              "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
          ],
          "accept-encoding": [
              "gzip, deflate, br"
          ],
          ...
      },
      "queryStringParameters": null,
      "multiValueQueryStringParameters": null,
      "pathParameters": null,
      "stageVariables": null,
      "body": null,
      "isBase64Encoded": false
  }

https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html#apigateway-example-event

Lambda@Edgeでもこれは同様です。

HonoのAWS向けのAdaptorは、AWS独自のリクエストイベントをweb標準イベントに変換するためのアダプターです。ラフに描くとこんな感じ。

Lambda event 
→ to request
→ Hono Logic
→ return response 
→ to Lambda response

扱い方

Deploy

基本的には公式の手順に従ってAWS CDKで実現するのがおすすめです。

https://hono.dev/getting-started/aws-lambda

https://hono.dev/getting-started/lambda-edge

CDKではHonoと同じTypeScriptで書けますし、NodejsFunctionが強力なので、.tsのコードを書いたら勝手にビルドしてデプロイするまでを任せられます。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html

CDKを採用できない場合は、Hono公式のStarterでnpx create hono@latastでセットアップしましょう。
そうすると最低限のnpm scriptは書いてあるので、こちらを参考にデプロイを検討するのが良いでしょう。Lambda@EdgeはCloudFront側のバージョン紐付けも忘れないように。

AWS Lambda

基本的にはこのようにimportして、entrypointに handlerを指定して扱います。

import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))

export const handler = handle(app)

通常のHonoContextでマッピングされない、eventやcontextなどのLambda handlerに入ってきた要素を取得する場合はこのようにします。

import { Hono } from 'hono'
import type { LambdaEvent, LambdaContext } from 'hono/aws-lambda'
import { handle } from 'hono/aws-lambda'

type Bindings = {
  event: LambdaEvent
  context: LambdaContext 
}

const app = new Hono<{ Bindings: Bindings }>()

app.get('/aws-lambda-info/', (c) => {
  return c.json({
      isBase64Encoded: c.env.event.isBase64Encoded,
      awsRequestId: c.env.context.awsRequestId
  })
})

export const handler = handle(app)

Lambda response streamingも対応していて、専用のAdaptorstreamHandleに変えると動きます。

import { Hono } from 'hono'
import { streamHandle } from 'hono/aws-lambda'

const app = new Hono()

app.get('/stream', async (c) => {
  return c.streamText(async (stream) => {
    for (let i = 0; i < 3; i++) {
      await stream.writeln(`${i}`)
      await stream.sleep(1)
    }
  })
})

const handler = streamHandle(app)

完全に手前味噌ですが、この「AWS Lambda response streaming」に対応してるのは、公式のLambda Web AdaptorかHonoだけなので便利です。自前で実装するときは、例えばここで紹介したような特有の実装が必要です。

https://speakerdeck.com/watany/aws-lambda-response-streaming-shi-zhuang-qian-nisiritaiyatu

Lambda@Edge

Lambda@Edgeはもう少し複雑で、

  • ユーザへ直接レスポンスする
import { Hono } from 'hono'
import { handle } from 'hono/lambda-edge'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono on Lambda@Edge!'))

export const handler = handle(app)
  • callbackしてCloudFrontのシーケンスへ流す
import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'
import type { Callback, CloudFrontRequest } from 'hono/lambda-edge'
import { handle } from 'hono/lambda-edge'

type Bindings = {
  callback: Callback
  request: CloudFrontRequest
}

const app = new Hono<{ Bindings: Bindings }>()

app.get(
  '*',
  basicAuth({
    username: 'hono',
    password: 'acoolproject',
  })
)

app.get('/', async (c, next) => {
  await next()
  c.env.callback(null, c.env.request)
})

export const handler = handle(app)

で書き方が変わります。

既知の課題

Lambda@Edge周りはc.env.callbackが前提なのが難点です。Lambda@EdgeのNode.js Runtimeの制約に合わせる形でいったん実装したのですが、可能であればreturn c.env.requestの形でシンプルな書き方で実現したいです。

これは実現性も含めて調査中なので、いや簡単っしょ!という方はPullRequestお願いします。

Other

今回はAWS系のAdaptorがあるAWS Lambda/Lambda@Edgeに重きを置いて説明しています。

例えばコンテナでの運用、AWSでいうとApp RunnerやECSでの採用も視野に入れたとき、frameworkにロックインされるのは……と悩みがちですが、基本的に

  • Adaptorのimport
  • EntryPointをAdaptor用の記述

の2行を変えれば済むので、あまり気にする必要はないかもしれません。

Node.js, Deno, Bunの3種類のJS系Runtimeで書いたのが下記の記事ですが、ロジック自体にほとんど手を入れずに書き換えられることがわかります!

https://zenn.dev/watany/articles/194e31331a25be

Hono採用で得られるもの

基本機能

公式サイトを見ると以下のような特徴が挙げられています。

  • 🚀Ultrafast & Lightweight
  • 🌍Multi-runtime
  • 🔋Batteries Included
  • 😃Delightful DX

軽いという点で、Frameworkのコアの部分だけなら20KB程度です。これはCloudFront Functions(10KB)の制約こそ満たせませんが、Lambda@Edge(Viewer 1MB)制限にまでは対応できるので汎用性がかなり高いです。

AWS CDKのためにTypeScriptを覚えたAWSユーザ的にも、同様にTypeScriptで書けるFrameworkは魅力的ではないかと思います。

JSX

あまり知られていなそうなので段落を分けてみました。Honoは元はAPIフレームワークとしての扱いが主体だったのですが、ここ最近はJSXサポートが充実しています。

import { renderToReadableStream, Suspense } from 'hono/jsx/streaming'

// ...

app.get('/', (c) => {
  const stream = renderToReadableStream(
    <html>
      <body>
        <h1>Hono v3.10.0</h1>
        <Suspense fallback={<div>loading...</div>}>
          <Component />
        </Suspense>
      </body>
    </html>
  )
  return c.body(stream, {
    headers: {
      'Content-Type': 'text/html; charset=UTF-8',
      'Transfer-Encoding': 'chunked'
    }
  })
})

https://github.com/honojs/hono/releases/tag/v3.10.0

当然AdaptorをつければLambdaでもLambda@edgeでもJSXで動かせます!実装例はこちらでも。

https://qiita.com/watany/items/b86c2304832126de76e0

Middleware

フレームワーク採用の嬉しい点として付随するエコシステムがあげられますが、Honoにも便利なミドルウェアがあるため、これらも採用することで実装を加速できます。

Auth

Basic, Bearer, jwtなどの、組み込みの認証ミドルウェアがあるので便利です。

  • Lambda Functions URLで IAMは採用できない。
  • バックエンドのコードに手を入れられないのでCloudFront側(Lambda@Edge)で対応したい

という際に便利です。後者であるLambda@Edgeで行うサンプルはこちらでも紹介しております。

https://zenn.dev/watany/articles/d8f7ed33aec139

Validator

Honoでバリデーションを行う場合、組み込みValidatorというよりは、Zodとミドルウェア@hono/zod-validatorで扱うのが一般的です。

import { zValidator } from '@hono/zod-validator'
const route = app.post(
  '/posts',
  zValidator(
    'form',
    z.object({
      body: z.string(),
    })
  ),
  (c) => {
    // ...
  }
)

他にもVailbot, Typebox, Typiaなどを利用するValidatorがあるので、用途で選べば良さそうです。

Other

他にもセキュリティ系やOpenAPI用など様々なミドルウェアがあるので、探してみてください!

https://hono.dev/

https://github.com/honojs/middleware

まとめ

今までもHono × AWSの話を個別にしてきたのですが、ひとまとめにしてみました。

AWSはマネージドでも様々な機能を提供していますが、細部で実現しづらい機能があったりコードで書く方がシンプルだな、というケースはあるはずなので、選択肢の一つとしてFrameworkの選択肢を持っておくと便利です。その中でもHonoは使っていて心地よくかけるので、ポジショントークというところを置いておいてもおすすめです。

AWS的に必要な機能はあるていど実装できたのですが、Contributeは続けていく予定なので、よく分らんところはIssuesやDiscussionsで私も見るようにします。お気軽にどうぞ。

Discussion