NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    AWS Lambdaカスタムリソースで基本認証用Lambda@Edgeを作成するAWS Cloudformationスタックを別リージョンにデプロイする

    小西秀和です。
    AWS LambdaカスタムリソースでAWS Cloudformationスタックを別リージョンにデプロイする」の記事でAWS Cloudformationスタックを別リージョンにデプロイするAWS Lambdaカスタムリソースの実装例を説明しました。

    また、その記事の続編である「AWS LambdaカスタムリソースでACM証明書を作成するAWS Cloudformationスタックを別リージョンにデプロイする」でカスタムリソースを使用して実際にAWS Certificate Manager(ACM)証明書をデプロイする方法を紹介しました。

    今回はさらにその続編という位置づけでカスタムリソースを使用してAmazon CloudFrontで基本認証を行うLambda@Edgeをデプロイする方法を紹介します。

    AWS Cloudformationスタックを別リージョンにデプロイするAWS Lambdaカスタムリソースについては元記事を参照してください。

    ※本記事および当執筆者のその他の記事で掲載されているソースコードは自主研究活動の一貫として作成したものであり、動作を保証するものではありません。使用する場合は自己責任でお願い致します。また、予告なく修正することもありますのでご了承ください。

    本記事で試す内容の概要図

    今回は次の内容をAWS Cloudformationとカスタムリソースを中心に試しています。

    • 呼出元Cloudformationスタックはカスタムリソースを呼び出し、基本認証をするLambda@EdgeのバージョンをCloudformationスタックでus-east-1に作成してARNを受け取る
    • 呼出元CloudformationスタックはAmazon S3をオリジンとするAmazon CloudFrontをLambda@EdgeバージョンのARNと関連付けて作成し、カスタムリソースで設定したIDとパスワードで基本認証できるようにする

    Lambdaカスタムリソースによる別リージョンへのLambda@Edge作成スタックデプロイと関連付けの例
    Lambdaカスタムリソースによる別リージョンへのLambda@Edge作成スタックデプロイと関連付けの例

    各ファイル(テンプレート、関数)の名称設定

    AWS CloudformationテンプレートやAWS Lambdaカスタムリソースそれぞれが一意に必要なものを識別して連携できれば各ファイルの名称は任意ですが、説明のために次のように各ファイルの名称を設定します。

    • 呼出元AWS Cloudformationテンプレート
      ⇒ OrgSourceOfRunCfnBasicAuthLambdaEdge.yml
    • Amazon CloudFront用のLambda@Edgeを作成するAWS CloudformationスタックをデプロイするAWS Lambdaカスタムリソース
      ⇒ CustomResourceToDeployCloudformationStack
    • Amazon CloudFront用のLambda@Edgeを作成するAWS Cloudformationテンプレート
      ⇒ CfnBasicAuthLambdaEdge.yml

    各ファイルやパラメータの例

    各ファイルやパラメータの例を記載していきます。
    特に入力パラメータに関しては入力形式を知っていただくための例なので、使用する場合は各パラメータを要件に合わせて設定する必要があります。

    呼出元AWS Cloudformationテンプレートへの入力パラメータ例

    後述するカスタムリソースの呼出元となるAWS Cloudformationテンプレートに入力するパラメータの例です。
    入力値は例なので使用する場合は各パラメータを要件に合わせて設定する必要があります。
    次にYAML形式にコメントする形で説明しています。
    これらのパラメータはAWSマネジメントコンソールから呼出元AWS Cloudformationテンプレートを実行する場合は、各パラメータを手動入力する必要があります。

    #作成するS3バケットのサフィックスに追加する環境名
    env: dev
    #静的ウェブサイトホスティングに使用するS3バケット名
    bucketName: cfnlambdaedge4cloudfront-20210822000000-hostingbucket
    #Lambda@Edgeを作成するスタックをus-east1にデプロイするLambdaカスタムリソースのARN
    runCfnInUsEast1LambdaARN: arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:CustomResourceToDeployCloudformationStack
    #Lambda@Edgeを作成するためにus-east1にデプロイするCloudformationスタック名
    runCfnInUsEast1StackName4LambdaEdge: CfnLambdaEdge4CloudFrontDemo
    #Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット名
    cfnTplS3Bucket4LambdaEdge: h-o2k
    #Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
    cfnTplS3Key4LambdaEdge: CfnBasicAuthLambdaEdge.yml
    #基本認証をするLambda@Edgeの関数名
    basicAuthFuncName4LambdaEdge: BasicAuthWithLambdaEdge
    #基本認証をするLambda@Edgeを関連付けるAmazon CloudFrontのDistribution ID。AWS Secrets Managerシークレットの一意識別で使用する。
    #※スタックの初回Create処理でAmazon CloudFrontを作成してから、2回目のUpdate処理で入力する
    basicAuthCloudFrontDistId4LambdaEdge: XXXXXXXXXXXXX
    #基本認証をするLambda@Edgeで認証するID。AWS Secrets Managerシークレットとして保管する。
    basicAuthID4LambdaEdge: Iam
    #基本認証をするLambda@Edgeで認証するパスワード。AWS Secrets Managerシークレットとして保管する。
    basicAuthPW4LambdaEdge: Nobody
    

    呼出元AWS Cloudformationテンプレート

    ファイル名:OrgSourceOfRunCfnBasicAuthLambdaEdge.yml

    カスタムリソースの呼出元となるAWS Cloudformationテンプレートの例を記載します。
    このテンプレートはAWS Amplify CLIでAmazon S3+Amazon CloudFrontの静的ウェブサイトホスティング環境を作成したときに生成されるJSONテンプレートをYAMLに変換し、カスタムリソースによる基本認証をするLambda@Edgeバージョン作成処理とCloudFrontへの関連付け処理を追加したものです。
    このYAMLファイルに追加した処理やポイントとなる箇所についてコメントを追記しておきます。
    AWS Amplify CLIによるAmazon S3+Amazon CloudFrontの作成については次の記事を参照してください。

    AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify CLI)

    AWSTemplateFormatVersion: '2010-09-09'
    Description: 'Hosting and Lambda@Edge resource stack creation'
    Parameters:
        env: #作成するS3バケットのサフィックスに追加する環境名
            Type: String
        bucketName: #静的ウェブサイトホスティングに使用するS3バケット名
            Type: String
        runCfnInUsEast1LambdaARN: #Lambda@Edgeを作成するスタックをus-east1にデプロイするLambdaカスタムリソースのARN
            Type: String
        runCfnInUsEast1StackName4LambdaEdge: #Lambda@Edgeを作成するためにus-east1にデプロイするCloudformationスタック名
            Type: String
        cfnTplS3Bucket4LambdaEdge: #Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット名
            Type: String
        cfnTplS3Key4LambdaEdge: #Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
            Type: String
        basicAuthFuncName4LambdaEdge: #基本認証をするLambda@Edgeの関数名
            Type: String
        basicAuthCloudFrontDistId4LambdaEdge: #基本認証をするLambda@Edgeを関連付けるAmazon CloudFrontのDistribution ID。AWS Secrets Managerシークレットの一意識別で使用する。
            Type: String                      #※スタックの初回Create処理でAmazon CloudFrontを作成してから、2回目のUpdate処理で入力する
        basicAuthID4LambdaEdge: #基本認証をするLambda@Edgeで認証するID。AWS Secrets Managerシークレットとして保管する。
            Type: String
        basicAuthPW4LambdaEdge: #基本認証をするLambda@Edgeで認証するパスワード。AWS Secrets Managerシークレットとして保管する。
            Type: String
    Conditions:
        ShouldNotCreateEnvResources:
            'Fn::Equals':
                -
                    Ref: env
                - NONE
        ShouldCreateBasicAuthLambdaEdge: #入力パラメータにAmazon CloudFrontのDistribution IDがある場合はTrueでLambda@Edgeを作成、ない場合はFalseでLambda@Edge作成をスキップ。
            'Fn::Not': ['Fn::Equals':[{Ref: basicAuthCloudFrontDistId4LambdaEdge}, '']]
    Resources:
        S3Bucket:
            Type: 'AWS::S3::Bucket'
            #DeletionPolicy: Retain
            Properties:
                BucketName:
                    'Fn::If':
                        - ShouldNotCreateEnvResources
                        - {Ref: bucketName}
                        - {'Fn::Join': ["", [{Ref: bucketName}, '-', {Ref: env}]]}
                WebsiteConfiguration:
                    IndexDocument: index.html
                    ErrorDocument: index.html
                CorsConfiguration:
                    CorsRules:
                        - {AllowedHeaders: [Authorization, Content-Length], AllowedMethods: [GET], AllowedOrigins: ['*'], MaxAge: 3000}
        PrivateBucketPolicy:
            Type: 'AWS::S3::BucketPolicy'
            DependsOn: OriginAccessIdentity
            Properties:
                PolicyDocument:
                    Id: MyPolicy
                    Version: '2012-10-17'
                    Statement:
                        - {Sid: APIReadForGetBucketObjects, Effect: Allow, Principal: {CanonicalUser: {'Fn::GetAtt': [OriginAccessIdentity, S3CanonicalUserId]}}, Action: 's3:GetObject', Resource: {'Fn::Join': ["", ['arn:aws:s3:::', {Ref: S3Bucket}, '/*']]}}
                Bucket:
                    Ref: S3Bucket
        OriginAccessIdentity:
            Type: 'AWS::CloudFront::CloudFrontOriginAccessIdentity'
            Properties:
                CloudFrontOriginAccessIdentityConfig:
                    Comment: CloudFrontOriginAccessIdentityConfig
        #Lambda@Edgeを作成するために呼び出すカスタムリソース
        RunCfnInUsEast1Func4LambdaEdge:
            Type: 'Custom::RunCfnInUsEast1Func4LambdaEdge'
            Properties:
                ServiceToken:
                    Ref: runCfnInUsEast1LambdaARN
                StackName:
                    Ref: runCfnInUsEast1StackName4LambdaEdge
                CfnTplS3Bucket:
                    Ref: cfnTplS3Bucket4LambdaEdge
                CfnTplS3Key:
                    Ref: cfnTplS3Key4LambdaEdge
                CloudFrontDistId:
                    Ref: basicAuthCloudFrontDistId4LambdaEdge
                BasicAuthFuncName:
                    Ref: basicAuthFuncName4LambdaEdge
                BasicAuthID:
                    Ref: basicAuthID4LambdaEdge
                BasicAuthPW:
                    Ref: basicAuthPW4LambdaEdge
                IsSkip: ##入力パラメータにAmazon CloudFrontのDistribution IDがない場合はカスタムリソースにIsSkip:trueを送信し、Lambda@Edge作成処理をスキップする。
                    'Fn::If':
                        - ShouldCreateBasicAuthLambdaEdge
                        - "false"
                        - "true"
        CloudFrontDistribution:
            Type: 'AWS::CloudFront::Distribution'
            DependsOn:
              - S3Bucket
              - OriginAccessIdentity
              - RunCfnInUsEast1Func4LambdaEdge
            Properties:
                DistributionConfig:
                    HttpVersion: http2
                    Origins:
                        - {DomainName: {'Fn::GetAtt': [S3Bucket, RegionalDomainName]}, Id: hostingS3Bucket, S3OriginConfig: {OriginAccessIdentity: {'Fn::Join': ["", [origin-access-identity/cloudfront/, {Ref: OriginAccessIdentity}]]}}}
                    Enabled: 'true'
                    DefaultCacheBehavior:
                        AllowedMethods: [DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT]
                        TargetOriginId: hostingS3Bucket
                        ForwardedValues: {QueryString: 'false'}
                        ViewerProtocolPolicy: redirect-to-https
                        DefaultTTL: 86400
                        MaxTTL: 31536000
                        MinTTL: 60
                        Compress: true
                        LambdaFunctionAssociations:
                          'Fn::If':
                            - ShouldCreateBasicAuthLambdaEdge
                            - [{EventType: viewer-request, IncludeBody: 'true', LambdaFunctionARN: {'Fn::GetAtt': [RunCfnInUsEast1Func4LambdaEdge, LambdaFunctionVersionArn]}}]
                            - { Ref: 'AWS::NoValue' }
                    DefaultRootObject: index.html
                    CustomErrorResponses:
                        - {ErrorCachingMinTTL: 300, ErrorCode: 400, ResponseCode: 200, ResponsePagePath: /}
                        - {ErrorCachingMinTTL: 300, ErrorCode: 403, ResponseCode: 200, ResponsePagePath: /}
                        - {ErrorCachingMinTTL: 300, ErrorCode: 404, ResponseCode: 200, ResponsePagePath: /}
    Outputs:
        Region:
            Value:
                Ref: 'AWS::Region'
        HostingBucketName:
            Description: 'Hosting bucket name'
            Value:
                Ref: S3Bucket
        WebsiteURL:
            Value:
                'Fn::GetAtt':
                    - S3Bucket
                    - WebsiteURL
            Description: 'URL for website hosted on S3'
        S3BucketSecureURL:
            Value:
                'Fn::Join':
                    - ""
                    -
                        - 'https://'
                        - {'Fn::GetAtt': [S3Bucket, DomainName]}
            Description: 'Name of S3 bucket to hold website content'
        CloudFrontDistributionID:
            Value:
                Ref: CloudFrontDistribution
        CloudFrontDomainName:
            Value:
                'Fn::GetAtt':
                    - CloudFrontDistribution
                    - DomainName
        CloudFrontSecureURL:
            Value:
                'Fn::Join':
                    - ""
                    -
                        - 'https://'
                        - {'Fn::GetAtt': [CloudFrontDistribution, DomainName]}
        CloudFrontOriginAccessIdentity:
            Value:
                Ref: OriginAccessIdentity
        CustomResourceSkiped:
            Value:
                'Fn::If':
                    - ShouldCreateBasicAuthLambdaEdge
                    - "false"
                    - "true"
    
    

    呼出元AWS Cloudformationテンプレートからカスタムリソースに渡される引数

    前述の呼出元AWS Cloudformationテンプレートのカスタムリソース(RunCfnInUsEast1Func4LambdaEdge)から渡される引数は以下になります。
    StackName、cfnTplS3Bucket、cfnTplS3Key、CloudFrontDistId、BasicAuthFuncName、BasicAuthID、BasicAuthPW、IsSkip

    StackName:基本認証用Lambda@Edgeなどリソースを作成するCloudformationスタックの名称
    CfnTplS3Bucket:基本認証用Lambda@Edgeなどリソースを作成するCloudformationテンプレートがあるS3バケット
    CfnTplS3Key:基本認証用Lambda@Edgeなどリソースを作成するCloudformationテンプレートがあるS3バケット配下のオブジェクトキー
    CloudFrontDistId:認証情報を格納するAWS Secrets Managerシークレットに使用する基本認証用Lambda@Edgeバージョンと関連付けるAmazon CloudFrontのDistribution ID
    BasicAuthFuncName:基本認証用Lambda@Edgeの関数名
    BasicAuthID:基本認証用Lambda@Edgeでの認証に使用するID。AWS Secrets Managerシークレットとして保管する。
    BasicAuthPW:基本認証用Lambda@Edgeでの認証に使用するパスワード。AWS Secrets Managerシークレットとして保管する。
    IsSkip:Amazon CloudFrontのDistribution IDが発行されていない初回Createの場合に基本認証用Lambda@Edge作成をスキップするために使用する。trueの場合は処理を実行せずスキップする。

    これらを設定した上で呼出元AWS Cloudformationテンプレートからカスタムリソースに渡るイベントのフォーマット例を次に示します。

    {
      "RequestType": "Create",
      "ResponseURL": "http://pre-signed-S3-url-for-response",
      "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/stack-name/guid",
      "RequestId": "unique id for this create request",
      "ResourceType": "Custom::TestResource",
      "LogicalResourceId": "MyTestResource",
      "ResourceProperties": {
        "ServiceToken": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:CustomResourceToDeployCloudformationStack",
        "StackName": "CfnAcm4CloudFrontDemo",
        "CfnTplS3Bucket": "h-o2k",
        "CfnTplS3Key": "CfnACMCertificate.yml",
        "CloudFrontDistId": "XXXXXXXXXXXXX",
        "BasicAuthFuncName": "BasicAuthWithLambdaEdge",
        "BasicAuthID": "Iam",
        "BasicAuthPW": "Nobody",
        "IsSkip": "false"
      }
    }
    

    カスタムリソースではこのイベントを受けて処理を実行します。

    基本認証用Lambda@Edgeバージョンを作成するAWS Cloudformationスタックをus-east-1にデプロイするAWS Lambdaカスタムリソース

    関数名:CustomResourceToDeployCloudformationStack

    カスタムリソースは「AWS LambdaカスタムリソースでAWS Cloudformationスタックを別リージョンにデプロイする」で作成したものを使用します。
    このカスタムリソース内でAmazon S3バケットに保存されている基本認証用Lambda@Edgeバージョンを作成するCloudformationテンプレートを読み込み、呼出元Cloudformationスタックから渡された引数を使用してus-east-1に基本認証用Lambda@Edgeバージョンを作成するCloudformationスタックをデプロイします。
    実行したスタックが完了し、基本認証用Lambda@Edgeバージョンが作成されるとそのARNを呼出元に返却して呼出元で作成しているAWS CloudFrontに関連付けます。

    基本認証用Lambda@Edgeを作成するAWS Cloudformationテンプレート

    ファイル名:CfnBasicAuthLambdaEdge.yml

    認証情報を保管するAWS Secrets Managerと基本認証用Lambda@Edgeを作成し、呼出元のAmazon CloudFrontに関連付けるLambdaバージョンを発行するまでを実施するAWS Cloudformationテンプレートは次のようになります。
    これをS3バケットに保存しておき、カスタムリソースから読み込んでAWS Cloudformationスタックをus-east-1にデプロイします。

    AWSTemplateFormatVersion: '2010-09-09'
    Description: 'Stack creation of Lambda@Edge Version and Secrets Manager Secret'
    Parameters:
      CloudFrontDistId: #基本認証用Lambda@Edgeを呼び出すAmazon CloudFrontのDistribution ID。AWS Secrets ManagerのシークレットIDに使用する。
        Type: String    #※このCloudFormationテンプレートではAmazon CloudFrontとLambda@Edgeバージョンの関連付けはしないため注意。関連付けは呼出元で実施する想定。
      BasicAuthFuncName: #基本認証用Lambda@Edgeの関数名
        Type: String
      BasicAuthID: #AWS Secrets Managerシークレットに保管し、基本認証で使用するID。
        Type: String
      BasicAuthPW: #AWS Secrets Managerシークレットに保管し、基本認証で使用するパスワード。
        Type: String
    Resources:
      LambdaEdgeBasicAuth:
        Type: AWS::Lambda::Function
        DependsOn: 
          - SecretsManagerSecret
          - LambdaEdgeBasicAuthRole
        Properties:
          FunctionName: {Ref: BasicAuthFuncName}
          Runtime: python3.8
          MemorySize: 128 #Lambda@Edgeのクォータ最大値を設定
          Timeout: 5 #Lambda@Edgeのクォータ最大値を設定
          Role: !GetAtt LambdaEdgeBasicAuthRole.Arn
          Handler: index.lambda_handler
          Code:
            ZipFile: |
              # 基本認証を実施するLambda@Edgeの関数
              import json
              import boto3
              import base64
              # ID、パスワードを保管し、認証に使用するAWS Secrets Managerを扱うクライアントを作成
              asm_client = boto3.client('secretsmanager', region_name='us-east-1')
              # エラーの場合のレスポンスを定義
              err_response = {
                  'status': '401',
                  'statusDescription': 'Unauthorized',
                  'body': 'Authentication Failed.',
                  'headers': {
                      'www-authenticate': [
                          {
                            'key': 'WWW-Authenticate',
                            'value': 'Basic realm="Basic Authentication"'
                          }
                      ]
                  }
              }
              
              def lambda_handler(event, context):
                  try:
                      print('event:')
                      print(event)
                      # イベントからCloudFrontのリクエストを取得
                      request = event['Records'][0]['cf']['request']
                      # イベントからCloudFrontのDistribution IDを取得
                      cf_dist_id = event['Records'][0]['cf']['config']['distributionId']
                      # リクエストからヘッダーを取得
                      headers = request['headers']
                      if (headers.get('authorization') != None):
                          # ヘッダーにauthorizationがあれば認証を試行する
                          # headers['authorization'][0]['value']の中身は「Basic <base64でエンコードされた[ID:Password]の文字列>」
                          # authorizationの内容を分解してID、パスワードを取り出す
                          target_credentials_str = headers['authorization'][0]['value'].split(
                              " ")
                          target_credentials = base64.b64decode(
                              target_credentials_str[1]).decode().split(":")
                          target_id = target_credentials[0]
                          target_pw = target_credentials[1]
              
                          # 取得したCloudFrontのDistribution IDおよび基本認証で入力されたIDでシークレットを取得する
                          response = asm_client.get_secret_value(
                              SecretId='CloudFrontBasicAuth/' +
                              cf_dist_id + '/' + str(target_id)
                          )
                          # シークレットが取得でき、格納されている文字列が基本認証で入力されたパスワードと一致すれば認証成功としてリクエストを返却する。
                          # それ以外の場合はエラーレスポンスを返す。
                          if (response.get('SecretString') != None):
                              secret_string = json.loads(response['SecretString'])
                              if (secret_string.get('Password') == target_pw):
                                  return request
                              else:
                                  return err_response
                          else:
                              return err_response
              
                      else:
                          return err_response
              
                  except Exception as e:
                      print("Exception:")
                      print(e)
              
                      return err_response
              
      LambdaEdgeBasicAuthVersion: #呼出元スタックでAmazon CloudFrontと関連付けるためのLambdaEdgeバージョンを作成する
        Type: AWS::Lambda::Version
        DependsOn: 
          - LambdaEdgeBasicAuth
        Properties:
          FunctionName: !Ref LambdaEdgeBasicAuth
          Description: 'Basic Auth Lambda Edge for Amazon CloudFront'
      LambdaEdgeBasicAuthRole: #基本認証用Lambda@Edgeに適用するIAMロールとIAMポリシーの設定
        Type: AWS::IAM::Role
        Properties:
          RoleName: !Sub 'LambdaEdgeBasicAuthRole4${BasicAuthFuncName}'
          Path: /
          AssumeRolePolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Principal:
                  Service:
                    - edgelambda.amazonaws.com
                    - lambda.amazonaws.com
                Action:
                  - sts:AssumeRole
          ManagedPolicyArns:
            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
          Policies:
          - PolicyName: LambdaEdgePolicy
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
              - Effect: Allow
                Action:
                - iam:CreateServiceLinkedRole
                Resource:
                - '*'
              - Effect: Allow
                Action:
                - lambda:GetFunction
                - lambda:EnableReplication
                Resource:
                - !Sub 'arn:aws:lambda:us-east-1:${AWS::AccountId}:function:${BasicAuthFuncName}'
              - Effect: Allow
                Action:
                - cloudfront:UpdateDistribution
                Resource:
                - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistId}'
          - PolicyName: SecretsManagerGetSecretValue
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
              - Effect: Allow
                Action:
                - secretsmanager:GetSecretValue
                Resource:
                - !Sub 'arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:CloudFrontBasicAuth/${CloudFrontDistId}/*'
      SecretsManagerSecret: #基本認証に使用するID、パスワードを格納するAWS Secrets Managerシークレットの作成
        Type: 'AWS::SecretsManager::Secret'
        Properties:
          Name: !Sub CloudFrontBasicAuth/${CloudFrontDistId}/${BasicAuthID}
          SecretString: !Sub '{"Password":"${BasicAuthPW}"}'
          Description: "SecretsManagerSecret of LambdaEdgeBasicAuth"
    Outputs:
      Region:
        Value:
          Ref: 'AWS::Region'
      LambdaFunctionArn:
        Value:
          Ref: LambdaEdgeBasicAuth #基本認証用Lambda@EdgeのARN
      LambdaFunctionVersionArn:
        Value:
          Ref: LambdaEdgeBasicAuthVersion #基本認証用Lambda@EdgeバージョンのARN
      SecretsManagerSecretArn:
        Value:
          Ref: SecretsManagerSecret #基本認証情報を格納したAWS Secrets ManagerシークレットのARN
          
    

    基本認証をするLambda@Edgeの概要

    上記のCloudformationテンプレートのうち「ZipFile: |」の下から「LambdaEdgeBasicAuthVersion: 」の上までが基本認証をするLambda@Edgeの関数部分です。
    CloudformationテンプレートではデプロイするAWS Lambda関数のコードは、ZipFileプロパティによるテンプレートへの直書き、S3バケットのオブジェクト(バージョン含む)、Amazon ECRのコンテナイメージのいずれかの方法で指定することができます。
    この記事の例ではブログ上での説明を簡略化するためZipFileプロパティによる直書きを選択しました。
    今回の基本認証をするLambda@Edgeの特徴には次のような点があります。

    • 基本認証用のIDとパスワードはAWS Secrets Managerから取得する
    • 認証に使用するAWS Secrets ManagerのシークレットIDを次のフォーマットで取得を試みる
      CloudFrontBasicAuth/<Lambda@Edgeを関連付けたCloudFrontのDistribution ID>/<ブラウザから入力された基本認証用のID>
    • シークレットIDが存在し、シークレット内に次のように格納されている基本認証用のパスワードがブラウザから入力されたパスワードと一致するかを確認する
      {
        "Password": "<基本認証用のパスワード>"
      }
      

    構築手順

    1. AWS Lambda関数「CustomResourceToDeployCloudformationStack」を用意しておく
      基本認証用Lambda@Edgeバージョンを作成するAWS Cloudformationスタックをus-east-1にデプロイするAWS Lambdaカスタムリソースを予め準備しておく
    2. 基本認証用Lambda@Edgeバージョンを作成するCloudformationテンプレートファイル「CfnBasicAuthLambdaEdge.yml」をS3バケットに置く
      基本認証用Lambda@Edgeバージョンを作成するAWS CloudformationテンプレートをAmazon S3バケット(前述のパラメータ例では「h-o2k」)に予め配置しておく
    3. 呼出元Cloudformationテンプレートファイル「OrgSourceOfRunCfnBasicAuthLambdaEdge.yml 」をAWS Cloudformationで実行する
      呼出元CloudformationテンプレートをAWSマネジメントコンソールなどからパラメータを入力の上でAWS Cloudformationスタックとして実行する
    4. 静的ウェブサイトホスティングに使用するS3バケットにコンテンツを追加する
      静的ウェブサイトホスティング用S3バケット(前述のパラメータ例では「cfnlambdaedge4cloudfront-20210822000000-hostingbucket-dev」)にindex.htmlなどコンテンツを追加する。
      (今回の例では「CfnLambdaEdge4CloudFrontDemo」とだけ表示されるindex.htmlを追加しました。)
    5. 初回実行(Create処理でAmazon CloudFrontが作成され、Distribution IDが発行される。カスタムリソースによる基本認証用Lambda@Edgeバージョン作成は実行しない。)
      最初の呼出元AWS Cloudformationスタックの実行では「basicAuthCloudFrontDistId4LambdaEdge」にAmazon CloudFrontのDistribution IDを入力しないことで、基本認証用Lambda@Edgeを作成するAWS Cloudformationスタックの作成処理はスキップして、それ以外のリソースを作成します。

      ■初回実行時(Create)のパラメータ入力例
      env: dev
      bucketName: cfnlambdaedge4cloudfront-20210822000000-hostingbucket
      runCfnInUsEast1LambdaARN: arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:CustomResourceToDeployCloudformationStack
      runCfnInUsEast1StackName4LambdaEdge: CfnLambdaEdge4CloudFrontDemo
      cfnTplS3Bucket4LambdaEdge: h-o2k
      cfnTplS3Key4LambdaEdge: CfnBasicAuthLambdaEdge.yml
      basicAuthFuncName4LambdaEdge: BasicAuthWithLambdaEdge
      basicAuthCloudFrontDistId4LambdaEdge: #初回時は入力しない(Distribution IDが発行されていないので入力できない)
      basicAuthID4LambdaEdge: Iam
      basicAuthPW4LambdaEdge: Nobody
      
    6. 2回目実行(Update処理でAmazon CloudFrontのDistribution IDをカスタムリソースに渡し、基本認証用Lambda@Edgeバージョンを作成する。)
      最初の呼出元AWS Cloudformationスタックの実行では「basicAuthCloudFrontDistId4LambdaEdge」に初回実行で発行されたAmazon CloudFrontのDistribution IDを入力することで、基本認証用Lambda@Edgeを作成するAWS Cloudformationスタックの作成処理を実行して、基本認証用Lambda@Edgeのバージョンと呼出元のAmazon CloudFrontを関連付けます。

      ■2回目実行時(Update)のパラメータ入力例
      #2回目実行時(Update)はすべてのパラメータを入力
      env: dev
      bucketName: cfnlambdaedge4cloudfront-20210822000000-hostingbucket
      runCfnInUsEast1LambdaARN: arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:CustomResourceToDeployCloudformationStack
      runCfnInUsEast1StackName4LambdaEdge: CfnLambdaEdge4CloudFrontDemo
      cfnTplS3Bucket4LambdaEdge: h-o2k
      cfnTplS3Key4LambdaEdge: CfnBasicAuthLambdaEdge.yml
      basicAuthFuncName4LambdaEdge: BasicAuthWithLambdaEdge
      basicAuthCloudFrontDistId4LambdaEdge: XXXXXXXXXXXXX
      basicAuthID4LambdaEdge: Iam
      basicAuthPW4LambdaEdge: Nobody
      
    7. 結果確認
      正しく実行されていればAmazon CloudFrontのURLにアクセスすると基本認証のダイアログが表示され、パラメータで指定した基本認証のIDとパスワードで認証を通過します。
      正しく動作しない場合は呼出元AWS Cloudformationスタックのイベント内容、カスタムリソースでデプロイしたスタックのイベント内容、AWS LambdaカスタムリソースのAmazon CloudWatch Logsの内容、AWS CloudTrailのログから原因を特定して不具合を修正します。

    削除手順

    今回のAWS CloudformationテンプレートではLambda@EdgeとAmazon CloudFrontの関連付け解除とカスタムリソースの削除の順序制御をしていないため、一度にすべてのスタックを削除することができません。
    そのため、削除する場合は次の手順のように関連付けを解除した後、呼出元AWS Cloudformationスタックと基本認証用Lambda@Edgeバージョンを作成したスタックをそれぞれ削除する必要があります。

    1. 呼出元AWS CloudformationスタックのパラメータでAmazon CloudFrontのDistribution IDを空にしてUpdate処理をする
      呼出元AWS Cloudformationスタックのパラメータ「basicAuthCloudFrontDistId4LambdaEdge」を空にしてスタックを更新する。
      us-east-1の基本認証用Lambda@Edgeのスタックの削除はCloudFrontとの関連付けがあるので失敗するが、呼び出し元のスタックではAmazon CloudFrontとLambda@Edgeの関連付けが削除される。
    2. 呼出元AWS Cloudformationスタックを削除する
      呼出元AWS Cloudformationスタックと基本認証用Lambda@Edgeのスタックの関連付けが解除されているため、呼出元AWS Cloudformationスタックが削除できる。
    3. us-east-1で基本認証用Lambda@Edgeバージョンを作成したCloudformationスタックを削除する
      呼出元AWS Cloudformationスタックと基本認証用Lambda@Edgeのスタックの関連付けが解除されているため、基本認証用Lambda@Edgeバージョンを作成したCloudformationスタックを単独で削除する。


    参考:
    What is AWS CloudFormation? - AWS CloudFormation
    Tech Blog with related articles referenced
    AWS CloudFormation Templates and AWS Lambda Custom Resources for Associating AWS Certificate Manager, Lambda@Edge, and AWS WAF with a Website on Amazon S3 and Amazon CloudFront Cross-Region
    Deploy AWS Cloudformation Stack Cross-Region with AWS Lambda Custom Resources

    まとめ

    今回は「AWS LambdaカスタムリソースでAWS Cloudformationスタックを別リージョンにデプロイする」の記事で説明したAWS Cloudformationスタックを別リージョンにデプロイするAWS Lambdaカスタムリソースを使用し、基本認証をするLambda@Edgeバージョンと認証情報を格納するAWS Secrets Managerをus-east-1で作成して呼出元AWS Cloudformationスタックで管理しているAmazon CloudFrontに関連付ける例を紹介しました。
    次回は別リージョンにAmazon S3バケットを作成し、呼出元リージョンのAmazon S3バケットとクロスリージョンレプリケーションおよびAmazon CloudFrontオリジンフェイルオーバーを構成する例を紹介しようと考えています。

    Written by Hidekazu Konishi
    Hidekazu Konishi (小西秀和), a Japan AWS Top Engineer and a Japan All AWS Certifications Engineer

    執筆者小西秀和

    Japan AWS Top Engineer / Japan All AWS Certifications Engineer(AWS認定全冠)として、知識と実践的な経験を活かし、AWSの活用に取り組んでいます。
    Personal Tech Blog | Web Tools Collection | 
    小西 秀和 - Amazon著者ページ | [B! Bookmark] |