Hono on AWS Lambdaを体感する

NTTテクノクロス株式会社の渡邉洋平です。

この間、HonoがAWS Lambdaでも動くと知ったので、今回は試していきます。

https://twitter.com/yusukebe/status/1636363822327009283

## What's Hono?

Honoは@yusukebe氏を中心にOSSで開発されている、Edge向けのWebフレームワークです。

https://hono.dev/

> Hono - [炎] means flame🔥 in Japanese - is a small, simple, and ultrafast web framework for the Edges. It works on Cloudflare Workers, Fastly Compute@Edge, Deno, Bun, Vercel, Lagon, Node.js, and others. Fast, but not only fast.

この辺の資料も読むとわかる通り、標準技術を極力用いて「どこでも動く」を実現することが目的の一つである様子がわかります。

https://speakerdeck.com/yusukebe/dokodemodong-kuwebhuremuwakuwotukuru

---

## 検証

今回は以下の２通りのアーキテクチャパターンでHonoでの動作を試します。

1. AWS Lambda Function URLs
2. Amazon API Gateway + AWS Lambda

:::note warn 
本検証ではHono v3.1.8を利用しています
:::

### 1. GET

#### Hono

https://hono.dev/getting-started/basic

公式に従って、以下のコマンドで簡単にテンプレートを作成することができます。

```bash
npm create hono@latest my-app
Which template do you want to use? › aws-lambda
```

`my-app`の中身を見ると、`npm run deploy`で直接Lambda Functionにデプロイできるようデザインされていることがわかります。

```json:my-app/package.json
{
  "type": "module",
  "scripts": {
    "build": "esbuild --bundle --outfile=./dist/index.js --platform=node --target=node18 ./src/index.ts",
    "zip": "zip -j lambda.zip dist/index.js",
    "update": "aws lambda update-function-code --zip-file fileb://lambda.zip --function-name hello",
    "deploy": "run-s build zip update"
  },
  "devDependencies": {
    "esbuild": "^0.17.11",
    "npm-run-all": "^4.1.5"
  },
  "dependencies": {
    "hono": "^3.1.6"
  }
}
```

#### CDK

Hono標準でデプロイ機構が組み込まれているとはいえ、Typescriptで書いたコードだけでなくIAM RoleやAPI Gatewayまで含めた一式を扱う時はCDKが便利なので、今回は簡単にCDK環境を整備します。

```bash
mkdir cdk
cd cdk
cdk init app -l typescript
npm i hono
mkdir lambda
cp ../my-app/src/index.ts lambda/
```

Lambda用のコードは、まずは`my-app/src/index.ts`の内容をそのまま使います。

```ts:lambda/index.ts
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)
```

Honoで書いたコードを動かすLambda環境は以下で定義して、`cdk deploy`コマンドでデプロイします。

```ts:cdk/lib/cdk-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigateway'
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

export class CdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const fn = new NodejsFunction(this, 'lambda', {
      entry: 'lambda/index.ts',  
      handler: 'handler', 
      runtime: lambda.Runtime.NODEJS_18_X,
    });
    fn.addFunctionUrl({
      authType: lambda.FunctionUrlAuthType.NONE
    })
    new apigw.LambdaRestApi(this, 'myapi', {
      handler: fn,
    });
  }
}
```

#### 動作

Lambda、API Gateway、どちらのエンドポイントでもレスポンスが想定通りに返却されることがわかりました。

```bash
# Lambda Function URLs
curl https://4tlatzqa463rz42z3ytb2ingua0twkik.lambda-url.ap-northeast-1.on.aws/
Hello Hono!

# API Gateway
curl https://tof1s221v8.execute-api.ap-northeast-1.amazonaws.com/prod/
Hello Hono!
```

### 2. POST 

#### ソースコード

チュートリアル相当が動くのは分かったので、少しだけ下の機能をhonoで追加します。

- ロガーの追加
- GETの内容でRuntime情報を返却するように変更
- POSTの追加

