0
votes

I have been trying for a day to configure automating a lambda@Edge to be associated with a Distribution through the serverless framework but things aren't working well.

Here is the documentation and they said we can use a predefined cloud front distribution from resources but not shown how?

Here is my Resources.yml that include the S3 bucket and associated two distribution's origins to it:

Resources:

ResourcesBucket:
    Type: AWS::S3::Bucket
    Properties:
        BucketName: ${self:custom.resourcesBucketName}
        AccessControl: Private
        CorsConfiguration:
            CorsRules:
            -   AllowedHeaders: ['*']
                AllowedMethods: ['PUT']
                AllowedOrigins: ['*']

ResourcesBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
        Bucket:
            Ref: ResourcesBucket
        PolicyDocument:
            Statement:
            # Read permission for CloudFront
            -   Action: s3:GetObject
                Effect: "Allow"
                Resource: 
                    Fn::Join: 
                        - ""
                        - 
                            - "arn:aws:s3:::"
                            - 
                                Ref: "ResourcesBucket"
                            - "/*"
                Principal:
                    CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId

CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
        CloudFrontOriginAccessIdentityConfig:
            Comment:
                Fn::Join: 
                    - ""
                    -
                            - "Identity for accessing CloudFront from S3 within stack "
                            - 
                                Ref: "AWS::StackName"
                            - ""
                # I can use this instead of Fn::Join !Sub 'Identity for accessing CloudFront from S3 within stack #{AWS::StackName}' Getting benefit of
                # serverless-pseudo-parameters plugin

# Cloudfront distro backed by ResourcesBucket
ResourcesCdnDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
        DistributionConfig:
            Origins:
                # S3 origin for private resources
                -   DomainName: !Sub '${self:custom.resourcesBucketName}.s3-${self:provider.region}.amazonaws.com'
                    Id: S3OriginPrivate
                    S3OriginConfig:
                        OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
                # S3 origin for public resources           
                -   DomainName: !Sub '${self:custom.resourcesBucketName}.s3-${self:provider.region}.amazonaws.com'
                    Id: S3OriginPublic
                    S3OriginConfig:
                        OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
            Enabled: true
            Comment: CDN for public and provate static content.
            DefaultRootObject: index.html
            HttpVersion: http2
            DefaultCacheBehavior:
                AllowedMethods:
                    - DELETE
                    - GET
                    - HEAD
                    - OPTIONS
                    - PATCH
                    - POST
                    - PUT
                Compress: true
                TargetOriginId: S3OriginPublic
                ForwardedValues:
                    QueryString: false
                    Headers:
                    - Origin
                    Cookies:
                        Forward: none
                ViewerProtocolPolicy: redirect-to-https
            CacheBehaviors:
                - 
                    PathPattern: 'private/*'
                    TargetOriginId: S3OriginPrivate
                    AllowedMethods:
                    - DELETE
                    - GET
                    - HEAD
                    - OPTIONS
                    - PATCH
                    - POST
                    - PUT
                    Compress: true
                    ForwardedValues:
                        QueryString: false
                        Headers:
                            - Origin
                        Cookies:
                            Forward: none
                    ViewerProtocolPolicy: redirect-to-https
                - 
                    PathPattern: 'public/*'
                    TargetOriginId: S3OriginPublic
                    AllowedMethods:
                    - DELETE
                    - GET
                    - HEAD
                    - OPTIONS
                    - PATCH
                    - POST
                    - PUT
                    Compress: true
                    ForwardedValues:
                        QueryString: false
                        Headers:
                            - Origin
                        Cookies:
                            Forward: none
                    ViewerProtocolPolicy: redirect-to-https

            PriceClass: PriceClass_200

Now I have all set regarding the CloudFront and I just want to add a lambda at the edge to authenticate my private content (The Origin with Id: S3OriginPrivate). So here is my serverless.yml file:

        service: mda-app-uploads
    
    plugins:
      - serverless-offline
      - serverless-pseudo-parameters
      - serverless-iam-roles-per-function
    
    custom:
      stage: ${opt:stage, self:provider.stage}
      resourcesBucketName: ${self:custom.stage}-mda-resources-bucket
    
    
        provider:
          name: aws
          runtime: nodejs12.x
          stage: ${opt:stage, 'dev'}
          region: us-east-1
          versionFunctions: true
        
        
        
        resources:
          - ${file(resources/s3-cloudfront.yml)}
          
        # functions:
        functions: 
          mdaAuthEdge:
            handler: mda-edge-auth.handler
            events:
              - cloudFront:
                  eventType: viewer-request
                  origin:
                    Id: S3OriginPrivate

When deploying I am getting this issue:

TypeError: Cannot read property 'replace' of undefined

