Lambda Web FunctionsとCloudFront・API Gatewayを連携してみる
Lambda Web Functions+Honoの前段にCloudFrontとAPI Gatewayを置き、認証、endpointへの直接アクセス、cache、timeoutを実測しました。設定を変えた対照から、前段とアプリの責任を分けます。
Lambda Web FunctionsでHonoのHTTPサーバーが動いても、公開の入口は別に考える必要があります。前段にCloudFrontやAPI Gatewayを置くだけで、直接アクセスや個人別の応答まで守れるのでしょうか。
ここでは、前段を通さず、Web FunctionのURLへ送る呼び出しを「直接アクセス」と呼びます。
今回の検証では、CloudFrontのOACと、HTTP APIのHTTP_PROXYを比べました。OACを設定すると、CloudFrontが転送先へのリクエストにIAM署名を付けます。この転送先をオリジンと呼びます。転送先のendpointが、署名を検証します。HTTP_PROXYは、受け取ったHTTPリクエストを別のHTTPサーバーへ転送する方式です。
認証、cache、timeoutを分けて試すと、前段へ任せられる部分と、Honoで守る部分が見えてきました。特に、署名付きの直接アクセスと、日本語を含むURLでは、入口を追加しただけでは済まない結果になりました。
検証する構成
同じWeb Functionに、同じZIPから作った2つのrevisionを配置しました。revisionは、コードと設定を固定した版です。環境変数だけを変え、CloudFront用とAPI Gateway用の処理を分けています。endpointは、Web FunctionへアクセスするURLです。
| 項目 | 設定 |
|---|---|
| 配置先 | ap-northeast-1。CloudFrontはglobal |
| Runtime | nodejs24.x。実際の応答はNode.js v24.21.0、arm64 |
| CloudFrontの接続先 | IamAuth endpoint。IAMの署名で認証 |
| HTTP APIの接続先 | ApplicationManaged endpoint。アプリが認証 |
| Lambdaの上限 | timeout 6秒。endpointごとの環境数2、環境内の同時処理数4 |
| 呼び出し | HTTP/1.1。毎秒1件以下で順に送信 |
| 比較 | 主な条件を2回ずつ試す。設定を戻した後も確認 |
Web Functionsの対応runtime一覧 を確認して選びました。Event Functionsのruntime一覧とは区別しています。

検証時の構成です。青い実線は前段を通るHTTPSリクエスト、赤い実線はendpointへの直接アクセスです。破線はSecretの取得とログ出力を示します。応答は実線の逆向きに返ります。以下の実測画像は、保存したAWSの応答やCloudWatch Logsを、CloudShellの画面へ表示して撮影したものです。
CloudFront OACを試す
OACには
always-amz-authを指定しました。Web Functionのオリジンでは、この値だけが使えます。利用者のAuthorizationをHonoへ渡しながら、CloudFrontがオリジン向けのIAM署名を付ける設定です。利用者の識別には、公開してよいダミー値を使いました。
Bearer lab-aliceとBearer lab-bobをHonoで照合します。この比較はheaderの転送を確かめるためのものです。実際の利用者に使うJWTの検証は行っていません。cacheを無効にし、
AllViewerExceptHostHeaderで転送すると、aliceとbobをそれぞれ返しました。Bearerがない場合と不一致の場合は401でした。Authorizationを転送しない設定では、正しいダミーBearerを送っても401になりました。Hostも区別が必要でした。CloudFrontへのHostをそのまま転送する対照では、2回とも502でした。オリジンのドメイン名を使う基準設定では成功しています。AWSの管理origin request policy にも、Web Functions向けのAllViewerExceptHostHeaderが用意されています。endpointへの直接アクセスを制限する
Web Functionのresource-based policyで、CloudFrontのサービス主体を許可しました。このpolicyは、関数側で呼び出しを許可・拒否する設定です。許可する呼び出し元を、今回のdistributionの
aws:SourceArnに限定しています。別のSourceArnにすると403になり、正しい値へ戻すと200へ復旧しました。ただし、このAllowだけでは直接アクセス経路を閉じられません。同じAWS account内で、専用roleへendpointの呼び出し権限を与えると、署名付きの直接アクセスは200でした。署名なしは403です。
直接アクセスも拒む対照では、対象endpointの呼び出しだけに、次のDeny条件を加えました。
1
2
3
4
5
6
7
8
9
10
11
{
"Effect": "Deny",
"Principal": "*",
"Action": "lambda:InvokeWebFunctionEndpoint",
"Resource": "<CloudFront用endpointのARN>",
"Condition": {
"ArnNotEquals": {
"aws:SourceArn": "<許可するdistributionのARN>"
}
}
}この条件では、CloudFront経由は2回とも200でした。署名付きと署名なしの直接アクセスは、各2回とも403でした。Denyを外した後は、CloudFront経由の200を再確認しています。