```diff
import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'
+ import { logger } from 'hono/logger'

const app = new Hono()

+ interface UserBody {
+  user: string;
+ }

+ app.use('*', logger())
app.get('/', (c) => { 
- return c.text('Hello Hono!'))
+ return c.text(`Hono is running on ${c.runtime}\n`)
})
+ app.post('/', async (c) => {
+   const { user } = await c.req.json<UserBody>();
+   if (!user) {
+     return c.text('Please Setting "user"', 400)
+   } 
+   return c.json({"user": user})
+ })

export const handler = handle(app)
```

あくまで開発向け機能だが、一度AWS環境が作られていれば、コードの差分はCDKの`hotswap`モードで更新できる。

```bash
cdk deploy --hotswap
```

#### 動作(GET)

```bash
# Lambda Function URLs
curl https://4tlatzqa463rz42z3ytb2ingua0twkik.lambda-url.ap-northeast-1.on.aws/
Hono is running on node

# API Gateway
curl  https://tof1s221v8.execute-api.ap-northeast-1.amazonaws.com/prod/
Hono is running on node
````

どちらも想定通り動作しました。

公式サイトの記述を見るにRuntimeは`node`か`Other`かと想定していましたが、Lambda(Node.js)ではnodeと判定されるようです。

https://hono.dev/api/context#runtime


#### 動作(POST)

続いてPOSTでもリクエストを投げたところAPIGatewayは正常に動きました。 しかし、Lambda Function URLsは上手くいかない様子。

```bash
# Lambda Function URLs
curl -X POST -H 'Content-Type: application/json' -d '{ "user": "fuga" }'  https://4tlatzqa463rz42z3ytb2ingua0twkik.lambda-url.ap-northeast-1.on.aws/
Internal Server Error

# API Gateway
curl -X POST -H 'Content-Type: application/json' -d '{ "user": "fuga" }'  https://tof1s221v8.execute-api.ap-northeast-1.amazonaws.com/prod/
{"user":"fuga"}
curl -X POST -H 'Content-Type: application/json' -d '{ "name": "fuga" }'  https://tof1s221v8.execute-api.ap-northeast-1.amazonaws.com/prod/
Please Setting "user"
```

こんな時の為にLoggerを追加していたので、CloudWatch Logsを確認すると以下のログが出ていました。

```json
{
    "errorType": "TypeError",
    "errorMessage": "Request with GET/HEAD method cannot have body.",
    "stack": [
        "TypeError: Request with GET/HEAD method cannot have body.",
        "    at new Request (node:internal/deps/undici/undici:7171:17)",
        "    at createRequest (/var/task/index.js:1570:10)",
        "    at Runtime.handler (/var/task/index.js:1540:17)",
        "    at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1086:29)"
    ]
}
```

ん？ Lambda Function URLsでのリクエストはGET/HEADとして連携しているので、Body部と不整合を起こしたのだろうか。`-d`なしで再度POSTを投げてみます。

```bash
curl -X POST -H 'Content-Type: application/json'  https://4tlatzqa463rz42z3ytb2ingua0twkik.lambda-url.ap-northeast-1.on.aws/
Hono is running on node
```

やはりGETとして解釈されていることがわかりました。Hono、Function URLのいずれかの制約なのかもしれません。

:::note info 
Hono側のコードを確認すると、AWS Lambda用のAdaptorは、API Gatewayベースでのリクエストのみを利用する前提に見えます。
https://github.com/honojs/hono/blob/main/src/adapter/aws-lambda/handler.ts

現時点では、Lambda Function URLやLambda@Edgeでは利用しない方が良さそうです。
:::

## まとめ

今回はHono × CDKでの動作を簡単に確認できました。
 
今年の「AWS Dev Day 2023 Tokyo」でも作者様の話が聞けそうなので、楽しみにしています。

https://github.com/aws-events/aws-dev-day-tokyo-2023-cfp/issues/102