CloudFormation で Basic認証で利用できる CloudFront + S3 の静的コンテンツ配信インフラを作ってみた。

はじめに

AWSチームのすずきです。

Basic認証で実現するLamda@Edge、静的コンテンツの配信インフラとなるS3 + CloudFront、 S3コンテンツのアップロード操作用のIAMユーザを、CloudFormationを利用して設置する機会がありましたので、 紹介させていただきます。

構成図

参考

Amazon CloudFrontとAWS Lambda@EdgeでSPAのBasic認証をやってみる

CloudFormation で OAI を使った CloudFront + S3 の静的コンテンツ配信インフラを作る

特定のS3のフォルダにのみアクセス権のあるIAMユーザーを使ってCyberduckから参照する

作業手順

テンプレート

CreateStack

  • バージニアリージョンでCloudFormationを実行します。
  • 先にダウンロードしたCloudFormationテンプレートを 「ファイルで選択」で指定します。

  • CloudFormationのパラメータで指定した文字列を、Lambdaに埋め込み、Basic認証で利用するID、パスワード文字列として利用します。
  • Lambda、CloudFormationのIAM権限をもつユーザに対し、Basic認証のパスワードを隠匿する必要がある場合には、別の仕組みをご利用ください。

  • 確認画面に進み、最後に今回のCloudFormation、Lambda関数に付与するIAMロール、S3アクセス用のIAMユーザを作成するため、必要な承認を行い「作成」を実施します。

S3操作

  • 作成完了後、<CloudFormationスタック名>で始まるS3バケットが、CloudFront公開用として作成されています。

  • AWSコンソールから、ファイルをアップロードする事が可能です。

IAMユーザ

  • S3操作専用のIAMユーザが作成されます。
  • S3専用のIAMユーザは、ツールの誤操作などで意図せぬバケットポリシーやACLの変更を禁止する設定をしています。

  • S3操作用のツールで利用するアクセスキーの発行が可能です。

動作確認

  • CloudFrontのURL「https://xxxxx.cloudfront.net/xxxxx.png」にアクセスすると、Basic認証のダイアログが表示されます。
  • 正しいID、パスワードを入力する事で、S3にアップロード済みコンテンツの表示が可能です。

  • LambdaEdge関数のログ、日本国内からのアクセスの大半は東京リージョンのログとして確認できます。

変更手順

直接更新

  • バージニアリージョンのLambda関数を選択します。

  • インラインエディタでコードを修正後、「保存」します。

  • 新しいバージョンを発行します

  • Lambda@Edgeを利用するCloudFrontのDistributionを指定します。

  • 「Behavior」設定より、Lambda@Edgeを実行するパスを指定し「Edit」します。

  • 「Lambda Function ARN」の設定、末尾のバージョン欄を先に発行したバージョンに更新します。

CloudFormation更新

  • 設置したCloudFormationスタックを「更新」操作する事で、パラメータとして指定する認証文字列や、Lambda関数の更新が可能です。

  • 現時点(2018年8月)の制約により、2種類(Blue/Green)と定義した「AWS::Lambda::Version」を交互に作り直す事で、最新バージョンのLambda関数のCloudFrontへの紐付けを実現しました。

  • 15分程度で、Lambda@Edgeの更新処理は完了します。

削除

  • 配信インフラが不要になった場合、CloudFormationの削除により、関連リソースを撤去する事が可能です。
  • Lambda@Edge、各国に展開されているレプリカの削除に数時間を要する関係で、CloudFormationの削除時点ではエラーが発生する場合があります。
  • CloudFormationの削除時にエラーが発生した場合、スタックはそのままとし、半日〜1日経過後に再度削除をお試しください。

Lambda@Edge 関数とレプリカの削除

  • 今回、S3バケット(コンテンツ、アクセスログ保管用)は、CloudFormationの削除対象外としています。別途手動削除してください。

まとめ

CloudFormationを利用して、Lambda@Edge関数を簡単に設置、管理できる事を確認できました。

今回紹介させて頂いたBasic認証を実現するテンプレート、低コストで 検索エンジンのクローラやURLを知り得た第三者からの参照を回避できる一定の効果は期待できますが、 パスワードの管理などは非常に簡易なものとなります。

重要度の高いコンテンツ保護の必要性がある場合には、より適切な認証、認可を実現する仕組みの追加や、 別途VPNなどセキュアなネットワークを利用する事をおすすめします。

リソース詳細

CloudFormationで設置される主要リソースの解説です。

S3

  • S3は非公開で利用する前提で、バケット名の重複を避けるため、リージョン、アカウントIDを付与しています。
  S3Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      BucketName: !Sub '${AWS::StackName}-${AWS::Region}-${AWS::AccountId}'
      VersioningConfiguration:
        Status: Enabled
  • バケットポリシーで、特定のOAI(origin-access-identity) をもつCloudFrontからのS3オリジンとして利用出来るようにしました。
  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref 'S3Bucket'
      PolicyDocument:
        Statement:
          - Action: s3:GetObject
            Effect: Allow
            Resource: !Sub 'arn:aws:s3:::${S3Bucket}/*'
            Principal:
              AWS: !Sub 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity
                ${CloudFrontOriginAccessIdentity}'
  • Lambda@Edgeとの同時設定のためS3はバージニアに設置しましたが、国内からのS3アクセス遅延が問題となる場合、別途東京リージョンに設置したS3バケットを利用することも可能です。