This telling that this id already exists and can't be replaced as I think. My main focus is to get the lambda at edge deployed and associated with the cloud front within the serverless framework, so I made another trial to add almost everything to the cloud formation resources and depend only on the serverless framework in deploying the function and here was my serverless.yml and the resources file:

service: mda-app-uploads

plugins:
  - serverless-offline
  - serverless-pseudo-parameters
  - serverless-iam-roles-per-function

custom:
  stage: ${opt:stage, self:provider.stage}
  resourcesBucketName: ${self:custom.stage}-mda-resources-bucket


provider:
  name: aws
  runtime: nodejs12.x
  stage: ${opt:stage, 'dev'}
  region: us-east-1
  versionFunctions: true



resources:
  # Buckets
  - ${file(resources/s3-cloudfront.yml)}
  
# functions:
functions: 
  mdaAuthEdge:
    handler: mda-edge-auth.handler
    role: LambdaEdgeFunctionRole

The resources:

Resources:

    LambdaEdgeFunctionRole:
        Type: "AWS::IAM::Role"
        Properties:
            Path: "/"
            ManagedPolicyArns:
                - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
            AssumeRolePolicyDocument:
                Version: "2012-10-17"
                Statement:
                -
                    Sid: "AllowLambdaServiceToAssumeRole"
                    Effect: "Allow"
                    Action: 
                        - "sts:AssumeRole"
                    Principal:
                        Service: 
                            - "lambda.amazonaws.com"
                            - "edgelambda.amazonaws.com"
    LambdaEdgeFunctionPolicy:
        Type: "AWS::IAM::Policy"
        Properties:
            PolicyName: MainEdgePolicy
            PolicyDocument:
                Version: "2012-10-17"
                Statement:
                    Effect: "Allow"
                    Action: 
                        - "lambda:GetFunction"
                        - "lambda:GetFunctionConfiguration"
                    Resource: !Ref MdaAuthAtEdgeLambdaFunction.Version #!Join [':', [!GetAtt MdaAuthAtEdgeLambdaFunction.Arn, '2']]
            Roles:
                - !Ref LambdaEdgeFunctionRole




    ResourcesBucket:
        Type: AWS::S3::Bucket
        Properties:
            BucketName: ${self:custom.resourcesBucketName}
            AccessControl: Private
            CorsConfiguration:
                CorsRules:
                -   AllowedHeaders: ['*']
                    AllowedMethods: ['PUT']
                    AllowedOrigins: ['*']

    ResourcesBucketPolicy:
        Type: AWS::S3::BucketPolicy
        Properties:
            Bucket:
                Ref: ResourcesBucket
            PolicyDocument:
                Statement:
                # Read permission for CloudFront
                -   Action: s3:GetObject
                    Effect: "Allow"
                    Resource: 
                        Fn::Join: 
                            - ""
                            - 
                                - "arn:aws:s3:::"
                                - 
                                    Ref: "ResourcesBucket"
                                - "/*"
                    Principal:
                        CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
    
    CloudFrontOriginAccessIdentity:
        Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
        Properties:
            CloudFrontOriginAccessIdentityConfig:
                Comment:
                    Fn::Join: 
                        - ""
                        -
                                - "Identity for accessing CloudFront from S3 within stack "
                                - 
                                    Ref: "AWS::StackName"
                                - ""
                    # I can use this instead of Fn::Join !Sub 'Identity for accessing CloudFront from S3 within stack #{AWS::StackName}' Getting benefit of
                    # serverless-pseudo-parameters plugin

    # Cloudfront distro backed by ResourcesBucket
    ResourcesCdnDistribution:
        Type: AWS::CloudFront::Distribution
        Properties:
            DistributionConfig:
                Origins:
                    # S3 origin for private resources
                    -   DomainName: !Sub '${self:custom.resourcesBucketName}.s3-${self:provider.region}.amazonaws.com'
                        Id: S3OriginPrivate
                        S3OriginConfig:
                            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
                    # S3 origin for public resources           
                    -   DomainName: !Sub '${self:custom.resourcesBucketName}.s3-${self:provider.region}.amazonaws.com'
                        Id: S3OriginPublic
                        S3OriginConfig:
                            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
                Enabled: true
                Comment: CDN for public and provate static content.
                DefaultRootObject: index.html
                HttpVersion: http2
                DefaultCacheBehavior:
                    AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                    Compress: true
                    TargetOriginId: S3OriginPublic
                    ForwardedValues:
                        QueryString: false
                        Headers:
                        - Origin
                        Cookies:
                            Forward: none
                    ViewerProtocolPolicy: redirect-to-https
                CacheBehaviors:
                    - 
                        PathPattern: 'private/*'
                        TargetOriginId: S3OriginPrivate
                        AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                        Compress: true
                        LambdaFunctionAssociations:
                            - 
                                EventType: origin-request
                                LambdaFunctionARN: !Ref MdaAuthEdgeLambdaFunction.Version
                                    #!Join [':', [!GetAtt MdaAuthAtEdgeLambdaFunction.Arn, '2']]
            #    arn:aws:lambda:eu-west-1:219511374676:function:mda-aws-functions-dev-authLambdaAtEdge:1
                        ForwardedValues:
                            QueryString: false
                            Headers:
                                - Origin
                            Cookies:
                                Forward: none
                        ViewerProtocolPolicy: redirect-to-https
                    - 
                        PathPattern: 'public/*'
                        TargetOriginId: S3OriginPublic
                        AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                        Compress: true
                        ForwardedValues:
                            QueryString: false
                            Headers:
                                - Origin
                            Cookies:
                                Forward: none
                        ViewerProtocolPolicy: redirect-to-https

                PriceClass: PriceClass_200

