CloudFront + Lambda@EdgeでBasic認証するためのCloudFormationテンプレート


Lambda@EdgeでBasic認証したいと思ったが手でポチポチ作るのも面倒なのでCloudFormationのテンプレート化した。
Lambda@Edge用のLambda関数は`us-east-1`で作る必要があるためテンプレートは２つに別れてます


# Lambda関数を追加するテンプレート
- このテンプレートは`us-east-1`で実行する必要があります
- Lambda@edgeはエイリアス使えないが、SAMでLambdaのバージョン発行したかったので`AutoPublishAlias`を指定してバージョン作ってます
- コード内の`authUser`と`authPass`は任意の値を入れて実行する

```yaml
AWSTemplateFormatVersion: 2010-09-09
Transform: "AWS::Serverless-2016-10-31"

Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: basic-auth-lambda-role
      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

  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: basic-auth
      Handler: index.handler
      Runtime: nodejs8.10
      MemorySize: 128
      Timeout: 5
      Role: !GetAtt LambdaRole.Arn
      AutoPublishAlias: prod
      InlineCode: |
        exports.handler = (event, context, callback) => {
            const request = event.Records[0].cf.request;
            const headers = request.headers;
            const authUser = 'user';
            const authPass = 'pass';
            const authString = 'Basic ' + new Buffer(`${authUser}:${authPass}`).toString('base64');
            if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
                const response = {
                    status: '401',
                    statusDescription: 'Unauthorized',
                    body: 'Unauthorized',
                    headers: {
                        'www-authenticate': [{ key: 'WWW-Authenticate', value:'Basic' }],
                    },
                };
                callback(null, response);
            } else {
                callback(null, request);
            }
        };

  LogGroup:
    Type: AWS::Logs::LogGroup
    DependsOn:
      - LambdaFunction
    Properties:
      RetentionInDays: 1
      LogGroupName: !Sub "/aws/lambda/${LambdaFunction}"
```


# CloudFrontの設定をするテンプレート
- S3 + CloudFrontの構成になります
- このテンプレートはどのリージョンで実行してもいいですが今回のは別リージョンで実行する想定で作ってます
- `LambdaFunctionAssociations`がLambda@Edge使うための設定です
    - これを消せばBasic認証が外れます
    - ここでLambda関数名とバージョンが必要になるのでParameterから受け取るようにしました
- アクセス時にLambdaのログが`/aws/lambda/us-east-1.関数名`に出力されます
    - あとで削除したときにゴミ残るのでココでロググループを作ってしまいます 

```yaml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  BucketName:
    Type: String
  LambdaEdgeFunctionName:
    Type: String
  LambdaEdgeFunctionVersion:
    Type: String

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: PublicRead
      BucketName: !Ref BucketName
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: error.html

  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    DependsOn:
      - S3Bucket
      - CloudFrontOriginAccessIdentity
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Version: 2008-10-17
        Statement:
          - Action:
              - s3:GetObject
            Effect: Allow
            Resource: !Sub "${S3Bucket.Arn}/*"
            Principal:
              AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}"

  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: "access identity"

  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    DependsOn:
      - S3Bucket
      - CloudFrontOriginAccessIdentity
    Properties:
      DistributionConfig:
        Enabled: true
        DefaultCacheBehavior:
          AllowedMethods:
            - HEAD
            - GET
          CachedMethods:
            - HEAD
            - GET
          DefaultTTL: 0
          MaxTTL: 0
          MinTTL: 0
          TargetOriginId: !Sub "${BucketName}-Origin"
          ViewerProtocolPolicy: redirect-to-https
          ForwardedValues:
            QueryString: false
          LambdaFunctionAssociations:
            - EventType: viewer-request
              LambdaFunctionARN: !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:${LambdaEdgeFunctionName}:${LambdaEdgeFunctionVersion}"
        IPV6Enabled: true
        HttpVersion: http2
        DefaultRootObject: index.html
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
        Origins:
          - Id: !Sub "${BucketName}-Origin"
            DomainName: !Sub "${BucketName}.s3.${AWS::Region}.amazonaws.com"
            S3OriginConfig:
              OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
        CustomErrorResponses:
          - ErrorCachingMinTTL: 0
            ErrorCode: 403

  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 1
      LogGroupName: !Sub "/aws/lambda/us-east-1.${LambdaEdgeFunctionName}"
```

# 確認
- 作成されたCloudFront DistributionのDomain Nameを叩いて以下のようになればOK
- あとはLambdaに設定したUser/Passを入れてログインできればOK

![スクリーンショット 2019-08-12 18.02.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206915/12d321ac-8bac-04a1-83b7-7d9c3a709c3e.png "スクリーンショット 2019-08-12 18.02.29.png")