amazon-cloudfrontaws-cdk

CDK: Add origin/behavior to existing CloudFront distribution


We have a CF distribution that is used for caching responses from MediaPackage. We have multiple MediaPackage endpoints and for each endpoint there is a corresponding origin/behavior on the same CF distribution.

Using CDK how can we add a new origin/behavior on the existing CF distribution?. I tried below but ran into an error:

        // load existing distribution
        const distribution = Distribution.fromDistributionAttributes(scope, `${props.stackName}-Distribution`, {
            distributionId: 'E33333B',
            domainName: 'test.example.com'
        }) as Distribution;

        // Convert to CfnDistribution
        const distributionLive = distribution.node.defaultChild as CfnDistribution;
        const distributionConfig =
            distributionLive.distributionConfig as CfnDistribution.DistributionConfigProperty;
        
        // Fetch origin/behaviors list
        const cacheBehaviors = distributionConfig.cacheBehaviors as CfnDistribution.CacheBehaviorProperty[];
        const origins = distributionConfig.origins as CfnDistribution.OriginProperty[];

       // Add new origin/behavior
       origins.push({..})
       cacheBehaviors.push({..})

Error:

/dist/lib/resources/cloudfront.js:95
        const distConfig = distributionLive.distributionConfig;
                                                   ^

TypeError: Cannot read property 'distributionConfig' of undefined

Solution

  • You can create a lambda function which will update the CF distribution to add new behavior. Then create a custom AWS resource to call this lambda function on create/update as below -

    #lambda function code to be saved in lambda_codes/update_cf/update_distribution.py
    
    import boto3
    import os
    
    client = boto3.client('cloudfront')
    
    def lambda_handler(event,context):
        responseData={}
        try:
            if (event['RequestType'] == 'Create') or (event['RequestType'] == 'Update'):
                config_res = client.get_distribution_config(Id=event['ResourceProperties']["Id"])
                config_req = dict(config_res["DistributionConfig"])
                ETag = config_res["ETag"]
                LambdaFunctionARN = event['ResourceProperties']["LambdaFunctionARN"]
                    
                LambdaFunctionAssociations = {
                                'Quantity': 1,
                                'Items': [{
                                    'LambdaFunctionARN': LambdaFunctionARN,
                                    'EventType': 'viewer-request',
                                    'IncludeBody': False
                                }]
                            }
                CacheBehaviors= {}
                CacheBehaviors_Item=dict(config_req['DefaultCacheBehavior'])
                CacheBehaviors_Item['PathPattern'] ='/index.html'
                CacheBehaviors['Items'] = [CacheBehaviors_Item]
                CacheBehaviors['Items'][0]['LambdaFunctionAssociations'] = LambdaFunctionAssociations
                CacheBehaviors['Quantity'] = 1
    
                config_req['CacheBehaviors']=CacheBehaviors
                print(config_req)
    
                response = client.update_distribution(Id=event['ResourceProperties']["Id"],
                                IfMatch=ETag,
                                DistributionConfig=config_req)
    
                print("Response",response)
                responseData['Status'] = 'SUCCESS'
                print ('SUCCESS')
                return { "status" : responseData['Status'] }
            else:
                print("SUCCESS - operation not Create or Update, ResponseData=" + str(responseData))
                return { "status" : 'SUCCESS' }
    
    
        except Exception as e:
            responseData['Error'] = str(e)
            print("FAILED ERROR: " + responseData['Error'])
    

    Below is the python CDK stack to create above updating CF lambda and custom resource calling it.

    from constructs import Construct
    import json
    import os
    import base64
    from aws_cdk import (
        Duration,
        Stack,
        aws_iam as iam,
        aws_lambda as lambda_,
    
    )
    
    
    class InfraCodeStack(Stack):
    
        def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
            super().__init__(scope, construct_id, **kwargs)
            
            #Policy Statement for CF access
            cf_update_lambda_role_policy_statement = iam.PolicyDocument(
                        statements= [
                        iam.PolicyStatement(
                            effect = iam.Effect.ALLOW,
                            actions= [
                                "cloudfront:GetDistributionConfig",
                                "cloudfront:UpdateDistribution" 
                                ],
                            resources= ["*"],
                            )
                        ]
            )
    
            #Cf_Update lambda role
            cf_lambda_iamrole = iam.Role(
                self, "Cf_Update_Lambda_Role",
                role_name= "Cf_Update_Lambda_Role",
                assumed_by= iam.CompositePrincipal(iam.ServicePrincipal("lambda.amazonaws.com")),
                description= 'Role to allow lambda to update cloudfront config',
                inline_policies= {'CF_Update_lambda_Cloudfront_Policy':cf_update_lambda_role_policy_statement },
                managed_policies= [
                iam.ManagedPolicy.from_aws_managed_policy_name('service-role/AWSLambdaBasicExecutionRole')
                ]
            ) 
    
            #Cf_Update lambda function
            cf_update_lambda = lambda_.Function(self, "cf-update-lambda-dev",
                                            code=lambda_.Code.from_asset("lambda_codes/update_cf"),
                                            function_name="cf-update-lambda-dev",
                                            handler='update_distribution.lambda_handler',
                                            role=cf_lambda_iamrole,
                                            tracing=lambda_.Tracing.ACTIVE,
                                            runtime=lambda_.Runtime.PYTHON_3_9
                                            )
    
            cf_update_lambda.node.add_dependency(get_version)
    
            #Updating cloudfront via custom sdk call 
            input_event_to_cf_update_lambda = { 
                "RequestType" : "Update",
                "ResourceProperties" : { 
                                    "Id" : cloudfront_object.distribution_id, ## your cloudfront object created in the stack's id
                                    "LambdaFunctionARN" : function_arn,       ## lambda@edge arn to be attached to behavior
                                    "AWSRegion": "us-east-1"
                                     }
                }
                
            
            def lambda_context(custom=None,env=None,client=None):
                client_context = dict(
                    custom=custom,
                    env=env,
                    client=client)
                json_context = json.dumps(client_context).encode('utf-8')
                return base64.b64encode(json_context).decode('utf-8')
    
            context = {
                "custom": {},
                "env": {},
                "client": {}
                }
                
            cf_update_lambda_custom = cr.AwsCustomResource(self, "cf_update_lambda_custom",
                    on_update=cr.AwsSdkCall( 
                            service="Lambda",
                            action="invoke",
                            parameters={ "FunctionName": cf_update_lambda.function_name,
                                        "InvocationType" : "RequestResponse",
                                        "LogType" : "Tail",
                                        "ClientContext" : lambda_context(**context),
                                        "Payload" : json.dumps(input_event_to_cf_update_lambda),
                                        },
                            physical_resource_id=cr.PhysicalResourceId.of("lambda_update_cf_custom_resource")
                            ),
                    policy=cr.AwsCustomResourcePolicy.from_statements(
                            [ iam.PolicyStatement(
                                effect = iam.Effect.ALLOW,
                                actions= [
                                    "lambda:InvokeAsync",
                                    "lambda:InvokeFunction"                                                
                                    ],
                                resources= ['*'],
                                )]
                        
                    ),
                    )
                    
            cf_update_lambda_custom.node.add_dependency(cf_update_lambda)