🦞

Hono + AWS Lambda Function URLsで作るWebhook Proxy

2023/05/26に公開

はじめに

私が某ブログで検証した結果として送ったプルリクが無事マージされ、v3.2.0から無事にLambda Function URLsでもhonoが利用できるようになりました。

https://twitter.com/honojs/status/1659439603160604674

せっかくなので何か使ってみようと思い、このブログを書いています。

何を作るか

  • Serverless Frameworkの勉強をしたかった
  • zodを試してみたい
  • slack webhookで遊ぶ機会があった

という様々な経緯から、今回はSlackのWebhookのリクエストを手前で受ける関数をLambdaで作る事にした。

API Gateway less構成の魅力

主なpros/consはこの資料が詳しい。
https://speakerdeck.com/_kensh/aws-lambda-function-urls?slide=32

API Gatewayの設定は正直難しい。またAPI Gateway + lambdaに準ずる構成だとマイクロサービス、というとカッコいいが小さいプロダクトだと分割損も目立つはずだ(個人の見解です)

そこで、単機能のAPIをLambda Function URLs+Webframework入りのLambdaの組み合わせでシンプルに作るメリットが生まれてくる。

実装

Serverless Framework

まず、Serverless Frameworkの公式チュートリアルに従って環境をセットアップする。
https://www.serverless.com/framework/docs/tutorial

npm i -g serverless

プロジェクトを作成する。※serverlessのショートハンドがslsらしい。

sls

Creating a new serverless project

? What do you want to make? AWS - Node.js - Starter
? What do you want to call this project? hono-proxy

✔ Project successfully created in hono-proxy folder

? Do you want to deploy now? Yes

Deploying hono-proxy to stage dev (us-east-1)

✔ Service deployed to stack hono-proxy-dev (87s)

functions:
  function1: hono-proxy-dev-function1 (1.4 kB)

What next?
Run these commands in the project directory:

serverless deploy    Deploy changes
serverless info      View deployed endpoints and resources
serverless invoke    Invoke deployed functions
serverless --help    Discover more commands

Lambda

公式の設定を参考に、こんな感じで書いてみる。
https://www.serverless.com/framework/docs/providers/aws/guide/functions

service: hono-proxy
frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs18.x
  memorySize: 128
  timeout: 10
  tracing:
    lambda: true

functions:
  hello:
    handler: index.handler
    url: true
    environment:
      WEBHOOK_URL: https://hooks.slack.com/services/XXXXXX/

これをデプロイ。lambdaだけ完結する世界観だと確かに簡単。

cd hono-proxy
sls deploy

hono

先ほどのserverless frameworkのディレクトリの中にhonoのプロジェクトを作る。

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

せっかくなのでzod-validatorを使ってみる。

npm i "@hono/zod-validator"

いよいよコーディング。

import { Hono } from "hono";
import { handle } from "hono/aws-lambda";
import { logger } from "hono/logger";
import { basicAuth } from 'hono/basic-auth'
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const url: string = process.env.WEBHOOK_URL;
const app = new Hono();
app.use("*", logger());
app.use(
  '/webhook',
  basicAuth({
    username: 'yourname',
    password: 'yoursecret',
  })
)
const schema = z.object({
  text: z.string().min(1),
  icon_emoji: z.string().regex(/^:\w+:$/),
});

app.post(
  "/webhook",
  zValidator("json", schema, (result, c) => {
    if (!result.success) {
      return c.text("Invalid!", 400);
    }
  }),
  async (c) => {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(await c.req.json()),
    });
    return c.json({
      status: response.status,
      ok: response.ok
    });
  }
);

app.get('/health', async (c) => {
	return c.json({ status: 'OK' }, 200)
})

export const handler = handle(app);

healthcheck用のEndpointは、これを見ていいなと思ったので生やしている。
https://github.com/fujiwara/mkr2oncall-cloudflare-workers/blob/main/src/index.ts

Deploy

デプロイ用にスクリプトを軽く書き換えて

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",
+    "update": "aws lambda update-function-code --zip-file fileb://lambda.zip --function-name hono-proxy-dev-hello --region us-east-1",
    "deploy": "run-s build zip update"
  },
  "devDependencies": {
    "esbuild": "^0.17.11",
    "npm-run-all": "^4.1.5"
  },
  "dependencies": {
    "@hono/zod-validator": "^0.1.2",
    "hono": "^3.2.2",
    "node-fetch": "^3.3.1"
  }
}

デプロイする。

npm run deploy

動いた!

# healthcheck
curl https://boe2vqfcc4zqqhoexj6ejcdbgi0edbke.lambda-url.us-east-1.on.aws/health
{"status":"OK"}

# slack webhook
## Unauthorized
curl  -X POST -d '{"text": "sample", "icon_emoji": ":cold_face:"}' https://boe2vqfcc4zqqhoexj6ejcdbgi0edbke.lambda-url.us-east-1.on.aws/webhook         
Unauthorized 
## Authorized
curl  -X POST -u yourname:yoursecret -d '{"text": "sample", "icon_emoji": ":cold_face:"}' https://boe2vqfcc4zqqhoexj6ejcdbgi0edbke.lambda-url.us-east-1.on.aws/webhook                          
{"status":200,"ok":true}

連携も無事成功した!

まとめ

ということでLambdaでも簡単なAPIを運用できることがわかった。
当然だがプロダクション運用だとBasic認証頼りとはいかないし、IAM認証で不十分であればAPIGateway+WAF(+CloudFront)を前段に配置する必要はある。だが、ミニマムなパターンとしてこういった手段も持っておくと何かと便利なので覚えておきたい。というか、個人向けプロダクトならこれで十分かなと。

他の詳しい使い方は、作者様の解説を読むとイメージがつかめるかもしれない。
https://zenn.dev/yusukebe/articles/647aa9ba8c1550

Discussion