CloudFront Functionsを触ったのでついでにCDKも書いてみた

CloudFront Functionsとは

CloudFront FunctionsではCloudFront経由のリクエストに対して、Header、Cookie、URLの書き換えなどのシンプルな処理を安価に実行できるサービスです

似たようなサービスにLambda@Edgeがありますが、そちらに比べてRuntimeがJavaScript(ECMAScript 5.1)、最大実行時間1ms未満など安価で実行できる代わりに出来ることは限られます

また、Lambda@Edgeではus-east-1で作成したLambdaリソースを使用する必要がありましたが、CloudFront Functionsではリージョンは気にせずに実装できるようです

今回やること

CloudFront FunctionsのViewer Requestを使用してHeader、Cookie情報を変更してそのままレスポンスさせる処理をCDKで実装してみました

ソースコード

python/
  ├ cdk/
  │  ├ .venv
  │  ├ stacks/
  │  │    ├ __init__.py
  │  │    └ cloudfront_stack.py
  │  ├ app.py
  │  └ requirements.txt
  ├ lambda/src/cloudfront_function/
  │                 └ index.js
  └ img/sample_image.png

from aws_cdk import (
    Stack,
    RemovalPolicy,
    aws_cloudfront as cloudfront,
    aws_s3 as s3,
    aws_s3_deployment as s3_deployment,
    aws_iam as iam,
)
from constructs import Construct