CloudFront

  • 平文(HTTP)でBASIC認証パスワードが流れる事は望ましくないため、HTTPS専用に設定しました。
        DefaultCacheBehavior:
          ViewerProtocolPolicy: https-only
  • OAI(origin-access-identity) を発行、S3オリジンとして指定したS3アクセスに利用します。
  • DNS更新ラグの影響が少ないと予想される「s3-us-east-1.amazonaws.com」とリージョンを含むFQDNをS3オリジンに指定しました。
  • CloudFormationで設定したCloudFront、S3のリダイレクトに起因するエラーが表示される場合、数時間待機の後、Invalidationをお試しください。
        Origins:
          - Id: S3Origin
            DomainName: !Sub '${S3Bucket}.s3-${AWS::Region}.amazonaws.com'
            S3OriginConfig:
              OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}'
  • Invalidation無しに利用出来る検証環境用として、CDNのキャッシュは極力機能しないように設定しました。
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: true
            Headers:
              - If-Modified-Since
              - If-None-Match
              - Upgrade-Insecure-Requests
              - User-Agent
            Cookies:
              Forward: all
          ViewerProtocolPolicy: https-only
          DefaultTTL: '0'
          MaxTTL: '0'
          MinTTL: '0'
  • Basic認証の必要性がないパスは、LambdaEdgeを実行しないBehaviorとして設定しました
        CacheBehaviors:
          - AllowedMethods:
              - GET
              - HEAD
            TargetOriginId: S3Origin
            ForwardedValues:
              QueryString: false
            PathPattern: '/favicon.ico'

Lambda

  • Lambda@Edgeとして必要とする最小限のロールを用意、過剰な権限をLambdaに与えないようにしています。
  • Webコンソールのインラインエディタを利用可能とするため、ソースはテンプレートに記載する形としました。

  • Basic認証の判定は、以下のソースを流用させて頂きました。

    lmakarov/lambda-basic-auth.js

  • Webホスティングを有効にしたS3で利用できるインデックスドキュメントの代用として、以下のソースを参考に処理を追加しています。

Implementing Default Directory Indexes in Amazon S3-backed Amazon CloudFront Origins Using Lambda@Edge

  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt 'LambdaRole.Arn'
      Code:
        ZipFile: !Sub |
          'use strict';
          exports.handler = (event, context, callback) => {
            // Get request and request headers
            const request = event.Records[0].cf.request;
            const headers = request.headers;
            // Configure authentication
            const authUser = '${AuthUser}';
            const authPass = '${AuthPass}';
            // Construct the Basic Auth string
            const authString = 'Basic ' + new Buffer(authUser + ':' + authPass).toString('base64');
            // Require Basic authentication
            if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
              const body = 'Unauthorized';
              const response = {
                status: '401',
                statusDescription: 'Unauthorized',
                body: body,
                headers: {
                  'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}]
                },
              };
              // Debug log
              console.log("request: " + JSON.stringify(request));
              callback(null, response);
            }
            // Instead of index document processing
            var olduri = request.uri;
            var newuri = olduri.replace(/\/$/, '\/index.html');
            if ( olduri != newuri ) {
              console.log("Old URI: " + olduri);
              console.log("New URI: " + newuri);
            }
            request.uri = newuri;
            // Continue request processing if authentication passed
            callback(null, request);
          };
      Runtime: nodejs6.10
      MemorySize: 128
      Timeout: 1
      Description: Basic authentication with Lambda@Edge

Lambda::Version

  • CloudFrontでLambda@Edgeを利用する場合、Lambdaパージョンの明示を必須とします。
  • 2018年8月時点、CloudFormation管理下のLambda関数を更新しても、作成済みのVersionリソースは以前のまま保持され、$Latestに追従させる事ができない制限が存在します。
  • CloudFormation を利用して Lambda@Edge関数を更新する暫定対策として、パラメータ「SelectLambdaDeployment」を変更する事とする事で、Versionリソースの作り直しを発動させ、最新のLambda関数($Latest)をCloudFrontへの紐づけを実現しました。
Conditions:
  CloudFrontAliaseEnable: !Not [!Equals [!Ref 'CloudFrontAliase', 'none']]
  LambdaVersionIsBlue: !Equals [!Ref 'SelectLambdaDeployment', 'blue']
  LambdaVersionIsGreen: !Equals [!Ref 'SelectLambdaDeployment', 'green']

Resources:
  LambdaFunctionVersionBlue:
    Type: AWS::Lambda::Version
    Condition: LambdaVersionIsBlue
    Properties:
      FunctionName: !Ref 'LambdaFunction'

  LambdaFunctionVersionGreen:
    Type: AWS::Lambda::Version
    Condition: LambdaVersionIsGreen
    Properties:
      FunctionName: !Ref 'LambdaFunction'

  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          LambdaFunctionAssociations:
            - EventType: viewer-request
              LambdaFunctionARN: !If
                - LambdaVersionIsBlue
                - !Ref 'LambdaFunctionVersionBlue'
                - !Ref 'LambdaFunctionVersionGreen'

IAM

  • 特定のS3バケットのみフルアクセス可能とする、Cyberduck等を想定した設定を行いました。
  • 意図せぬACL設定、バケットポリシーが反映されることを回避するため、バケット、ACL設定の変更は禁止としています。
  IamGroup:
    Type: AWS::IAM::Group
    Properties:
      GroupName: !Sub 'iam-group-s3-access-${S3Bucket}'
      Policies:
      - PolicyName: PolicieAllow
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - s3:List*
            - s3:GetBucketLocation
            Resource:
            - arn:aws:s3:::*
          - Effect: Allow
            Action:
            - s3:*
            Resource:
            - !Sub 'arn:aws:s3:::${S3Bucket}/*'
          - Effect: Deny
            Action:
            - s3:PutBucket*
            - s3:PutObjectAcl
            - s3:PutObjectVersionAcl
            Resource:
            - arn:aws:s3:::*

S3のGUIクライアントを利用するのに必要なIAM Policy

テンプレート全文