AWS CDKでLambda@Edgeをデプロイする

# AWS CDKでLambda@Edgeをデプロイしてみた

## デプロイ対象の前提
- マルチユーザーが一定の制限範囲で動画を配信するディストリビューション
- 制限範囲を超えているかどうかを東京リージョンにデプロイされたDynamoDBを参照して確認し認証する
- カスタムドメインを与えておりACMで証明書を発行済
- 動画データはS3に保存されている
- アクセスログから制限を行うためアクセスログを吐き出すバケットを別に用意している
- S3バケットは全て東京リージョンでデプロイ済み
- DynamoDBのテーブル名は自動的にCloudFormationによって生成されているため，テーブル名をLambda Functionにわたす必要あり

## 環境
```
AWS CDK: 1.19.0
AWS CLI: 1.16.143 
Python: 3.7.3
```

## Lambda@Edgeの制限
基本的には下記ページに記載されています。
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html

今回はViewerRequestを実装したいと思うので，注意するべき主な点は下記の通り

- LambdaのRuntimeは`nodejs8.10、nodejs10.x、または python3.7`のみ対応
- 関数のメモリサイズは128MB固定
- 関数のタイムアウト5秒
- 環境変数のサポートなし

## 東京リージョンで作成されたリソースの情報を出力する
AWS CDKでクロスリージョンでのリソース情報を渡すことは，現状できません。
そこで，一旦CloudFormationのOutputsに出力し情報を得る必要があります。

基本的には東京リージョンへのデプロイを行うCDK App内で下記のようにOutputsへの出力指定をします。

```py:tokyo_stack.py
core.CfnOutput(self,'DynamoDBTableName',value=dynamodbTable.table_name)
core.CfnOutput(self,'DynamoDBArn',value=dynamodbTable.table_arn)
core.CfnOutput(self,'MovieBucket',value=movieBucket.bucket_domain_name)
core.CfnOutput(self,'OAI',value=oai.ref)
core.CfnOutput(self,'DistributionLogBucket',value=distributionLogBucket.bucket_domain_name)
```

これでAWS CLIを使って下記のような形で情報を取得することができるようになります。

```bash

export TableName=`aws cloudformation describe-stacks --stack-name tokyo_stack --region ap-northeast-1 | jq -r '.Stacks[0].Outputs[] | select (.OutputKey == "DynamoDBTableName").OutputValue'`
```

## Lambda Function内でDynamoDBテーブル名を取得する
Lambda@Edgeは環境変数が使えません。
本来Lambda@Edgeの特にViewer側ではタイムアウトも短いのでAWSリソースにアクセスすること自体オススメできませんが，
アクセスしたいケースあると思います。
できるだけAWSリソースを多用せず必要最小限にテーブル名を渡すには，Lambda Function内にJSONをおいて読み込むのが良いかなぁと思いました。

```bash:create-env.sh
#! /bin/bash

TableName=`aws cloudformation describe-stacks --stack-name tokyo_stack --region ap-northeast-1 | jq -r '.Stacks[0].Outputs[] | select (.OutputKey == "DynamoDBTableName").OutputValue'`

cat << EOS > functions/checkConditions/env.json
{
  "TABLE_NAME": "$TableName"
}
EOS
```

バージニアリージョン用CDK Appをデプロイする前に，このシェルを実行しenv.jsonを出力することで柔軟にテーブル名を渡すことが出来るかと思います。

## バージニアリージョン用CDK App
とりあえず作ったCDK Appをそのまま

```py:virginia_app.py
#!/usr/bin/env python3

from aws_cdk import core

from cloud_front.cloud_front_stack import CloudFrontStack

app = core.App()
cloudfront_stack = CloudFrontStack(app, "cloudfront",env={'region': 'us-east-1'})

app.synth()
```

