0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudFront のオリジンに Lambda Web Functions を指定できるようになったので OAC 付きで試してみた

0
Posted at

はじめに

10 月 12 日に登場した Lambda Web Functions が、同じ日に CloudFront のオリジンとしてもサポートされました。

忙しい人のための AI で簡単に図解理解

オリジン統合の概要とメリット.png

アップデートの詳細

CloudFront のオリジンに Web Function のエンドポイントを指定できるようになりました。 独自ドメイン・キャッシュ・WAF は CloudFront 側で足すのが公式の推奨構成です(Compute Blog)。

鍵は IamAuth × OAC

ApplicationManaged と IamAuth + OAC の比較

OAC は、CloudFront がオリジンへのリクエストに AWS の署名を付ける仕組みです。IamAuth のエンドポイントは署名のないリクエストを 403 で返します(Lambda のドキュメント)。

なお、IAM で呼び出し権限を持つユーザーやロールは、署名を付ければ直接呼べます(Lambda のドキュメント)。

アプリのログインはそのまま使える

CloudFront の署名は専用ヘッダーに入るので、利用者のトークン(Authorization ヘッダー)はそのままアプリに届きます。

CloudFront の署名と利用者のトークンは別のヘッダーで届く

ふつうの AWS 署名は Authorization ヘッダーを使うため、上書きされるとアプリのログイン(JWT など)が壊れます。Web Function 用の OAC(署名動作 always-amz-auth)ではこれが起きません。

やってみた

Web Function の前に CloudFront を置き、Lambda 側のドメインを直接叩かれないようにするところまでを試します。

最終的にできあがる構成はこんな形です。
CloudFront と Lambda Web Functions の構成図

手順 1: IamAuth のエンドポイントで Web Function を作る

手順 1: IamAuth の Web Function を作ると、署名なしの直接アクセスは 403

届いた 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 とキャッシュポリシーを作る

手順 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: ディストリビューションを作る

手順 3: ディストリビューションを作っただけでは 403

オリジンに 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 を許可する

手順 4: リソースポリシーで CloudFront を許可すると 200

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 経由は 200、直接アクセスは 403

① 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 を選ぶ

AllViewer と AllViewerExceptHostHeader の違い

Host ヘッダーは「どのサイト宛てか」を表す宛名です。AllViewer は利用者が送った宛名(CloudFront や独自ドメイン)のまま渡すため、Lambda が自分宛てと判断できずエラーになります(手元では 502、ドキュメント上は 404)。

まとめ

IamAuth・OAC・リソースポリシーに、Authorization を通すキャッシュポリシーと AllViewerExceptHostHeader を組み合わせるだけで、Lambda Web Functions を CloudFront の裏に置けました。アプリのログインもそのまま使えるので、独自ドメインや WAF を足したいときの土台として使いやすそうです。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?