CloudFront OAC + Function URLのPOST/PUT問題をHono RPCで解決するには
この記事はHono Advent Calendar 2024 13日目の記事です。
こんにちはwatanyです。HonoのこういうPRを送ったりしてます。
最近はあまりHonoの開発に入れてなくて、じゃあ何やってんだというとAWS CDKにこんなパッチを入れていました。
このパッチの内容である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)が実装され、この課題は解決したのです!
とはいえ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時には以下のいずれかの制約が加わってしまいました。
- SHA256で計算したリクエストボディのハッシュ値を
x-amz-content-sha256
ヘッダーへ含める
- CloudFrontでリクエストでフックされたLambda@Edgeに代わりに1.のハッシュ計算をさせる
Hono on AWS LambdaがOriginの場合は?
2のLambda@Edge案だと、リクエスト毎にLambda@Edgeが起動し、馬鹿にならないコストになります。とはいえ各クライアントに実装する1よりはマシか……と消極的に採用するか悩ましいのですが、HonoだとRPCの仕組みがあるので、このクライアントのヘッダーに入れてしまえば良さそうです。
Code
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してみました!
したのですが、ちょっと二重実装になのでメンテしずらさそうな感じですね…… calculateSHA256
があるだけでも、クライアント/L@Eで使いやすくて助かるので、それはそれで嬉しそうですが。
Discussion