Hono + AWS Lambda Function URLsで作るWebhook Proxy
はじめに
私が某ブログで検証した結果として送ったプルリクが無事マージされ、v3.2.0から無事にLambda Function URLsでもhonoが利用できるようになりました。
せっかくなので何か使ってみようと思い、このブログを書いています。
何を作るか
- Serverless Frameworkの勉強をしたかった
- zodを試してみたい
- slack webhookで遊ぶ機会があった
という様々な経緯から、今回はSlackのWebhookのリクエストを手前で受ける関数をLambdaで作る事にした。
API Gateway less構成の魅力
主なpros/consはこの資料が詳しい。
API Gatewayの設定は正直難しい。またAPI Gateway + lambdaに準ずる構成だとマイクロサービス、というとカッコいいが小さいプロダクトだと分割損も目立つはずだ(個人の見解です)
そこで、単機能のAPIをLambda Function URLs+Webframework入りのLambdaの組み合わせでシンプルに作るメリットが生まれてくる。
実装
Serverless Framework
まず、Serverless Frameworkの公式チュートリアルに従って環境をセットアップする。
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
公式の設定を参考に、こんな感じで書いてみる。
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は、これを見ていいなと思ったので生やしている。
Deploy
デプロイ用にスクリプトを軽く書き換えて
{
"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)を前段に配置する必要はある。だが、ミニマムなパターンとしてこういった手段も持っておくと何かと便利なので覚えておきたい。というか、個人向けプロダクトならこれで十分かなと。
他の詳しい使い方は、作者様の解説を読むとイメージがつかめるかもしれない。
Discussion