見出し画像

CloudFront で Basic 認証する場合の選択肢

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

AWS Blog からの引用

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 認証以外でも様々な用途があると思いますので、それぞれの機能の特徴を把握した上で最適なサービスを選択していただけると幸いです。

この記事が気に入ったらサポートをしてみませんか?