```py:cloud_front_stack.py
from aws_cdk import (
    aws_iam as iam,
    aws_lambda as awslambda,
    aws_logs as logs,
    aws_s3 as s3,
    aws_cloudfront as cloudfront,
    core
)
import json
from datetime import datetime 

class CloudFrontStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)
        
        now = datetime.now()
        LambdaEdgePrincipals = iam.CompositePrincipal(iam.ServicePrincipal(service="edgelambda.amazonaws.com"),iam.ServicePrincipal(service="lambda.amazonaws.com"))
        LambdaEdgeRole = iam.Role(self,"LambdaEdgeRole",assumed_by=LambdaEdgePrincipals)
        LambdaEdgeRole.add_to_policy(statement=iam.PolicyStatement(actions=['dynamodb:*'],resources=[self.node.try_get_context('dynamodb_arn'),self.node.try_get_context('dynamodb_arn')+'/*']))
        checkConditionsFunction = awslambda.Function(self,'checkConditionsFunction',
            code=awslambda.Code.asset('functions/checkConditions'),
            handler='app.lambda_handler',
            runtime=awslambda.Runtime.PYTHON_3_7,
            memory_size=128,
            timeout=core.Duration.seconds(5),
            role=LambdaEdgeRole,
            description="Generated on"+now.isoformat(),
        )
        checkConditionsLoggroup = logs.LogGroup(self,'checkConditionsFunctionLogGroup',
            log_group_name='/aws/lambda/'+checkConditionsFunction.function_name,
            retention=logs.RetentionDays.TWO_MONTHS
        )
        now = datetime.now()
        checkConditionsFunctionVersion = checkConditionsFunction.add_version(name=now.isoformat())

        cloudfront.CfnDistribution(self,'moviedistribution',
            distribution_config={
                'aliases': [self.node.try_get_context('web_domain')],
                'enabled': True,
                'defaultCacheBehavior': {
                    'forwardedValues': {
                        'queryString': False
                    },
                    'lambdaFunctionAssociations': [{
                        'eventType': 'viewer-request',
                        'lambdaFunctionArn': checkConditionsFunctionVersion.function_arn
                    }],
                    'targetOriginId': 'origin1',
                    'viewerProtocolPolicy': 'redirect-to-https'
                },
                'ipv6Enabled': True,
                'logging': {
                    'bucket': self.node.try_get_context('distribution_log_bucket'),
                    'prefix': "logs/"
                },
                'origins': [{
                    'domainName': self.node.try_get_context('movie_bucket'),
                    'id': "origin1",
                    's3OriginConfig': {
                        'originAccessIdentity': 'origin-access-identity/cloudfront/'+self.node.try_get_context('oai')
                    }
                }],
                'priceClass': 'PriceClass_All',
                'viewerCertificate': {
                    'acmCertificateArn': self.node.try_get_context('acm_arn'),
                    'sslSupportMethod': 'sni-only'
                }
            }
        )

```

### Lambda@Edge用Principals
Lambda@EdgeのためのIAM Roleでは，Lambdaとして動かすための`lambda.amazonaws.com`への信頼関係と，Lambda@Edgeとして動かすための`edgelambda.amazonaws.com`への信頼関係が必要なので，CompositePrincipalを使ってAssumePolicyを設定します。

### FunctionVersion
Lambda@EdgeとしてLambda Functionを指定するためには，バージョンが発行されている必要があります。
また，CDKでFunction Versionを発行するためには，毎回一意の名前を付ける必要があるようです。
また，現在のところまだIssueに上がっている（[#5334](https://github.com/aws/aws-cdk/issues/5334))バグのようですが，Lambda Functionの何かしらがデプロイのたびに変わっていないと，同じバージョンもうあるよと言われてしまいデプロイが失敗します。
なので，このIssueにもあるようにとりあえずDescriptionを毎回変わるようにしてデプロイしています。

### CfnDistribution
最初はCloudFrontWebDistributionを使って，iBucketなリソースをbucket_arn等から作成し定義したのですが，ARNから作っても，オリジンのドメインがus-east-1で設定されてしまい，デプロイ自体は成功するもののアクセスすると失敗する形になってしまったため，遺憾ですが，CfnDistributionを使って定義しました。
このあたりはissue等を投げて待たないとかもですね。

### 東京リージョンで作成したリソース情報の取得
東京リージョンで作成したリソース情報は，Contextという機能を使用してAppに与えます。
なので，それらの情報は`self.node.try_get_context('context_name')`で取得して扱います。

## デプロイ
いよいよデプロイです

東京リージョンで出力した情報や予め作成したACM等の情報を環境変数に設定し，Contextに与えます

```bash

cdk deploy -a "python3 virginia_app.py" -c movie_bucket=$MOVIE_BUCKET -c distribution_log_bucket=$DISTRIBUTION_LOG_BUCKET -c oai=$OAI -c dynamodb_arn=$DYNAMODB_ARN -c acm_arn=$ACM_ARN -c web_domain=$WEB_DOMAIN
```

以上でクロスリージョンを含めたデプロイが無事できました