But I've faced many errors related to defining the version and so on. I searched, debugged, and investigated that for many hours but seems hard configuration. Any help on how to get lambda edge works with predefined cloud front through the serverless framework?

1

1 Answers

0
votes

It's a bit tricky to do so using a serverless framework but I solved it by combining cloud formation with the serverless framework. I have the answer here to another question which contains a full description of how to do so:

How to access AWS CloudFront that connected with S3 Bucket via Bearer token of a specific user (JWT Custom Auth)

I don't want to repeat everything again here and also I found the question so important and facing many people without a concrete solution so pleas let me know in case you are facing any issue.

The approach is to just create the function inside the serverless.yml then inside the cloud formation you can do all the magic of creating the versions, roles and another function that will help you publish your arn and use it dynamically.

Here is my Serverless.yml:

    service: mda-app-uploads
    
    plugins:
      - serverless-offline
      - serverless-pseudo-parameters
      - serverless-iam-roles-per-function
      - serverless-bundle
    
    
    custom:
      stage: ${opt:stage, self:provider.stage}
      resourcesBucketName: ${self:custom.stage}-mda-resources-bucket
      resourcesStages:
        prod: prod
        dev: dev
      resourcesStage: ${self:custom.resourcesStages.${self:custom.stage}, self:custom.resourcesStages.dev}
    
    
    provider:
      name: aws
      runtime: nodejs12.x
      stage: ${opt:stage, 'dev'}
      region: us-east-1
      versionFunctions: true
    
    functions: 
      oauthEdge:
        handler: src/mda-edge-auth.handler
        role: LambdaEdgeFunctionRole
        memorySize: 128
        timeout: 5
    
    
    resources:
      - ${file(resources/s3-cloudfront.yml)}

Here is my resources/s3-cloudfront.yml:

