CloudFront Lambda@Edge with Python 3.8 で Basic 認証を実現する

# はじめに

Lambda@Edge を使って Basic 認証をサイトにかけようと思ったのだが、日本語で検索すると普通に Node 6 とか 8 とかでの記事が多く引っかかる。 ただ、この制限は Lambda@Edge が一般リリースされた直後の話であり、流石に情報が古すぎる気がしたので、改めて現状の Lambda@Edge の対応状況を調べてみた。
すると 現時点 (2020/10/08) では Lambda@Edge に Python 3.8 まで使えるようになってたので、これを使って今回の目的である Basic 認証をかけてみることにした。

## 補足: 実行可能なLambda環境

記事執筆時点(2020/10/08)時点で実行可能な環境は以下の通り。

- Python 3.8
- Python 3.7
- Node.js 12
- Node.js 10
- Node.js 8 and Node.js 6

引用元: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html#lambda-requirements-lambda-function-configuration

Node.js だけでなく Python でも実行可能なようになっていた。 いつぐらいにこの変更が入ったのか見ていくと、Python 3.7 が使えるようになったのが 2019年8月、Python 3.8 と Node 12 は2020年3月あたりのようだ。

- https://aws.amazon.com/jp/about-aws/whats-new/2019/08/lambdaedge-adds-support-for-python-37/
- https://aws.amazon.com/jp/about-aws/whats-new/2020/03/lambda-at-edge-node12-python38/

# Lambda@Edge とは

Amazon CloudFront のリクエストの間に AWS Lambda を実行することでアプリケーションとは分離した形で必要とする処理を挟めるもの。　公式が紹介しているユースケースは以下を参照。

https://aws.amazon.com/jp/lambda/edge/

今回は、利用者(ビュワー) が CloudFront にアクセスし、そのキャッシュとオリジンにアクセスする前に Basic 認証の処理を挟むことで、本来のコンテンツとは独立して Basic 認証を実施することを目的とする。


# Lambda@Edge 用の Lambda 関数を作成する

## バージニア北部 (us-east-1) リージョン に Lambda 関数を作成

CloudFront はリージョンを選ばないグローバルなサービスなのだが、ここで利用するリソース (SSL Certificate Manager で利用する証明書など) は us-east-1 リージョンに作っておく必要がある。 Lambda 関数も同様にこのリージョンに作成する。

以下、マネジメントコンソールで作成する場合の手順を示す。 なお前提条件として、「適切なLambdaやIAM Roleの作成権限がある」こと、「既にBasic認証を利用するCloudFrontの設定は終了している」ことを前提とする。

- Lambda のページを開き、"関数の作成" ボタンを押して関数の作成を始める
- 以下の図にならって初期設定を行う
    - 実行ロールは新規に作成する場合
        - "AWS ポリシーテンプレートから新しいロールを作成
" として、"基本的な Lambda@Edge のアクセス権限 (CloudFront トリガーの場合)" を付与すること
        - 既に同様の権限を持つロールがあればそれを使っても良い
    - 名前などは自由にしてよい

![SnapCrab_NoName_2020-10-8_22-24-48_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/bfd3e62a-a7e4-23e9-a457-3701c6f2bbc7.png)
![SnapCrab_NoName_2020-10-8_22-25-11_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/97dbda27-e528-c9ce-8416-577ee0047331.png)

- 関数の作成を押して関数を作成する
- 関数コードを以下のように書き換える
    - ここで `authenticate` 関数でユーザー名・パスワードの判定をしているので自分で使うように書き換えること
    - Lambda@Edge で利用する Lambda Function では **環境変数は利用できない** のでべた書きとしている

```python
#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
Basic認証を行う Lambda@Edge の Python バージョンです。
"""

import base64


def authenticate(user, password):
    """ 認証 """
    return user == 'cloudfront' and password == 'CL0UDFR0NT'


def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    headers = request['headers']

    error_response = {
        'status': '401',
        'statusDescription': 'Unauthorized',
        'body': 'Authentication Failed',
        'headers': {
            'www-authenticate': [
                {
                    'key': 'WWW-Authenticate',
                    'value': 'Basic realm="Basic Authentication"'
                }
            ]
        }
    }

    if 'authorization' not in headers:
        return error_response

    try:
        auth_values = headers['authorization'][0]['value'].split(" ")
        auth = base64.b64decode(auth_values[1]).decode().split(":")
        (user, password) = (auth[0], auth[1])
        return request if authenticate(user, password) else error_response
    except Exception:
        # フォーマット不正など
        return error_response
```

- Deploy ボタンを押してこのコードを `$LATEST` に反映させる
- アクションから "新しいバージョン" を作成する
    - Lambda@Edge は `$LATEST` を利用できないため、必ず固定バージョンを発行すること
- バージョンが "現在作られた最新のバージョン" になっていることを確認し、下図左下にある "＋トリガーを追加" ボタンを押す

![SnapCrab_NoName_2020-10-8_22-39-6_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/3f267943-0b9f-8a00-bbeb-9b72330dd576.png)

- トリガーの設定で CloudFront を選択すると、既に設定済の CloudFront に対して Lambda@Edge を設定する項目が表示される
    - どのディストリビューションにするかは自分で選ぶ
    - "キャッシュ動作の選択" には CloudFront の Behavior で指定したパス単位で出てくるので、Lambda@Edge を適用する Behavior を選択する
        - なお、CloudFront 側から設定する場合は Behavior で同様に設定する
    - CloudFront イベントには `"ビュワー(利用者)" -> CloudFront` の間に処理を挟み込みたいので、"ビュワーリクエスト" を設定する
    - 全リージョンへの関数レプリケーションに同意する
    - "追加" を押す

![SnapCrab_NoName_2020-10-8_22-42-28_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/e63da66a-ae5e-ff95-a151-c362f71711ff.png)


以上の処理が終わり、CloudFront への適用が済めばアクセスで Basic認証がかかるようになる。


## 補足: トラブルシュート

実際に自分がやった事柄。

- Lambda@Edge が予想通りの結果を返してくれない
    - バージョン固定していなかったため。 ちゃんとバージョンは固定する必要がある
- Lambda@Edge の Lambda 関数から "CloudWatchのログを表示" としてもログが表示されない
    - 実際に処理されたリージョンの CloudWatch ログに記録される
    - 今回は ap-northeast-1 リージョンに `/aws/lambda/us-east-1.cloudfront-basic-auth` というロググループが作られてここにログが出力されていた
- 何度入力しても Basic 認証をパスしない
    - CloudFront イベントのデフォルト設定は "オリジンリクエスト" なので、これを "ビュワーリクエスト" に正しく変える必要がある
    - デフォルトの Origin Request Policy だと `Authorization` ヘッダはオリジンに流れない設定になっている。 ここに Lambda@Edge を挟んでしまうとそもそも認証トークンが Lambda@Edge 渡ってこないので常に `error_response` が返る


# まとめ

初期の記事だと IAM Role の設定とかが色々あって初期設定が難しい、という印象があったが、現時点ではウィザードを使うことで難しく考えなくても簡単に Lambda@Edge を実現できることが分かった。 
開発サイトを一般アクセスからの保護したり、完全社内向けサービスとしてサイトに Basic 認証をかけたり、サイトの管理画面などのルートパスにBasic認証を後付けでつけたりするような場合には利用できるのではないだろうか。 

# 参考

- https://gist.github.com/lmakarov/e5984ec16a76548ff2b278c06027f1a4
- [CloudFront + Lambda@Edge + SecretsManagerでBasic認証](https://xp-cloud.jp/blog/2020/08/21/7453/)