Hono on Lambda 〜真のLambdalith構成を目指して〜
こんにちは、サーバーレス大好き芸人のt-maedaです。
LambdaでREST APIの開発を進めながら、「同じLambdaでEventBridgeのスケジュール処理もまとめて面倒を見たい」と欲張ると、いつの間にか関数を分割してテストもデプロイもローカル開発の手法も複雑になっていた……そんな経験はありませんか?
AWSはLambdaベースのイベント駆動型アプリケーションのアンチパターンとしてLambdaモノリスパターン(以下Lambdalith)を挙げ、パッケージサイズや最小特権の難しさなど複数の欠点を指摘しています。
- パッケージサイズ
- 最小特権の強制が困難
- アップグレードが困難
- 維持が困難
- コードの再利用が困難
- テストが困難
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/concepts-event-driven-architectures.html#event-driven-anti-patterns
しかし小〜中規模のWebアプリで見渡すと、普段使い慣れたWebフレームワークを選び、アップグレード性やテスト容易性をそのまま持ち込みたいという現実的なニーズが根強くあります。
そこでLambda Web AdapterやServerless Expressを導入してみるのですが、API Gatewayのタイムアウト制限に阻まれたり、S3やEventBridgeを意識した瞬間にシングルパーパスLambdaが積み上がり、実装やテストの工数が爆発…
そんな「HTTPアプリを書きたい」と「イベントハンドラーとしてデプロイしたい」のギャップが悩みの種です。
本記事では、それらの処理を1つのLambdaで処理できるような構成を検証していきます。
HTTP向けに書いたアプリケーションコードを、そのままEventBridgeなどの非HTTPイベントにも転用できる 真のLambdalith 体験を目指したアプローチです。
作ったもの
EventBridge Scheduled Eventsによる自動期限切れチェック機能を持つTodo APIです。
実装コードはこちらから確認できます。
主な機能:
- Todo CRUD API - REST API (GET/POST/PUT/DELETE)
- EventBridge定期実行 - 毎日UTC 0:00 (JST 9:00)に期限切れチェック
-
自動ステータス更新 - 期限切れTodoを自動的に
status: "overdue"
に更新 - OpenAPI/Swagger UI - 本番・ローカル両方で動作するAPI仕様書
- 開発用エンドポイント - ローカル開発専用のイベントトリガーエンドポイント
- 単一コードベース - HTTPリクエストもEventBridgeイベントも同じアプリケーションコードで処理
技術スタック
- Framework: Hono
- Runtime: AWS Lambda (Node.js 20.x)
- API: AWS API Gateway (REST API)
- Database: Amazon DynamoDB
- Scheduler: Amazon EventBridge
- IaC: AWS CDK
アーキテクチャ
本番環境
API GatewayとEventBridge Schedulerの2つのイベントソースから、単一のLambda関数が呼び出されます。
Lambda内でイベントソースを判定し、適切な処理に振り分けるLambdalithパターンを採用しています。
Honoのアプリケーションを一度だけ初期化し、その上にHTTPルーターと非HTTPイベント用のハンドラーを同居させるのがポイントです。
ローカル開発環境
ローカル開発では、adapters/mock-lambda.ts
がLambda context/eventをシミュレートし、開発用エンドポイント(/dev/trigger-eventbridge
)を提供します。
本番のDynamoDBテーブルに直接アクセスして開発できます。
実装のポイント
最小構成は以下の3ファイルで役割を分担しています。
-
api/src/app.ts
: HTTPルーティングとイベント振り分けを担うHonoアプリ本体。 -
api/src/handler.ts
: 上記アプリをそのままLambdaハンドラーとして公開。 -
api/src/index.ts
: ローカル開発時にのみ呼ばれるエントリーポイントで、モックLambdaアダプター経由で同じアプリを起動。
この土台を押さえたうえで、どこでイベント検知と分岐を行っているかを見ていきます。
1. 単一Lambda関数で複数イベントソースを処理
通常のLambdalithパターンの欠点を回避しつつ、Webフレームワークの恩恵を受けるため、Honoのルーティング機能を活用します。
ルートパス(/
)でEventBridgeイベントを検出し、通常のHTTPリクエストとは別の処理フローに分岐させています。
Lambda handlerはシンプルです:
2. EventBridgeイベントも同じアプリケーションで処理する
非HTTPイベントを別Lambdaに切り出さず、同一のHonoアプリケーションのルート(/
)がEventBridgeイベントを検出して処理します。
HTTPと同じサービス層・リポジトリを共有しているため、期限切れチェックのロジックを二重に持つ必要がありません。
イベントの種類が増えたときも、events/
配下にハンドラーを追加し、ルートでイベントの判定を増やすだけで同じアプリケーションコードを再利用できます。
3. ローカル開発用アダプター
本番コードを変更せずに、ローカル開発時のみモックエンドポイントを追加:
このアダプターはindex.ts
(ローカル開発用エントリーポイント)でのみ使用され、本番デプロイには一切影響しません。
ローカルで/dev/trigger-eventbridge
にPOSTすることで、EventBridgeイベントを手動実行できます:
curl -X POST http://localhost:3000/dev/trigger-eventbridge
まとめ
Honoを使うことで、単一のLambda関数が以下の両方を処理できる構成を実現しました。
従来のLambdalithが抱えていた「パッケージサイズ」や「最小特権」の問題は残りますが、小〜中規模のアプリケーションであれば、開発効率とメンテナンス性の向上というメリットが上回ると感じました。
実際のプロダクション環境では、負荷やセキュリティ要件に応じて、Lambda関数の分割を検討する必要がありますが、まずはこの構成でスタートし、必要に応じて分割していくアプローチが現実的だと考えています。
今後の拡張
この構成をベースに、AppSync Eventsを用いたWebSocket通信や通知機能、デバッグが煩雑化しがちなStepFunctionsの統合も目指していきたいです。
Discussion