CloudFront で Basic 認証する場合の選択肢
※この記事については、t_maruさんの許可をいただいて、Buildサービスチームのアカウントにて転載させていただいております。
Solution Architect の t_maru です。
今回は CloudFront で Basic 認証を設定する手段である Lambda@Edge と CloudFront Functions の違いについて取り上げたいと思います。
CloudFront に Basic 認証を設定する動機
CloudFront は AWS が提供する Managed の Content Delivery Network (CDN) で、Origin として S3 や EC2 などを指定して、そこからのコンテンツをエッジロケーションと呼ばれる世界各国に配置されたデータセンターに保持し、ユーザーからリクエストがあった際には最もユーザーに近いエッジロケーションからコンテンツを送信するため、コンテンツ配信のパフォーマンスが高くなります。また、キャッシュ設定を適切にすることで、バックエンドにあたる Origin へのリクエスト数を削減することができる点も CloudFront を使うメリットだと思います。
今回とりあげる Basic 認証を設定したい動機については、皆様の置かれている状況により様々なパターンがあるかと思いますが、我々が活動する中でよくある例は `開発中の Web サイト (アプリ) なので、外部の人に見られては困る` というケースです。
先程も記載したように、CloudFront は世界中のエッジロケーションにコンテンツをキャッシュしてそこから配信を行う特性上、何もしなければ CloudFront から全世界に対してコンテンツが公開されてしまうことになるため、これを手軽に防ぐ手段として Basic 認証を設定することがあるかと思います。
Basic 認証を設定する際の選択肢
現状、 CloudFront に対して Basic 認証を設定する場合は下記の 2 パターンのうちいずれかを利用することができます。
Lambda@Edge
CloudFront Functions
Lambda@Edge は CloudFront の Regional Edge Location にて、クライアントから Origin への通信を Lambda を使ってカスタマイズすることができます。
Lambda@Edge の場合、Viewer request, Origin request, Origin response, Viewer response という 4 つのリクエスト/レスポンスをカスタマイズすることができ、Basic 認証を設定する場合には Viewer request に対して Lambda で処理をするように設定を行う必要があります。
各種リクエスト/レスポンスの詳細については下記の公式ドキュメントを参照してください。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-edge.html
次に CloudFront Functions ですが、Lambda@Edge が実行される Regional Edge Location よりも更にクライアントに近い Edge Location で処理が実行されるので、Regional Edge Cache の背後にいる Origin までを認識することはないため CloudFront Functions の、 Viewer request と Viewer response をトリガーに処理することのみができる仕様となっております。今回の題材となっている Basic 認証に関しては Origin に関わらず処理できる内容のため、こちらも Lambda@Edge と同様に Viewer request に対して設定する形となります。
上記の説明出てきた Regional edge location や Edge location に関しては下記、公式のドキュメントを参照ください。
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/HowCloudFrontWorks.html
Basic 認証で使用する場合の Lambda@Edge と CloudFront Functions の違い
結論から言ってしまうと、Basic 認証用途だけであれば CloudFront Functions のほうが設定上の制約が少ないため使いやすいと思います。
Lambda@Edge を使う場合は名前の通り Lambda が必要となるため、作成する必要があるリソースとしては以下のものが必要となってきます。
Lambda Function
Lambda の Execution Role
また、Lambda@Edge を使う際に気をつける点は下記のようなものがあります。
Lambda は us-east-1 リージョンにデプロイする必要がある
Lambda の明確な version を指定する必要がある ($latest が使用不可)
Lambda@Edge と CloudFront Distribution の紐付けを解除してもしばらくは Lambda@Edge を削除できない
この中で 3 点目については解説が必要かと思いますので以下で説明します。
実は 1 点目に挙げた us-east-1 リージョンにデプロイが必要という部分とも少し関わってくるのですが、Lambda@Edge は us-east-1 デプロイされたあと、Edge location にレプリカとして配備されます。レプリカがあることで世界中のどのロケーションからアクセスがあっても毎回 us-east-1 の Lambda に対して問い合わせが発生することなく、処理を実行することができます。ただし、このレプリカは CloudFront の Distribution と Lambda@Edge の紐付けが解除されてから数時間以内に削除される仕様で、Lambda@Edge はレプリカがすべて削除された状態でなければ削除することできないため注意が必要となるのです。
Lambda@Edge のレプリカについて、下記の公式のドキュメントもご確認いただけると幸いです。
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-delete-replicas.html
また、CloudFront Functions と Lambda@Edge の機能の比較についても公式でドキュメントがありますのでご確認いただければと思います。
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/edge-functions.html
CloudFormation でデプロイする場合のサンプル
CloudFormation で CloudFront Functions を構成するためのサンプルテンプレート (YAML) を紹介します。
テンプレート中に Lambda@Edge の場合の記載方法をコメントアウトしている状態で掲載しておりますので、Lambda@Edge を利用したい場合も参考にしていただけるかと思います。
AWSTemplateFormatVersion: "2010-09-09"
Description: "CloudFront with basic auth template"
Parameters:
BasicAuthUser:
Type: String
BasicAuthPassword:
Type: String
Resources:
HostingBucket:
Type: AWS::S3::Bucket
HostingBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref HostingBucket
PolicyDocument:
Id: AccessPolicyForHostingBucket
Version: "2012-10-17"
Statement:
- Sid: PublicReadForGetBucketObjects
Action: s3:GetObject
Effect: Allow
Resource: !Sub
- "${ARN}/*"
- { ARN: !GetAtt HostingBucket.Arn }
Principal:
AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${OriginAccessIdentity}"
OriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: "$cf-basic-auth-public-contents-distribution"
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
PriceClass: PriceClass_200
DefaultRootObject: index.html
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
DefaultTTL: 0
MaxTTL: 0
MinTTL: 0
ForwardedValues:
QueryString: false
FunctionAssociations:
- EventType: viewer-request
FunctionARN: !GetAtt AuthFunction.FunctionARN
# Lambda@Edge で basic 認証する場合
# LambdaFunctionAssociations:
# - EventType: viewer-request
# LambdaFunctionARN: !Ref AuthFunctionVersion
Origins:
- Id: S3Origin
DomainName: !GetAtt HostingBucket.RegionalDomainName
S3OriginConfig:
OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${OriginAccessIdentity}"
CustomErrorResponses:
- ErrorCode: 400
ResponseCode: 200
ErrorCachingMinTTL: 300
ResponsePagePath: /
- ErrorCode: 403
ResponseCode: 200
ErrorCachingMinTTL: 300
ResponsePagePath: /
- ErrorCode: 404
ResponseCode: 200
ErrorCachingMinTTL: 300
ResponsePagePath: /
AuthFunction:
Type: AWS::CloudFront::Function
Properties:
Name: "cf-basic-auth-auth-func"
AutoPublish: true
FunctionConfig:
Comment: "cf basic auth func"
Runtime: cloudfront-js-1.0
FunctionCode: !Sub |
function handler(event) {
var request = event.request;
var headers = request.headers;
var authUser = '${BasicAuthUser}';
var authPass = '${BasicAuthPassword}';
var authString = 'Basic ' + (authUser + ':' + authPass).toString('base64');
console.log('base64 auth string: ' + authString);
if (typeof headers.authorization == 'undefined' || headers.authorization.value != authString) {
var response = {
statusCode: 401,
statusDescription: 'Unauthorized',
headers: {
'www-authenticate': { value: 'Basic' }
}
};
console.log('request: ' + JSON.stringify(request));
return response;
}
var oldUri = request.uri;
var newUri = oldUri.replace(/\/$/, '\/index.html');
if (oldUri != newUri ) {
console.log('URI changed. old: ' + oldUri + ', new: ' + newUri);
request.uri = newUri;
}
return request;
};
# 以下、 Lambda@Edge で basic 認証する場合
# AuthFunctionRole:
# Type: AWS::IAM::Role
# Properties:
# AssumeRolePolicyDocument:
# Version: "2012-10-17"
# Statement:
# - Effect: Allow
# Principal:
# Service:
# - lambda.amazonaws.com
# - edgelambda.amazonaws.com
# Action:
# - sts:AssumeRole
# Path: /
# ManagedPolicyArns:
# - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# AuthFunction:
# Type: AWS::Lambda::Function
# Properties:
# Handler: index.handler
# Role: !GetAtt AuthFunctionRole.Arn
# # nodejs14.x は ZipFile での設定がエラーになるので一旦 12.x で設定
# Runtime: nodejs12.x
# MemorySize: 128
# Timeout: 3
# Description: "cf basic auth Lambda@Edge"
# Code:
# ZipFile: !Sub |
# 'use strict';
# exports.handler = (event, context, callback) => {
# const request = event.Records[0].cf.request;
# const headers = request.headers;
# const authUser = '${BasicAuthUser}';
# const authPass = '${BasicAuthPassword}';
# const authString = 'Basic ' + new Buffer.from(authUser + ':' + authPass).toString('base64');
# if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
# const body = 'Unauthorized';
# const response = {
# status: '401',
# statusDescription: 'Unauthorized',
# body,
# headers: {
# 'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}]
# },
# };
# console.log("request: " + JSON.stringify(request));
# callback(null, response);
# }
# var oldUri = request.uri;
# var newUri = oldUri.replace(/\/$/, '\/index.html');
# if ( oldUri != newUri ) {
# console.log('URI changed. old: ' + oldUri + ', new: ' + newUri);
# request.uri = newUri;
# }
# callback(null, request);
# };
# AuthFunctionVersion:
# Type: AWS::Lambda::Version
# Properties:
# FunctionName: !Ref AuthFunction
Outputs:
DistributionUrl:
Value: !Join
- ""
- - "https://"
- !GetAtt CloudFrontDistribution.DomainName
本テンプレートでデプロイ後、S3 に index.html を配置し、CloudFormation の Distribution に Web ブラウザでアクセスすることで Basic 認証の挙動を確認できると思います。
まとめ
今回は、CloudFront で配信されるコンテンツに Basic 認証を設定する方法を解説する中で、CloudFront Functions と Lambda@Edge の違いについても解説を行いました。これら Edge で起動できる処理は今回題材とした Basic 認証以外でも様々な用途があると思いますので、それぞれの機能の特徴を把握した上で最適なサービスを選択していただけると幸いです。