CloudFrontとLambda@EdgeによるBASIC認証をセットで作るCloudFormation

こんにちは。Kaneyasuです。
最近はIoTまわりをやったり、フロントエンドやったりと幅広く頑張らせていただいています。
今回はLambda@EdgeによるBASIC認証付きのCloudFrontを作るCloudFormationテンプレートを書いてみました。
フロントエンドのお仕事でデプロイ先をサクッと用意するために作ったものです。

# CloudFormationテンプレート

https://gist.github.com/satoshi256kbyte/a2ea5e88b6ac041aad17757f0ffa6427

# ポイント

## バージニアリージョンで実行する

上側のCloudFormationテンプレートはバージニアリージョンで実行します。
Lambda@Edgeとして使えるLambdaはバージニアのみだからです。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-how-it-works-tutorial.html

>US-East-1 (バージニア北部) リージョン (us-east-1) にいることを確認します。Lambda@Edge 関数を作成するには、このリージョンに設定されている必要があります。

## BASIC認証用のID・パスワードをコードに埋め込む

```python
  Code:
    ZipFile: !Sub |
      import base64
      from typing import Dict, Any, List, Optional

      BASIC_AUTH_USER = "${BasicAuthUser}"
      BASIC_AUTH_PASSWORD = "${BasicAuthPassword}"
```

LambdaのコードはPythonで書いて、CloudFormationテンプレート内に埋め込んでいます。
BASIC認証のパスワードは、CloudFormationのパラメータとしました。
環境変数にしておこうと思いましたが、環境変数を用いたLambdaはLambda＠Edgeとして使用できないようです。
環境変数を使ったLambdaでLambda@Edgeを設定するCloudFormationテンプレートを書いてみましたが、実行時にエラーになることを確認しました。
というわけで、CloudFormationパラメータから持ってきた値を埋め込む形にしました。

### Lambdaのバージョンを作っておく

```python
  LambdaEdgeFunctionVersion:
    Type: AWS::Lambda::Version
    Properties:
      FunctionName: !Ref LambdaEdgeFunction
      Description: v1
```

Lambda@Edgeとして使う場合、バージョンを作っておくことが必要なので作成しています。

## CloudFormationテンプレートのパラメータでLambdaのバージョンのARNを渡す

```yaml
  LambdaEdgeFunctionVersionArn:
    Type: String
    Description: Lambda Edge Function Version ARN
```

下側のCloudFormationテンプレートは、LambdaのバージョンのARNをCloudFormationテンプレートのパラメータで受け取り、それを後述のビューワーリクエストにセットしています。

下側のCloudFormationテンプレートは、S3・CloudFront、Lambda@Edgeの設定をしています。
こちらのCloudFormationテンプレートは、東京リージョンで実行しそうなので、Lambda関数のバージョンのARNはOutputsではなくパラメータで渡す形にしています。
リージョンをまたぐと、Outputsを経由した渡し方は機能しないのが理由です。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html

>リージョン間でクロススタック参照を作成することはできません。組み込み関数 Fn::ImportValue を使用して、同じリージョン内にエクスポートされた値のみをインポートできます。

S3と、CloudFront＋Lambda@Edgeで分けたらこの問題は解決しそうな気もしますが、あまり分けるのも煩雑なのでこのままでいいかなと思っています。

### バケットポリシーの設定を行う

CloudFrontディストリビューションからのみアクセスを許可するよう指定しています。
これがないと、デプロイ後にアクセスしても権限エラーが出ます。

```yaml
  S3BucketPolicyForWebsite:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3BucketForWebsite
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowCloudFrontServicePrincipal
            Effect: Allow
            Principal:
              Service: 'cloudfront.amazonaws.com'
            Action:
              - s3:GetObject
            Resource: !Sub arn:aws:s3:::${SystemName}-${Environment}-s3-bucket-for-website/*
            Condition:
              StringEquals:
                'aws:SourceArn': !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}

```

### OACの設定を行う

```yaml
  OriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Description: ''
        Name: !Sub ${SystemName}-${Environment}-origin-access-control-for-website
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        DefaultRootObject: 'index.html'
        Origins:
          - DomainName: !GetAtt S3BucketForWebsite.RegionalDomainName
            Id: !Sub ${SystemName}-${Environment}-s3-bucket-for-website
            OriginAccessControlId: !GetAtt OriginAccessControl.Id
            S3OriginConfig: {}
```

CloudFrontの画面で言う、ディストリビューション＞オリジンのこの部分を設定しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1959433/45a823e8-5ce4-3b27-92d9-7fd1d5328831.png)

### DefaultRootObjectの設定

```yaml
        DefaultRootObject: 'index.html'
```

これがないと、ファイル名を指定せずにアクセスすると何も表示されるエラーとなります。
ただし、この設定マネジメントコンソール側に見当たらないんですがどこにあるんですかね？

### ビューワーリクエストにLambdaのバージョンのARNをセット

```yaml
  LambdaFunctionAssociations:
    - EventType: viewer-request
      LambdaFunctionARN: !Ref LambdaEdgeFunctionVersionArn
```

Lambda@Edgeを設定しているのはここです。
ビューワーリクエストにLambdaのバージョンのARNをセットします。

# まとめ

Lambda@Edgeの設定は手動で行うと、なかなか成功しないので今回テンプレートにしてみました。
BASIC認証ID・パスをLambdaにハードコーディングしてる部分は、他にやりようがある気がするのでまた調べてみようと思います。