CloudFrontからendpointへのIAM署名と、Honoによる利用者のBearer照合は、別に設計します。CloudFrontだけを入口にする場合は、同じaccount内のIAM権限も含めて直接アクセス経路を確認します。endpointのアクセス制御 も、この権限の境界を説明しています。
cacheを分ける
個人別の応答は、CloudFrontの
CachingDisabledと、アプリのCache-Control: private, no-store, max-age=0を組み合わせました。利用者を変えても同じURLへ送ります。alice、bob、alice、bobの順で送ったところ、応答の利用者は一致しました。応答を作るたびに発行する
renderIdも、8件の成功応答で全て異なりました。認証なしと不一致は、計4件とも401でした。CloudWatch Logsでも、これら12件に対応するHonoの開始ログを確認しています。公開データは、別の
/public/*へ分けました。cache key、つまり保存した応答を選ぶ条件に、queryのvを含めます。TTLは最小0秒、既定0秒、最大30秒です。アプリは公開データへpublic, max-age=30を付けます。| 同じURLへ送る順序 | v | CloudFrontの結果 | renderId |
|---|---|---|---|
| 1 | red | Miss | red用を新規発行 |
| 2 | red | Hit | 1と同じ |
| 3 | blue | Miss | blue用を新規発行 |
| 4 | blue | Hit | 3と同じ |
| 5 | red | Hit | 1と同じ |
| 6 | blue | Hit | 3と同じ |
この並びを別の公開URLでも試し、同じ結果になりました。公開12件は4回のMissと8回のHitでした。公開データへ利用者の情報は入れていません。

まず個人別の経路でcacheを無効にし、保存してよい公開データだけを分けると、確認する範囲を絞れます。管理cache policyの仕様 では、最小TTLが0より大きいと、
no-storeなどが付いた応答もその時間は保存されます。今回は、この設定による個人別データの混在実験は行っていません。HTTP APIを接続する
API GatewayはHTTP APIを使い、
HTTP_PROXYでApplicationManaged endpointへ接続しました。Lambdaのイベント形式へ変換する連携ではなく、HonoのHTTPサーバーへHTTPSで転送します。呼び出し元の認証はrouteの
AWS_IAMに任せました。API GatewayがIAM署名を検証します。HTTP API側では、Bearerで利用者を見分ける検証はしていません。署名付きは成功し、署名なしのAPI呼び出しは2回とも403でした。ApplicationManagedでは、Lambdaは認証せず、呼び出し時のresource-based policyも評価しません。そのため、API Gatewayを通さないリクエストもHonoへ届きます。直接アクセスを拒む役割は、アプリに残ります。
今回は、Secrets Managerで専用tokenを生成し、API Gatewayが
X-Lab-Origin-Tokenを上書きします。Honoは、全routeの前でこの値を照合する実装にしました。1
overwrite:header.x-lab-origin-token = <専用Secretの値>値はCloudFormationのdynamic referenceで渡しました。コードやCDK contextへは書いていません。Honoは実行時にSecrets Managerから取得し、短時間だけメモリへ保持します。
| 入力 | 結果。各条件2回 |
|---|---|
| endpointへ直接アクセス。tokenなし | 403 |
| endpointへ直接アクセス。誤ったtoken | 403 |
| endpointへ直接アクセス。正しいtoken | 200 |
| 署名付きAPI呼び出し。呼び出し元が誤tokenを付与 | 上書きされて200。誤tokenは2種類で比較 |

これは、Secretを知る相手の直接アクセスまで拒む方式ではありません。tokenの保管、ログへ出さない実装、更新時の両側の設定は、運用する側の責任になります。tokenのrotationと無停止の切り替えは、今回の検証には含めていません。
CloudFront以外を前段に置く場合、AWSはアプリでのtoken照合などを案内しています 。
URLと本文を比べる
英数字のpath、重複するquery、日本語を含むJSON本文は、HTTP API経由でも受け取れました。重複queryは、今回のHonoでは2つの値として読めています。
一方、次の日本語pathは400になりました。
1
/echo/%E6%97%A5%E6%9C%AC?q=%E6%97%A5%E6%9C%AC最初は、
overwrite:path = $request.pathを設定していました。parameter mapping で、stage名を除くpathを渡す指定です。書き換えを削除するCDK更新も試しました。しかし、更新後に取得したAPI設定には、同じ値が残っていました。書き換えがない場合の動作は未確認です。| 経路・入力 | 結果。各条件2回 |
|---|---|
| IAM署名付きの直接アクセス。同じ日本語path | 200 |
| CloudFront。同じ日本語path | 200 |
| HTTP API。path書き換えあり | 400 |
| HTTP API。削除を試した更新後も、実設定に書き換えが残存 | 400 |
| HTTP API。英数字path、日本語はqueryだけ | 200 |

400にはWeb Functionsのrequest IDが付いていました。どの内部処理で拒まれたかは、この応答から断定できません。HTTP_PROXYという方式だけで、URLが常に同じように届くとは判断できない結果です。
今回の構成を使うなら、日本語をpathへ入れるAPIは採用前に確かめます。queryやJSON本文へ移せる場合も、実際に使う形式で比較します。
timeoutを比べる
前段のtimeoutと、アプリの処理終了は別でした。3秒かかる待機処理を用意し、前段の待ち時間だけを8秒から1秒へ変えました。CloudFrontとHTTP APIの両方へPOSTで送りました。CloudFrontのオリジンへの接続試行は1回にしています。
| 条件 | CloudFront | HTTP API | Honoの処理 |
|---|---|---|---|
| 前段8秒、アプリ期限4秒 | 200。約3.74秒 | 200。約3.12秒 | 約3秒で完了 |
| 前段1秒、アプリ期限4秒 | 504。約1.40秒 | 503。約1.08〜1.10秒 | 応答後も続き、約3秒で完了 |
| 前段8秒、アプリ期限2秒 | 504。約2.74秒 | 504。約2.10〜2.13秒 | 約2秒で待機を取消し |
各条件を2回ずつ試しました。時間はクライアントが送信を始めてから、応答を読み終えるまでです。通信を含む少数の測定なので、前段の速度比較には使いません。

アプリの期限には、
AbortControllerを待機処理へ渡しました。期限を過ぎると待機を取り消し、アプリ自身が504を返します。CloudWatch Logsでは、前段だけを短くした場合のcompleted=trueと、アプリが止めた場合のcancelled=trueを確認しました。外部への書き込みがあるアプリでは、timeout後に処理が続く前提で設計します。処理へ伝わる期限と、再送しても重複しない仕組みが必要です。今回、取消しを確かめた対象はタイマーによる待機です。DBや外部HTTP呼び出しの取消しは測っていません。
CORSとstreamを比べる
CORSは、別のoriginからブラウザーが応答を読める条件です。Honoに設定を残す場合は、HTTP API側のCORS設定を空にしました。
Honoの許可originを
https://app.exampleにすると、CloudFrontとHTTP APIの両経路で、この値のAccess-Control-Allow-Originが返りました。不許可originでもOPTIONSは204、GETは200でしたが、この許可headerは付きません。statusだけでCORSの許可を判断しないようにします。次にHTTP API側へCORSを設定し、許可originを
https://gateway.exampleへ変えました。OPTIONSには、認証不要の専用routeも設けています。HonoとGatewayの両方へ異なる設定を置く対照です。許可headerはGateway側の値になり、Honoで許可したapp.exampleには付かなくなりました。設定を戻すと、Hono側の許可originへ戻りました。CORSの設定を管理する場所は、一方に決めます。応答を少しずつ返すstreamも比べました。Honoは250ミリ秒間隔で3つのJSON行を返します。直接アクセスとCloudFrontでは、受信の間隔も約250ミリ秒でした。HTTP APIでは、2回とも3行が同じ受信時刻にまとまりました。

ここで測ったのはHTTP応答のheaderと、クライアントでの受信時刻です。ブラウザーによるCORSの強制は測っていません。ブラウザーからSigV4で呼ぶ場合は、署名に使うheaderの許可も別に確かめる必要があります。HTTP APIの結果を、REST APIのresponse streaming へ一般化しないようにします。
まとめ
CloudFrontでは、OAC、header転送、cacheを別々に決めます。利用者のBearerをHonoへ渡し、個人別の応答はcacheを無効にします。CloudFrontだけを入口にするなら、IAM署名付きの直接アクセスも確認します。
HTTP APIでは、呼び出し元のIAM署名に加え、ApplicationManaged endpointへ届く専用tokenを照合します。今回の構成では、日本語pathの400と、短いstreamがまとまる挙動も採用判断に残りました。
公開データのcacheや逐次の受信が必要な経路は、今回成功したCloudFrontの構成が候補になります。HTTP APIを使う経路は、呼び出し元の認証と、endpointでのtoken照合を設けます。そのうえで、実際のURL、本文、応答形式を比べます。どちらでも、前段のtimeoutに処理の取消しまで任せず、アプリの期限と再送時の扱いを決めます。
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article