🤕

CloudFront OAC + Function URLのPOST/PUT問題をHono RPCで解決するには

2024/12/14に公開

この記事はHono Advent Calendar 2024 13日目の記事です。

https://qiita.com/advent-calendar/2024/hono


こんにちはwatanyです。HonoのこういうPRを送ったりしてます。

https://hono.dev/docs/getting-started/lambda-edge

https://hono.dev/docs/helpers/ssg

最近はあまりHonoの開発に入れてなくて、じゃあ何やってんだというとAWS CDKにこんなパッチを入れていました。

https://github.com/aws/aws-cdk/pull/31339

このパッチの内容であるCloudFront OAC + Lambda Function URLの説明から今回の話になります。

CloudFront OAC + Function URL

Lambdaの組み込みエンドポイントとしてLambda Functions URLが使えるのですが、AuthがPublic公開かAWS IAM認証かの二択であったり、セキュリティ系の設定がCORSしかなく、CloudFront + AWS WAF経由でのアクセスを強制したいものの、Originの直接アクセスを防ぐことができませんでした。

これが2024/4のアップデートにより、CloudFrontのOrigin Access Control(OAC)が実装され、この課題は解決したのです!

https://aws.amazon.com/jp/about-aws/whats-new/2024/04/amazon-cloudfront-oac-lambda-function-url-origins/

とはいえCDKがこれを公式にはサポートしていなかったので愚直に書くしかなく、9月に私が素案を書き、色々あって11月にリリースされました。

CDKでの書き方

AWS Lambda(Functions URL) + CloudFront(OAC) + Honoという構成は、Hono公式ドキュメントをちょっと変えてこれでデプロイできます。

import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";

export class MyAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const fn = new NodejsFunction(this, 'lambda', {
      entry: 'lambda/index.ts',
      handler: 'handler',
      runtime: lambda.Runtime.NODEJS_20_X,
    })
    fn.addFunctionUrl({
      authType: lambda.FunctionUrlAuthType.AWS_IAM,
    })

    const distribution = new cloudfront.Distribution(this, 'MyDistribution', {
      defaultBehavior: {
        origin:
//          origins.FunctionUrlOrigin(myFunctionUrl),
          origins.FunctionUrlOrigin.withOriginAccessControl(myFunctionUrl),
          allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_ALL,
          cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
          originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
      },
    });
    new cdk.CfnOutput(this, 'ServiceUrl', { value: `https://${distribution.distributionDomainName}` });

//    const cfnDistribution = distribution.node.defaultChild as cloudfront.CfnDistribution;
    // OAC
//    const cfnOriginAccessControl = new cloudfront.CfnOriginAccessControl(
//      this,
//      'OriginAccessControl',
//        {
//            originAccessControlConfig: {
//                name: 'oac-lambda',
//                originAccessControlOriginType: 'lambda',
//                signingBehavior: 'always',
//                signingProtocol: 'sigv4',
//                description: 'Access Control'
//            }
//        }
//    );
//    cfnDistribution.addPropertyOverride(
//        'DistributionConfig.Origins.0.OriginAccessControlId',
//        cfnOriginAccessControl.attrId
//    );
//
//    lambdaFunc.addPermission('allowOacfromCf', {
//        action: 'lambda:InvokeFunctionUrl',
//        principal: new iam.ServicePrincipal('cloudfront.amazonaws.com'),
//        sourceArn: `arn:aws:cloudfront::${this.account}:distribution/${distribution.distributionId}`
//    });
  }
}

ちなみにコメントアウトした部分が私のパッチが入るまでの記述量でした。ヤバいね!

CloudFront OAC + Functions URLのPOST/PUT問題

これで話が解決すればよかったのですが、このアーキテクチャを取る場合、POST/PUT時には以下のいずれかの制約が加わってしまいました。

  1. SHA256で計算したリクエストボディのハッシュ値をx-amz-content-sha256ヘッダーへ含める

https://zenn.dev/10q89s/articles/b96b5a09ee45f2

  1. CloudFrontでリクエストでフックされたLambda@Edgeに代わりに1.のハッシュ計算をさせる

https://dev.classmethod.jp/articles/cloudfront-lambda-url-with-post-put-request/

Hono on AWS LambdaがOriginの場合は?

2のLambda@Edge案だと、リクエスト毎にLambda@Edgeが起動し、馬鹿にならないコストになります。とはいえ各クライアントに実装する1よりはマシか……と消極的に採用するか悩ましいのですが、HonoだとRPCの仕組みがあるので、このクライアントのヘッダーに入れてしまえば良さそうです。

Code

client.ts
import { hc } from 'hono/client';
import type { EchoAppType } from './index';

const client = hc<EchoAppType>('https://xxxxxxxxxxxxxx.cloudfront.net/');

const calculateSHA256 = async (message: string): Promise<string> => {
  const msgBuffer = new TextEncoder().encode(message);
  const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
  const hashBytes = new Uint8Array(hashBuffer);

  let hashHex = "";
  for (let i = 0; i < hashBytes.length; i++) {
    const b = hashBytes[i];
    hashHex += b < 16 ? '0' + b.toString(16) : b.toString(16);
  }

  return hashHex;
};

async function runClient() {
  // ペイロードをJSON形式で作成
  const payload = { name: 'Alice', message: 'Hello World' };

  // POSTリクエスト送信
  const res = await client.echo.$post(
    {
      json: payload,
    },
    {
      headers: {
        'x-amz-content-sha256': await calculateSHA256(JSON.stringify(payload)), 
      },
    }
  );

  if (res.ok) {
    const data = await res.json();
    console.log('Response:', data);
  } else {
    console.error('Request failed:', res.status);
  }
}

runClient();

というコードが本家にあったらいいよね

上記の役割を自動で行うClientがあったら嬉しいはずなのでクライアントをPRしてみました!

https://github.com/honojs/hono/pull/3740

したのですが、ちょっと二重実装になのでメンテしずらさそうな感じですね…… calculateSHA256があるだけでも、クライアント/L@Eで使いやすくて助かるので、それはそれで嬉しそうですが。

Discussion