class CloudFrontStack(Stack):

    def __init__(self, scope: Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        service_name = "test"

        contentsBucket = s3.Bucket(self, 'contentsBucket',
            server_access_logs_prefix='contentsBucketlogs',
            auto_delete_objects=True,
            removal_policy=RemovalPolicy.DESTROY
            )

        contentsBucket.add_cors_rule(
            allowed_origins=['*'],
            allowed_methods=[s3.HttpMethods.HEAD, s3.HttpMethods.GET,
                             s3.HttpMethods.PUT, s3.HttpMethods.POST, s3.HttpMethods.DELETE],
            max_age=3000,
            exposed_headers=[
                'x-amz-server-side-encryption',
                'x-amz-request-id',
                'x-amz-id-2',
                'ETag'
            ],
            allowed_headers=['*'],
        )

        oai = cloudfront.OriginAccessIdentity(self, 'oai',
                  comment=f"{service_name} Frontend")

        contentsBucketPolicyStatemant = iam.PolicyStatement(
            effect=iam.Effect.ALLOW)
        contentsBucketPolicyStatemant.add_canonical_user_principal(
            oai.cloud_front_origin_access_identity_s3_canonical_user_id)
        contentsBucketPolicyStatemant.add_actions('s3:GetObject')
        contentsBucketPolicyStatemant.add_resources(
            contentsBucket.bucket_arn + '/*')
        contentsBucket.add_to_resource_policy(contentsBucketPolicyStatemant)

        cf_function = cloudfront.Function(self, "CloudFrontFunction",
            code=cloudfront.FunctionCode.from_file(file_path="../lambda/src/cloudfront_function/index.js"))

        distribution = cloudfront.CloudFrontWebDistribution(self, 'webdistribution',
            origin_configs=[
                {
                    's3OriginSource': {
                        's3BucketSource': contentsBucket,
                        'originPath': '/dist',
                        'originAccessIdentity': oai
                    },
                    'behaviors': [cloudfront.Behavior(is_default_behavior=True)]
                },
                {
                    's3OriginSource': {
                        's3BucketSource': contentsBucket,
                        'originAccessIdentity': oai
                    },
                    'behaviors': [cloudfront.Behavior(
                        path_pattern='/function',
                        function_associations=[cloudfront.FunctionAssociation(
                            function=cf_function,
                            event_type=cloudfront.FunctionEventType.VIEWER_REQUEST
                        )]
                    )]
                }
            ],
            price_class=cloudfront.PriceClass.PRICE_CLASS_200,
            error_configurations=[
                {
                    "errorCode": 403,
                    "responsePagePath": "/index.html",
                    "responseCode": 200,
                    "errorCachingMinTTL": 300
                },
                {
                    "errorCode": 404,
                    "responsePagePath": "/index.html",
                    "responseCode": 200,
                    "errorCachingMinTTL": 300
                }
            ]
        )

        s3_deployment.BucketDeployment(self, "contents_deploy_memoryUp1024",
            destination_bucket=contentsBucket,
            memory_limit=1024,
            sources=[
                s3_deployment.Source.asset(
                    '../img')
            ],
            destination_key_prefix="dist",
            distribution=distribution,
            distribution_paths=["/*"]
            )

function handler(event) {
  var eventCookies = event.request.cookies
  var maxAge = 365 * 24 * 60 * 60
  var attributes = `Max-Age=${maxAge}; Path=/; Domain=.sample.site; Secure`
  var cookiesToReturns = ['sample1', 'sample2']
  var responseCookie = {}
  for (var i = 0; i < cookiesToReturns.length; ++i ) {
    var cr = cookiesToReturns[i]
    if (eventCookies[cr]) {
      responseCookie[cr] = eventCookies[cr]
      responseCookie[cr].attributes = attributes
    }
  }
  var response = {
      statusCode: 200,
      statusDescription: 'OK',
      headers: {
        'cache-control': { value: 'no-cache, must-revalidate' }
      },
      cookies: responseCookie
  };
  return response;
}

sample1, sample2という名前のCookieをつけてリクエストするとattributesを付与して返却する処理にしています

解説

S3作成

originとなるS3を作成しています

removal_policyをDESTROYにすることでスタック削除した際に消せるようにしています

contentsBucket = s3.Bucket(self, 'contentsBucket',
                 server_access_logs_prefix='contentsBucketlogs',
                 auto_delete_objects=True,
                 removal_policy=RemovalPolicy.DESTROY)

バケットポリシーの設定

指定したCloudFrontのOriginAccessIdentityからGetObjectできるようにしています

contentsBucketPolicyStatemant = iam.PolicyStatement(
        effect=iam.Effect.ALLOW)
    contentsBucketPolicyStatemant.add_canonical_user_principal(
        oai.cloud_front_origin_access_identity_s3_canonical_user_id)
    contentsBucketPolicyStatemant.add_actions('s3:GetObject')
    contentsBucketPolicyStatemant.add_resources(
        contentsBucket.bucket_arn + '/*')
    contentsBucket.add_to_resource_policy(contentsBucketPolicyStatemant)

CloudFront Functionsの設定

CloudFront Functionsを定義しています

関数の指定方法はいくつかありましたが、fileのpathを指定する方法をとりました

cf_function = cloudfront.Function(self, "CloudFrontFunction",
              code=cloudfront.FunctionCode.from_file(file_path="../lambda/src/cloudfront_function/index.js"))

behaviorの設定

behaviorに「/function」を設定して、そこにfunctionをVIEWER_REQUESTで関連付けました

{
    's3OriginSource': {
        's3BucketSource': contentsBucket,
        'originAccessIdentity': oai
    },
    'behaviors': [cloudfront.Behavior(
        path_pattern='/function',
        function_associations=[cloudfront.FunctionAssociation(
            function=cf_function,
            event_type=cloudfront.FunctionEventType.VIEWER_REQUEST
        )]
    )]
}

デプロイ&検証

デプロイ

$ cdk deploy

検証

curlコマンドのオプションIでヘッダ情報を表示します

cookiesをつける場合は、-bで'<name>=<value>’で指定します

$ curl -I \
-b 'sample1=test1; sample2=test2' \
https://<発行されたurl>.cloudfront.net/function

結果

set-cookie: sample1=test1; Max-Age=31536000; Path=/; Domain=.sample.site; Secure
set-cookie: sample2=test2; Max-Age=31536000; Path=/; Domain=.sample.site; Secure

返り値でattributeが追加されているのを確認できました

補足

ログの場所

CroudFront Functions内のログは、

us-east-1リージョンの「/aws/cloudfront/function/<function名>」に保存されていました

console.log()を仕込むと自動的に送信されるようです

 

関数内のeventの中身を出力

{
  version:'1.0',
  context:{
    distributionDomainName:'xxxxxxxx.cloudfront.net',
    distributionId:'E2YEXXXXXXX',
    eventType:'viewer-request',
    requestId:'XXXXXXXXXXXXXXXXXXXXX=='
  },
  viewer:{
    ip:'XXX.XXX.XX.XX'
  },
  request:{
    method:'GET',
    uri:'/function/',
    querystring:{},
    headers:{
      host:{value:'xxxxxxxx.cloudfront.net'},
      user-agent:{value:'curl/X.XX.X'},
      accept:{value:'*/*'}
    },
    cookies:{
      sample1:{value:'test1'},
      sample2:{value:'test2'}
    }
  }
}
無料相談実施中
AWSを使用したサーバーレスアプリケーションの構築
サーバーレス開発内製化、AWSへの移行等
様々な課題について無料でご相談お受けいたします。
最新情報をチェックしよう!