S3 + CloudFront + CloudFront Functions で Basic 認証付きの WEB コンテンツ配信

# はじめに
S3 上に格納した WEB コンテンツを CloudFront で配信する際に Basic 認証で簡易なアクセス制限をかけたい、というのはよくあるユースケースだと思います。というわけで、2週間ほど前（2021年5月3日）にリリースされたばかりの「CloudFront Functions」でやってみました。

ちなみに、この CloudFront Functions ですが、CDN のエッジ側で簡易な処理を高速かつ安価に実行できるサービスです。これまでも Lambda（[Lambda@Edge](https://aws.amazon.com/jp/lambda/edge/)）を使って同じようなことができていましたが、より Viewer（クライアント）に近いところに関数を配置でき、高速かつ安価な処理が実現できるようになった、とのことです。

- [Amazon CloudFront が軽量エッジコンピューティング機能である CloudFront Functions を発表](https://aws.amazon.com/jp/about-aws/whats-new/2021/05/cloudfront-functions/)
- [Introducing CloudFront Functions – Run Your Code at the Edge with Low Latency at Any Scale | AWS News Blog](https://aws.amazon.com/jp/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/)

# やったこと
S3 をストレージ、CloudFront を CDN として用いた WEB コンテンツ配信において、エッジに配置する CloudFront Functions で Basic 認証の処理を実行します。
![cloudfront_s3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/336603/3d66414e-92a0-008c-c5be-e8f5a7c33c84.png)

## 構成全体の設定手順
設定手順は大雑把には以下のとおりです。

- 独自ドメインで配信する場合、Certificate Manager で対象ドメインの SSL 証明書を用意
    - CloudFront で使うためには、us-east-1（「バージニア北部」リージョン） で証明書を作っておく必要あり
- 配信用の S3 バケットを用意して、WEB コンテンツを格納
    - バケットへのパブリックアクセスは「すべてブロック」
    - バケットのプロパティで「静的ウェブサイトホスティング」を有効化
- CloudFront で Distribution を作成
    - 「Origin Domain Name」に、作成した S3 バケットの ARN を設定
        - S3 バケットを us-east-1 以外のリージョンに作った場合、対象バケットの ARN には「<バケット名>.s3.<リージョン名>.amazonaws.com」と、リージョン名も含めるのがオススメ[^arn]
    - 独自ドメインで配信する場合、「Alternate Domain Names」にドメイン名を追加し、作成しておいた SSL 証明書を指定
    - 「Update Bucket Policy」にチェックを入れ、CloudFront に 対象 S3 バケットへの読み取り権限を付与
- CloudFront で Basic 認証を処理するための Function を作成し、作成した Distribution に設定
    - CloudFront の 管理コンソールに新たに設けられた「Functions」で、関数を作成
    - 作成した関数の「Associate」画面で、Basic 認証を適用したい Distribution を設定（複数設定可能）
    - 作成した関数の「Publish」画面で、Publish（and update）

## Basic 認証を処理するための CloudFront Function
ここでは、肝になる関数のコードのみ示します。

```javascript:basic-auth-test
function handler(event) {
    var request = event.request;
    var headers = request.headers;

    var authUser = 'username';
    var authPass = 'password';

    var authString = 'Basic ' + (authUser + ':' + authPass).toString('base64');
    if (typeof headers.authorization === 'undefined' || headers.authorization.value !== authString) {
        var response = {
            statusCode: 401,
            statusDescription: 'Unauthorized',
            headers: {
                'www-authenticate': {value: 'Basic'}
            }
        };
        return response;
    }

    return request;
}
```

Lambda@Edge でサポートされている Node.js ではなく、CloudFront Functions では「ECMAScript 5.1 に準拠した JavaScript」を使って関数を定義する必要があります。今回は、よく見かける Lambda@Edge（Node.js）で Basic 認証するためのコード[^edge]をベースに、CloudFront Functions で動くよう仕立ててみました。

# おわりに
CloudFront Functions は、クライアントに近いところで高速に実行できて費用も低コストに抑えられる反面、最大実行時間は 1ms と制限が厳し目です。重い処理をさせることはできない[^ex]のですが、公式サンプル[^sample]でも、HTTP リクエスト/レスポンスヘッダーの加工、URLのリダイレクトやリライト、リクエスト認証などの例が示されており、工夫次第で有効に活用できるユースケースはたくさんありそうです。

[^arn]: 「[Amazon S3 での HTTP 307 エラーのトラブルシューティング](https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-http-307-response/)」にあるとおり、S3 のエンドポイント情報の他リージョンへの浸透に時間がかかることへの対策です。
[^edge]: 例えば、「[Amazon CloudFrontとAWS Lambda@EdgeでSPAのBasic認証をやってみる | DevelopersIO](https://dev.classmethod.jp/articles/cloudfront-lambdaedge-basic-spa/)」にあるものなど。
[^sample]: 「[Example code for CloudFront Functions - Amazon CloudFront](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-example-code.html)」「[GitHub - aws-samples/amazon-cloudfront-functions](https://github.com/aws-samples/amazon-cloudfront-functions)」
[^ex]: 今回の Basic 認証のコードでも最大実行時間の 20〜30% 程度の実行時間がかかってます。