Resources:

    AuthEdgeLambdaVersion:
        Type: Custom::LatestLambdaVersion
        Properties:
            ServiceToken: !GetAtt PublishLambdaVersion.Arn
            FunctionName: !Ref OauthEdgeLambdaFunction
            Nonce: "Test"

    PublishLambdaVersion:
        Type: AWS::Lambda::Function
        Properties:
            Handler: index.handler
            Runtime: nodejs12.x
            Role: !GetAtt PublishLambdaVersionRole.Arn
            Code:
                ZipFile: |
                    const {Lambda} = require('aws-sdk')
                    const {send, SUCCESS, FAILED} = require('cfn-response')
                    const lambda = new Lambda()
                    exports.handler = (event, context) => {
                        const {RequestType, ResourceProperties: {FunctionName}} = event
                        if (RequestType == 'Delete') return send(event, context, SUCCESS)
                        lambda.publishVersion({FunctionName}, (err, {FunctionArn}) => {
                        err
                            ? send(event, context, FAILED, err)
                            : send(event, context, SUCCESS, {FunctionArn})
                        })
                    }

    PublishLambdaVersionRole:
        Type: AWS::IAM::Role
        Properties:
            AssumeRolePolicyDocument:
                Version: '2012-10-17'
                Statement:
                - Effect: Allow
                  Principal:
                    Service: lambda.amazonaws.com
                  Action: sts:AssumeRole
            ManagedPolicyArns:
            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
            Policies:
            - PolicyName: PublishVersion
              PolicyDocument:
                Version: '2012-10-17'
                Statement:
                - Effect: Allow
                  Action: lambda:PublishVersion
                  Resource: '*'

    LambdaEdgeFunctionRole:
        Type: "AWS::IAM::Role"
        Properties:
            Path: "/"
            ManagedPolicyArns:
                - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
            AssumeRolePolicyDocument:
                Version: "2012-10-17"
                Statement:
                -
                    Sid: "AllowLambdaServiceToAssumeRole"
                    Effect: "Allow"
                    Action: 
                        - "sts:AssumeRole"
                    Principal:
                        Service: 
                            - "lambda.amazonaws.com"
                            - "edgelambda.amazonaws.com"
    LambdaEdgeFunctionPolicy:
        Type: "AWS::IAM::Policy"
        Properties:
            PolicyName: MainEdgePolicy
            PolicyDocument:
                Version: "2012-10-17"
                Statement:
                    Effect: "Allow"
                    Action: 
                        - "lambda:GetFunction"
                        - "lambda:GetFunctionConfiguration"
                    Resource: !GetAtt AuthEdgeLambdaVersion.FunctionArn
            Roles:
                - !Ref LambdaEdgeFunctionRole


    ResourcesBucket:
        Type: AWS::S3::Bucket
        Properties:
            BucketName: ${self:custom.resourcesBucketName}
            AccessControl: Private
            CorsConfiguration:
                CorsRules:
                -   AllowedHeaders: ['*']
                    AllowedMethods: ['PUT']
                    AllowedOrigins: ['*']

    ResourcesBucketPolicy:
        Type: AWS::S3::BucketPolicy
        Properties:
            Bucket:
                Ref: ResourcesBucket
            PolicyDocument:
                Statement:
                # Read permission for CloudFront
                -   Action: s3:GetObject
                    Effect: "Allow"
                    Resource: 
                        Fn::Join: 
                            - ""
                            - 
                                - "arn:aws:s3:::"
                                - 
                                    Ref: "ResourcesBucket"
                                - "/*"
                    Principal:
                        CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
                -   Action: s3:PutObject
                    Effect: "Allow"
                    Resource: 
                        Fn::Join: 
                            - ""
                            - 
                                - "arn:aws:s3:::"
                                - 
                                    Ref: "ResourcesBucket"
                                - "/*"
                    Principal:
                        AWS: !GetAtt LambdaEdgeFunctionRole.Arn

                -   Action: s3:GetObject
                    Effect: "Allow"
                    Resource: 
                        Fn::Join: 
                            - ""
                            - 
                                - "arn:aws:s3:::"
                                - 
                                    Ref: "ResourcesBucket"
                                - "/*"
                    Principal:
                        AWS: !GetAtt LambdaEdgeFunctionRole.Arn

    
    CloudFrontOriginAccessIdentity:
        Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
        Properties:
            CloudFrontOriginAccessIdentityConfig:
                Comment:
                    Fn::Join: 
                        - ""
                        -
                            - "Identity for accessing CloudFront from S3 within stack "
                            - 
                                Ref: "AWS::StackName"
                            - ""


    # Cloudfront distro backed by ResourcesBucket
    ResourcesCdnDistribution:
        Type: AWS::CloudFront::Distribution
        Properties:
            DistributionConfig:
                Origins:
                    # S3 origin for private resources
                    -   DomainName: !Sub '${self:custom.resourcesBucketName}.s3.amazonaws.com'
                        Id: S3OriginPrivate
                        S3OriginConfig:
                            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
                    # S3 origin for public resources           
                    -   DomainName: !Sub '${self:custom.resourcesBucketName}.s3.amazonaws.com'
                        Id: S3OriginPublic
                        S3OriginConfig:
                            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
                Enabled: true
                Comment: CDN for public and provate static content.
                DefaultRootObject: index.html
                HttpVersion: http2
                DefaultCacheBehavior:
                    AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                    Compress: true
                    TargetOriginId: S3OriginPublic
                    ForwardedValues:
                        QueryString: false
                        Headers:
                        - Origin
                        Cookies:
                            Forward: none
                    ViewerProtocolPolicy: redirect-to-https
                CacheBehaviors:
                    - 
                        PathPattern: 'private/*'
                        TargetOriginId: S3OriginPrivate
                        AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                        Compress: true
                        LambdaFunctionAssociations:
                            - 
                                EventType: viewer-request
                                LambdaFunctionARN: !GetAtt AuthEdgeLambdaVersion.FunctionArn
                        ForwardedValues:
                            QueryString: false
                            Headers:
                                - Origin
                            Cookies:
                                Forward: none
                        ViewerProtocolPolicy: redirect-to-https
                    - 
                        PathPattern: 'public/*'
                        TargetOriginId: S3OriginPublic
                        AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                        Compress: true
                        ForwardedValues:
                            QueryString: false
                            Headers:
                                - Origin
                            Cookies:
                                Forward: none
                        ViewerProtocolPolicy: redirect-to-https

                PriceClass: PriceClass_200

But you will find the full description in my other question's answer.