【CDKあり】Lambdaの関数URLのPOSTで躓いたら見る記事
AWSでLambdaの関数URLで「CloudFrontからのアクセスだけに絞るとき」につまずいた点を備忘録として残しておきます。
どんな構成の話
Lambda Web Adapter(以下LWA)を使えば、Next.jsやReact RouterのようなWebアプリをLambdaにデプロイできます。この記事ではReact Routerを使ったLWAの話をします。
最終的なアーキテクチャはざっくりこんな感じです。

- LWAのアプリへのアクセスをCloudFront経由だけに絞る(=関数URLへのアクセスをさせない)
- 画像などはS3へアクセスさせるようにCloudFrontで設定
- React Router v7(Remix)を使う
PUT・POSTはハッシュ値の計算が必要なのでLambda@Edgeを使っています。公式ドキュメントだと次の箇所です。
Lambda 関数 URL で PUT メソッドまたは POST メソッドを使用する場合、ユーザーはリクエストを CloudFront に送信するときにリクエスト本文の SHA256 を計算し、本文のペイロードハッシュ値を x-amz-content-sha256 ヘッダーに含める必要があります。Lambda は、署名されていないペイロードをサポートしていません。
GETは通るのにPOSTが通らない
CDKで前述のような「CloudFront+LWA」を動かしてみました。
https://サンプル.cloudfront.net
でRemixのサイトの表示はできましたが、フォームの送信はできませんでした。
他にもいろいろつまずいた点があったのでまとめておきます。
遭遇したエラーたち
This distribution is not configured to allow the HTTP request method that was used for this request.he distribution supports only cachable requests.We can't connect to the server for this app or website at this time.There might be too much traffic or a configuration error.Try again later, or contact the app or website owner.
叩いたHTTPメソッドが許可されてないよって話です。
The request signature we calculated does not match the signature you provided.Check your AWS Secret Access Key and signing method.Consult the service documentation for details.
リクエスト時にLambda@Edgeで付与しているはずのx-amz-content-sha256
が無いよって話です。
解決策
CDKの最終的な例は長いので折りたたみの中に載せておきます。次のセクションから小分けにして説明します。
折りたたみ
this.myDistribution = new cloudfront.Distribution( this, "MyDistribution", { defaultBehavior: { origin: origins.FunctionUrlOrigin.withOriginAccessControl( this.myLambdaUrl, ), allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, edgeLambdas: [ { functionVersion: edgeFnVersion, eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, // ボディを含めないとLambda@Edgeでrequest.bodyが渡らず、POST/PUTのbodyハッシュ計算ができない includeBody: true, }, ], }, additionalBehaviors: { // パスの優先順位はこの上からの順番。最後にdefaultBehavior=`*`が加わる "/assets/*": { origin: myAssetsOrigin, allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, "/public/*": { origin: myAssetsOrigin, allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, "/*.data": { origin: origins.FunctionUrlOrigin.withOriginAccessControl( this.myLambdaUrl, ), allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, edgeLambdas: [ { functionVersion: edgeFnVersion, eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, // ボディを含めないとLambda@Edgeでrequest.bodyが渡らず、POST/PUTのbodyハッシュ計算ができない includeBody: true, }, ], }, "/*.*": { origin: origins.S3BucketOrigin.withOriginAccessControl( props.bucket, { originAccessLevels: [cloudfront.AccessLevel.READ], }, ), allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, }, }, ); this.myLambda.addPermission("AllowCloudFrontInvoke", { principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"), action: "lambda:InvokeFunctionUrl", sourceArn: this.myDistribution.distributionArn, });
this.myAssetsBucket.addToResourcePolicy( new iam.PolicyStatement({ actions: ["s3:GetObject"], resources: [this.myAssetsBucket.arnForObjects("*")], principals: [new iam.ServicePrincipal("cloudfront.amazonaws.com")], conditions: { StringEquals: { "AWS:SourceArn": this.myDistribution.distributionArn, }, }, }), );
ビヘイビア・メソッドの設定
ビヘイビアやそのメソッドが正しく設定されているか確認します。
今回は/contact
=*
(デフォルトビヘイビア)にPOSTすると考えていたため、次の設定で十分だと考えていました。
this.myDistribution = new cloudfront.Distribution(this, "MyDistribution", { // このままだとエラー defaultBehavior: { origin: origins.FunctionUrlOrigin.withOriginAccessControl( this.myLambdaUrl, ), allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, }, additionalBehaviors: { // パスの優先順位はこの順番。最後にdefaultBehavior=`*`が加わる "/assets/*": { origin: myAssetsOrigin, allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, // ... }, "/public/*": { origin: myAssetsOrigin, allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, // ... }, "/*.*": { origin: origins.S3BucketOrigin.withOriginAccessControl(...), allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, // ... }, }, },);
でもこれだと「許可されていないメソッド」のエラーになってしまいました。
ブラウザのコンソールを見るとPOSTなのは合ってましたが、実際には/contact
ではなく/contact.data
に対してPOSTされていました。

これはReact Routerによるものでした。リクエストは/foo.data
のようなエンドポイントに対して投げるようです。次の記事も参考になります。
React Router v7 の内部構造を探る:リクエストからレンダリングまでの道のり
元の設定だと/*.data
は/*.*
に当てはまるため、S3へのアクセスになってしまいたのが原因でした。
次のように「/*.data
ならLambdaへ行くビヘイビア」を追加しました。
this.myDistribution = new cloudfront.Distribution(this, "MyDistribution", { defaultBehavior: { origin: origins.FunctionUrlOrigin.withOriginAccessControl( this.myLambdaUrl, ), allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, }, additionalBehaviors: { // パスの優先順位はこの順番。最後にdefaultBehavior=`*`が加わる "/assets/*": { origin: myAssetsOrigin, allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, // ... }, "/public/*": { origin: myAssetsOrigin, allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, // ... }, "/*.data": { origin: origins.FunctionUrlOrigin.withOriginAccessControl( this.myLambdaUrl ), allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, // ... }, "/*.*": { origin: origins.S3BucketOrigin.withOriginAccessControl(...), allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, // ... }, }, },);
CDKだとadditionalBehaviors
で書いた順の最後にデフォルトビヘイビアが加わるようです。なので/*.*
よりも前に/*.data
を書きました。
x-amz-content-sha256の計算関連
コンテンツのハッシュの計算用Lambda@Edgeの実装は次の記事を参考にしました。
CloudFront + Lambda 関数 URL 構成でPOST/PUT リクエストを行うため Lambda@Edge でコンテンツハッシュを計算する | DevelopersIO
CDKではedgeLambdas
に書くだけで紐づけできます。筆者は最初勘違いしていたのですが、Lambda@Edge側で「トリガー」の設定は必要ありません。
このままだとPOSTを投げてもハッシュについてのエラーになります。
this.myDistribution = new cloudfront.Distribution( this, "MyDistribution", { defaultBehavior: { origin: origins.FunctionUrlOrigin.withOriginAccessControl( this.myLambdaUrl, ), // ... edgeLambdas: [ { functionVersion: edgeFnVersion, eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, // このままだとエラー }, ], }, additionalBehaviors: { "/assets/*": { // ... }, "/public/*": { // ... }, "/*.data": { origin: origins.FunctionUrlOrigin.withOriginAccessControl( this.myLambdaUrl, ), // ... edgeLambdas: [ { functionVersion: edgeFnVersion, eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, // このままだとエラー }, ], }, "/*.*": { // ... }, }, },);
CloudWatchのログを確認
ここで、CloudWatcでログを確認することにしました。
ログはLambda@Edgeを実行させるリージョンのCloudWatchの/aws/lambda/us-east-1.関数の名前
にあります(参考:公式ドキュメント)。
Lambda@Edge は、関数ログを CloudWatch Logs に自動的に送信し、関数が呼び出された AWS リージョンにログストリームを作成します。
Lambda@Edgeを作成するときのリージョン「バージニア北部us-east-1
」とは違う点に注意です。
実際にログを見てみた結果
先ほど紹介した記事では次のようにログを仕込んでます。
const request = event.Records[0].cf.request;console.log("originalRequest", JSON.stringify(request));
if (!request.body?.data) { return request;}
const body = request.body.data;const decodedBody = Buffer.from(body, "base64").toString("utf-8");
request.headers["x-amz-content-sha256"] = [ { key: "x-amz-content-sha256", value: await hashPayload(decodedBody) },];console.log("modifiedRequest", JSON.stringify(request));
筆者が投げたリクエストは全部originalRequest
のみがログに残っており、modifiedRequest
はログにありませんでした。
つまり、「request.body?.data
の分岐で終わっている = ボディーのデータが無い」ということです。
const request = event.Records[0].cf.request;console.log("originalRequest", JSON.stringify(request));
if (!request.body?.data) { return request;}// ここから先が実行されてない
const body = request.body.data;const decodedBody = Buffer.from(body, "base64").toString("utf-8");
request.headers["x-amz-content-sha256"] = [ { key: "x-amz-content-sha256", value: await hashPayload(decodedBody) },];console.log("modifiedRequest", JSON.stringify(request));
これはCloudFrontのビヘイビアにて、「ボディーを含める」べきところで含めていないのが原因でした。
ボディーを含める
ボディーを含めるにはCDKだと次のように設定が必要です。
this.myDistribution = new cloudfront.Distribution( this, "MyDistribution", { defaultBehavior: { origin: origins.FunctionUrlOrigin.withOriginAccessControl( this.myLambdaUrl, ), // ... edgeLambdas: [ { functionVersion: edgeFnVersion, eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, includeBody: true, }, ], }, additionalBehaviors: { "/assets/*": { // ... }, "/public/*": { // ... }, "/*.data": { origin: origins.FunctionUrlOrigin.withOriginAccessControl( this.myLambdaUrl, ), // ... edgeLambdas: [ { functionVersion: edgeFnVersion, eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, includeBody: true, }, ], }, "/*.*": { // ... }, }, },);
AWSコンソールだと「本文を含める」みたいなチェックマークです。
これで反映したら無事POSTが成功しました。
確認チェックリスト
チェックリストとしてまとめました。
- Lambda@Edge関係
- 想定どおりに実行されているか?
- CloudWatchのログは実行したリージョンにある(作成したリージョンではない)
- CloudFrontのビヘイビア関係
- オリジンリクエストとしてLambda@Edgeを関連付けられているか?
- 関連付けたLambda@Edgeのバージョンは正しいか?
- 正しいメソッドを指定しているか?
- ボディー(本文)を含める設定にしているか?
- リクエストは想定したビヘイビア届いているか?
- ビヘイビアの順番はあっているか?
以上、LWAでつまずいたCloudFrontとLambda@Edge関係の話でした。「S3へのアップロード」「デプロイ時のGitHub Actions」などは気が向いたら別の記事で書きます。
最後にAWSを触ったのが2年以上前だったので、いろいろ感慨深いです。