4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

AWS LambdaとServerless #2Advent Calendar 2019

Day 20

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

Last updated at Posted at 2019-12-25

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への出力指定をします。

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を使って下記のような形で情報を取得することができるようになります。


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をおいて読み込むのが良いかなぁと思いました。

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をそのまま

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()
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)バグのようですが,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に与えます


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

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

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?