はじめに
10 月 12 日に登場した Lambda Web Functions が、同じ日に CloudFront のオリジンとしてもサポートされました。
忙しい人のための AI で簡単に図解理解
アップデートの詳細
CloudFront のオリジンに Web Function のエンドポイントを指定できるようになりました。 独自ドメイン・キャッシュ・WAF は CloudFront 側で足すのが公式の推奨構成です(Compute Blog)。
鍵は IamAuth × OAC
OAC は、CloudFront がオリジンへのリクエストに AWS の署名を付ける仕組みです。IamAuth のエンドポイントは署名のないリクエストを 403 で返します(Lambda のドキュメント)。
なお、IAM で呼び出し権限を持つユーザーやロールは、署名を付ければ直接呼べます(Lambda のドキュメント)。
アプリのログインはそのまま使える
CloudFront の署名は専用ヘッダーに入るので、利用者のトークン(Authorization ヘッダー)はそのままアプリに届きます。
ふつうの AWS 署名は Authorization ヘッダーを使うため、上書きされるとアプリのログイン(JWT など)が壊れます。Web Function 用の OAC(署名動作 always-amz-auth)ではこれが起きません。
やってみた
Web Function の前に CloudFront を置き、Lambda 側のドメインを直接叩かれないようにするところまでを試します。
手順 1: IamAuth のエンドポイントで Web Function を作る
届いた Host と Authorization ヘッダーをそのまま返すだけのアプリを用意します。
export AWS_REGION=ap-northeast-1
echo $AWS_REGION
mkdir -p cf-origin-demo
cat > cf-origin-demo/server.mjs <<'EOF'
import http from 'node:http';
http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({
message: 'Hello from Lambda Web!',
host: req.headers['host'],
authorization: req.headers['authorization'] ?? null,
}));
}).listen(3000);
EOF
--auth-type IamAuth を付けてデプロイします。
aws lambda-web deploy --name cf-origin-demo --code ./cf-origin-demo --auth-type IamAuth --create
ORIGIN=$(aws lambda-web get-web-function-endpoint --function-name cf-origin-demo --endpoint-name dev --query domainName --output text)
echo $ORIGIN
curl -s -o /dev/null -w "%{http_code}\n" https://$ORIGIN/
署名なしで直接叩くと 403 になります。
手順 2: OAC とキャッシュポリシーを作る
OAC はオリジンタイプ lambda、署名動作 always-amz-auth で作ります。リージョン単位のエンドポイント(HomeRegion)なので署名方式は sigv4 です。
cat > oac.json <<'EOF'
{
"Name": "cf-origin-demo-oac",
"SigningProtocol": "sigv4",
"SigningBehavior": "always-amz-auth",
"OriginAccessControlOriginType": "lambda"
}
EOF
OAC_ID=$(aws cloudfront create-origin-access-control --origin-access-control-config file://oac.json --query 'OriginAccessControl.Id' --output text)
echo $OAC_ID
公式ドキュメントでは、Authorization をキャッシュポリシーに入れるよう求めています。 入れないと、GET や HEAD ではこのヘッダーが捨てられ、アプリにトークンが届きません。
MaxTTL(キャッシュしておく最長時間)は 1 秒にします。これは、0 にするとヘッダーを指定できず、作成時にエラーになるためです。
cat > cache-policy.json <<'EOF'
{
"Name": "cf-origin-demo-forward-auth",
"MinTTL": 0,
"DefaultTTL": 0,
"MaxTTL": 1,
"ParametersInCacheKeyAndForwardedToOrigin": {
"EnableAcceptEncodingGzip": false,
"EnableAcceptEncodingBrotli": false,
"HeadersConfig": { "HeaderBehavior": "whitelist", "Headers": { "Quantity": 1, "Items": ["Authorization"] } },
"CookiesConfig": { "CookieBehavior": "none" },
"QueryStringsConfig": { "QueryStringBehavior": "none" }
}
}
EOF
CP_ID=$(aws cloudfront create-cache-policy --cache-policy-config file://cache-policy.json --query 'CachePolicy.Id' --output text)
echo $CP_ID
手順 3: ディストリビューションを作る
オリジンに Web Function のドメインと OAC を指定します。オリジンリクエストポリシーは AllViewerExceptHostHeader にします。
ORP_ID=$(aws cloudfront list-origin-request-policies --type managed --query "OriginRequestPolicyList.Items[?OriginRequestPolicy.OriginRequestPolicyConfig.Name=='Managed-AllViewerExceptHostHeader'].OriginRequestPolicy.Id" --output text)
echo $ORP_ID
cat > dist.json <<EOF
{
"CallerReference": "cf-origin-demo-$(date +%s)",
"Comment": "cf-origin-demo",
"Enabled": true,
"Origins": {
"Quantity": 1,
"Items": [{
"Id": "lambda-web",
"DomainName": "$ORIGIN",
"OriginAccessControlId": "$OAC_ID",
"CustomOriginConfig": { "HTTPPort": 80, "HTTPSPort": 443, "OriginProtocolPolicy": "https-only" }
}]
},
"DefaultCacheBehavior": {
"TargetOriginId": "lambda-web",
"ViewerProtocolPolicy": "redirect-to-https",
"CachePolicyId": "$CP_ID",
"OriginRequestPolicyId": "$ORP_ID"
}
}
EOF
DIST_ID=$(aws cloudfront create-distribution --distribution-config file://dist.json --query 'Distribution.Id' --output text)
echo $DIST_ID
aws cloudfront wait distribution-deployed --id $DIST_ID
CF_DOMAIN=$(aws cloudfront get-distribution --id $DIST_ID --query 'Distribution.DomainName' --output text)
echo $CF_DOMAIN
curl -s -o /dev/null -w "%{http_code}\n" https://$CF_DOMAIN/
この時点では CloudFront 経由でも 403 です。
これは、OAC で署名はしていても、Web Function 側がまだ CloudFront を許可していないためです。
手順 4: Web Function に CloudFront を許可する
Web Function にリソースベースポリシーを付けます。
aws:SourceArn で「このディストリビューションからのリクエストだけ」に絞るのがポイントです。
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo $ACCOUNT_ID
ENDPOINT_ARN=$(aws lambda-web get-web-function-endpoint --function-name cf-origin-demo --endpoint-name dev --query endpointArn --output text)
echo $ENDPOINT_ARN
FUNCTION_ARN=$(aws lambda-web get-web-function --function-name cf-origin-demo --query functionArn --output text)
echo $FUNCTION_ARN
cat > policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowCloudFrontOACAccess",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "lambda:InvokeWebFunctionEndpoint",
"Resource": "$ENDPOINT_ARN",
"Condition": { "StringEquals": { "aws:SourceArn": "arn:aws:cloudfront::$ACCOUNT_ID:distribution/$DIST_ID" } }
}]
}
EOF
aws lambda-web put-resource-policy --resource-arn "$FUNCTION_ARN" --policy file://policy.json --query revisionId --output text
結果確認
3 つのアクセスを試します。① と ② が 200、③ が 403 なら成功です。 Lambda(IamAuth)が見るのは「このディストリビューションの CloudFront を通ったか」だけで、利用者のトークンは見ません。
① CloudFront 経由(トークンなし)
curl -s https://$CF_DOMAIN/; echo
{"message":"Hello from Lambda Web!","host":"xxx.yyy.lambda-web.ap-northeast-1.on.aws","authorization":null}
CloudFront 経由なら Lambda まで届きます。このサンプルのアプリはトークンをチェックしないので 200 ですが、実際のアプリでログインが必要なページなら、ここでアプリが 401 などで弾きます。
② CloudFront 経由(トークンあり)
curl -s -H "Authorization: Bearer my-app-token" https://$CF_DOMAIN/; echo
{"message":"Hello from Lambda Web!","host":"xxx.yyy.lambda-web.ap-northeast-1.on.aws","authorization":"Bearer my-app-token"}
送ったトークンが消されずにアプリまで届きます。アプリはこのトークンを検証して、ログイン済みかどうかを判断できます。
③ Lambda のドメインに直接(署名なし)
curl -s -o /dev/null -w "%{http_code}\n" https://$ORIGIN/
403
後片付け
ディストリビューションは無効化してから削除します。
Web Function を消しても残る IAM ロール・ロググループ・S3 のデプロイパッケージもあわせて消します(S3 バケットは CLI 共有なのでこの関数の分だけ。バージョニング有効なので古いバージョンごと消します)。
aws cloudfront get-distribution-config --id $DIST_ID --query DistributionConfig > disable.json
sed -i '' 's/"Enabled": true/"Enabled": false/' disable.json
aws cloudfront update-distribution --id $DIST_ID --if-match $(aws cloudfront get-distribution --id $DIST_ID --query ETag --output text) --distribution-config file://disable.json --query Distribution.Status --output text
aws cloudfront wait distribution-deployed --id $DIST_ID
aws cloudfront delete-distribution --id $DIST_ID --if-match $(aws cloudfront get-distribution --id $DIST_ID --query ETag --output text)
aws cloudfront delete-cache-policy --id $CP_ID --if-match $(aws cloudfront get-cache-policy --id $CP_ID --query ETag --output text)
aws cloudfront delete-origin-access-control --id $OAC_ID --if-match $(aws cloudfront get-origin-access-control --id $OAC_ID --query ETag --output text)
aws lambda-web delete-web-function --function-name cf-origin-demo
aws iam delete-role-policy --role-name awscli-lambdaweb-cf-origin-demo --policy-name awscli-lambdaweb-logs
aws iam delete-role --role-name awscli-lambdaweb-cf-origin-demo
aws logs delete-log-group --log-group-name /aws/lambda/web/cf-origin-demo
aws logs delete-log-group --log-group-name /aws/lambda/web/cf-origin-demo.local
aws s3api list-object-versions --bucket awscli-lambdaweb-$ACCOUNT_ID-$AWS_REGION-an --prefix lambda-web/cf-origin-demo/ --query '{Objects: [Versions, DeleteMarkers][][].{Key: Key, VersionId: VersionId}}' --output json > versions.json
aws s3api delete-objects --bucket awscli-lambdaweb-$ACCOUNT_ID-$AWS_REGION-an --delete file://versions.json --query 'length(Deleted)'
注意点
オリジンリクエストポリシーは AllViewerExceptHostHeader を選ぶ
Host ヘッダーは「どのサイト宛てか」を表す宛名です。AllViewer は利用者が送った宛名(CloudFront や独自ドメイン)のまま渡すため、Lambda が自分宛てと判断できずエラーになります(手元では 502、ドキュメント上は 404)。
まとめ
IamAuth・OAC・リソースポリシーに、Authorization を通すキャッシュポリシーと AllViewerExceptHostHeader を組み合わせるだけで、Lambda Web Functions を CloudFront の裏に置けました。アプリのログインもそのまま使えるので、独自ドメインや WAF を足したいときの土台として使いやすそうです。









