AWSユーザのためのHono入門
これ何
この記事で話そうとした、AWS Lambda/Lambda@EdgeにおけるHonoの利用方法を紹介します。
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を積極的に採用しているため、リクエストやレスポンスを以下の形式で扱います。
ところで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
}
Lambda@Edgeでもこれは同様です。
HonoのAWS向けのAdaptorは、AWS独自のリクエストイベントをweb標準イベントに変換するためのアダプターです。ラフに描くとこんな感じ。
Lambda event
→ to request
→ Hono Logic
→ return response
→ to Lambda response
扱い方
Deploy
基本的には公式の手順に従ってAWS CDKで実現するのがおすすめです。
CDKではHonoと同じTypeScriptで書けますし、NodejsFunction
が強力なので、.ts
のコードを書いたら勝手にビルドしてデプロイするまでを任せられます。
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だけなので便利です。自前で実装するときは、例えばここで紹介したような特有の実装が必要です。
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で書いたのが下記の記事ですが、ロジック自体にほとんど手を入れずに書き換えられることがわかります!
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'
}
})
})
当然AdaptorをつければLambdaでもLambda@edgeでもJSXで動かせます!実装例はこちらでも。
Middleware
フレームワーク採用の嬉しい点として付随するエコシステムがあげられますが、Honoにも便利なミドルウェアがあるため、これらも採用することで実装を加速できます。
Auth
Basic, Bearer, jwtなどの、組み込みの認証ミドルウェアがあるので便利です。
- Lambda Functions URLで IAMは採用できない。
- バックエンドのコードに手を入れられないのでCloudFront側(Lambda@Edge)で対応したい
という際に便利です。後者であるLambda@Edgeで行うサンプルはこちらでも紹介しております。
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用など様々なミドルウェアがあるので、探してみてください!
まとめ
今までもHono × AWSの話を個別にしてきたのですが、ひとまとめにしてみました。
AWSはマネージドでも様々な機能を提供していますが、細部で実現しづらい機能があったりコードで書く方がシンプルだな、というケースはあるはずなので、選択肢の一つとしてFrameworkの選択肢を持っておくと便利です。その中でもHonoは使っていて心地よくかけるので、ポジショントークというところを置いておいてもおすすめです。
AWS的に必要な機能はあるていど実装できたのですが、Contributeは続けていく予定なので、よく分らんところはIssuesやDiscussionsで私も見るようにします。お気軽にどうぞ。
Discussion