serverless / serverless

⚡ Serverless Framework – Use AWS Lambda and other managed cloud services to build apps that auto-scale, cost nothing when idle, and boast radically low maintenance.
https://serverless.com
MIT License
46.35k stars 5.69k forks source link

How to invoke a lambda edge function before any apiGatewayRestApi function request? #7141

Open josoroma-zz opened 4 years ago

josoroma-zz commented 4 years ago

I was wondering... How to invoke a lambda edge function before any apiGatewayRestApi function request?

I was using:

  before:
     handler: course/before.main
     events:
       - cloudFront:
           eventType: origin-request
           origin:
             DomainName: {0123456789}.execute-api.us-east-1.amazonaws.com
             OriginPath: /development

I am hitting the following error:

  Serverless Error ---------------------------------------

  An error occurred: CloudFrontDistribution - The function cannot have environment variables. Function: arn:aws:lambda:us-east-1:{0123456789}:function:scb-courses-api-development-api:4 (Service: AmazonCloudFront; Status Code: 400; Error Code: InvalidLambdaFunctionAssociation; Request ID: 0123456789).

"Current API gateway endpoint" or "execute-api URL" is:

sls --aws-profile=api-dev info --verbose -s development -r us-east-1
ServiceEndpoint: https://{0123456789}.execute-api.us-east-1.amazonaws.com/development

note: {0123456789}.execute-api.us-east-1.amazonaws.com/development

 sls -v

Serverless: DOTENV: Loading environment variables from .env.development:
Framework Core: 1.55.1
Plugin: 3.2.0
SDK: 2.1.2
Components Core: 1.1.2
Components CLI: 1.4.0

Thanks!

FanFataL commented 4 years ago

Here, you are creating cloudfront distribution with lambda-edge event. One of the limitation for lambda-edge functions is unable to use env variables and more like memory limit, executon time limit etc. To handle env variables you cant use serverless-dotenv-plugin, instead of this you can use serverless-plugin-embedded-env-in-code.

I don't know what you want to achive but my advice is to use authorizer insted of cf-event. https://serverless.com/framework/docs/providers/aws/events/apigateway/#http-endpoints-with-custom-authorizers

josoroma-zz commented 4 years ago

@FanFataL thanks for your insights. This is what I am trying to achieve:

Call a Lambda@Edge function before each API request. Invoke the "request" interceptor every time a request is sent to the origin.

I am creating the resource but I just can't make the edge function run before calling any other api-function.

I am getting a Status Code: 403 Forbidden response using the new Api Distribution URL as the new endpoint URL within my current conf/amplify.js file.

Part of my current serverless.yml file

...
plugins:
...
  - '@silvermine/serverless-plugin-cloudfront-lambda-edge' # Associate a Lambda with a CloudFront distribution
...

functions:

  # Call a Lambda@Edge function before each API request.
  # Invoke the "request" interceptor every time a request is sent to the origin.
  request:
    name: 'scb-courses-api-${self:custom.stage}-request-api-gateway-distribution'
    handler: course/interceptors/request.main
    memorySize: 128
    timeout: 1
    lambdaAtEdge:
      distribution: 'ApiGatewayDistribution'
      eventType: 'origin-request'

  # Defines an HTTP API endpoint that calls the main function in create.js
  # - path: url path is /courses
  # - method: POST request
  # - cors: enabled CORS (Cross-Origin Resource Sharing) for browser cross
  #     domain api call
  # - authorizer: authenticate using the AWS IAM role
  create:
    handler: course/create.main
    events:
      - http:
          path: courses
          method: post
          cors: true
          authorizer: aws_iam

...

resources:
  ...
  # cloudfront invoke lambda@edge before API Gateway request
  - ${file(resources/api-gateway-distribution.yml)}
  ...

The resource/distribution is currently created through resources/api-gateway-distribution.yml

  Resources:
    ApiGatewayDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Comment: Api Gateway Distribution
          DefaultCacheBehavior:
            TargetOriginId: ApiGatewayOrigin
            ViewerProtocolPolicy: 'redirect-to-https'
            DefaultTTL: 30
            ForwardedValues:
              QueryString: false
          Enabled: true
          Origins:
            - Id: ApiGatewayOrigin
              DomainName:
                Fn::Join:
                  - "."
                  - - Ref: ApiGatewayRestApi
                    - execute-api.us-east-1.amazonaws.com
              OriginPath: /development
              CustomOriginConfig:
                HTTPPort: 80
                HTTPSPort: 443
                OriginProtocolPolicy: https-only

where course/interceptors/request.js is:

/**
 * https://serverless.com/framework/docs/providers/aws/events/cloudfront
 * https://serverless.com/blog/lambda-at-edge-support-added
 *
 * Add support for associating a Lambda function with a CloudFront distribution
 * to take advantage of the Lambda@Edge features of CloudFront:
 *
 *  - https://github.com/silvermine/serverless-plugin-cloudfront-lambda-edge
 *
 * Instead of calling the URL we did earlier (the API Gateway URL),
 * we are going to use a different URL which points to the CloudFront
 * Distribution:
 *
 *  - https://www.yld.io/blog/caching-in-with-cloudfront-using-serverless
 */

export const main = (event, context, callback) => {
  console.log('*====> event: ', event);

  const request = event.Records[0].cf.request;

  console.log('*====> request: ', request);

  callback(null, request);
};

my current conf/amplify.js file, the one used by the frontend side:

export const amplifyConf = {
  Auth: {
    identityPoolId: '...',
    region: 'us-east-1',
    userPoolId: '...',
    userPoolWebClientId: '...',
  },
  API: {
    endpoints: [
      {
        name: 'ApiGatewayRestApi',
        endpoint: 'https://{ApiGatewayDistributionURL}.cloudfront.net',
      },
    ],
  },
};

export const storageSourceConf = {
  Storage: {
    bucket: '...',
    region: 'us-east-1',
    identityPoolId: '...',
  },
};
FanFataL commented 4 years ago

Hi @josoroma

Try to add Authorization header to forwarded values to the behavior option in the cloudfront distribution settings. Optionaly you can add access controll headers if you want use xhr requests with the api.

ForwardedValues:
  QueryString: false
  Headers:
    - "Authorization"
    - "Access-Control-Request-Headers"
    - "Access-Control-Request-Method"
josoroma-zz commented 4 years ago

@FanFataL thanks!

No luck yet!

image

image

image

Error

Access to XMLHttpRequest at 'https://{ApiGateWayRestApi}.cloudfront.net/courses' from origin 

'http://localhost:3002' has been blocked by CORS policy: Response to preflight request doesn't pass 

access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
functions:
  authreq:
    name: 'scb-courses-api-${self:custom.stage}-authreq'
    handler: course/interceptor/authreq.main
    memorySize: 128
    timeout: 1
    lambdaAtEdge:
      distribution: 'ApiGatewayDistribution'
      eventType: 'origin-request'

...
resources:
  ...
  - ${file(resources/api-gateway-distribution.yml)}
Resources:
  ApiGatewayDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Comment: Api Gateway Distribution
        DefaultCacheBehavior:
          TargetOriginId: ApiGatewayOrigin
          ViewerProtocolPolicy: 'redirect-to-https'
          DefaultTTL: 10
          ForwardedValues:
            QueryString: false
            Headers:
              - "Authorization"
              - "Access-Control-Request-Headers"
              - "Access-Control-Request-Method"
        Enabled: true
        Origins:
          - Id: ApiGatewayOrigin
            DomainName:
              Fn::Join:
                - "."
                - - Ref: ApiGatewayRestApi
                  - execute-api.us-east-1.amazonaws.com
            OriginPath: /development
            CustomOriginConfig:
              HTTPPort: 80
              HTTPSPort: 443
              OriginProtocolPolicy: https-only

Outputs:
  ApiGatewayDistributionDomain:
    Value:
      'Fn::GetAtt': [ ApiGatewayDistribution, DomainName ]

resources/api-gateway-errors.yml

Resources:
  GatewayResponseDefault4XX:
    Type: 'AWS::ApiGateway::GatewayResponse'
    Properties:
      ResponseParameters:
         gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
         gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
         gatewayresponse.header.Access-Control-Allow-Credentials: true
      ResponseType: DEFAULT_4XX
      RestApiId:
        Ref: 'ApiGatewayRestApi'
  GatewayResponseDefault5XX:
    Type: 'AWS::ApiGateway::GatewayResponse'
    Properties:
      ResponseParameters:
         gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
         gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
         gatewayresponse.header.Access-Control-Allow-Credentials: true
      ResponseType: DEFAULT_5XX
      RestApiId:
        Ref: 'ApiGatewayRestApi'

Outputs:
  ServiceEndpointDomain:
    Description: "API Gateway Domain"
    Value:
      Fn::Join:
        - "."
        - - Ref: ApiGatewayRestApi
          - "execute-api"
          - ${self:provider.region}
          - "amazonaws.com/${self:provider.stage}"
    Export:
      Name: ServiceEndpointDomain
FanFataL commented 4 years ago

Default methods handled by the Cloudfront are HEAD and GET, you need to add OPTIONS or all of them. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-defaultcachebehavior.html#cfn-cloudfront-distribution-defaultcachebehavior-allowedmethods

DefaultCacheBehavior:
  AllowedMethods:
    - "GET"
    - "HEAD"
    - "OPTIONS"
    - "PUT"
    - "PATCH"
    - "POST"
    - "DELETE"
  CachedMethods:
    - "GET"
    - "HEAD"
    - "OPTIONS"

Additionaly you need to enable cors in the lambda function sls configuration (if cors: * is not enough for you try this:)

          cors:
            origin: '*' # <-- Specify allowed origin
            headers: # <-- Specify allowed headers
              - Content-Type
              - X-Amz-Date
              - Authorization
              - X-Api-Key
              - X-Amz-Security-Token
              - X-Amz-User-Agent
            allowCredentials: false
josoroma-zz commented 4 years ago

@FanFataL thanks, all your hints have been so very helpful to me. Just one last thing, one thing I'm still struggling with, do you know what should I do in order to make event.requestContext.identity.cognitoIdentityId work, now the lambda edge function is successfully executed but I am getting a sort of null error message associated to the lack of the event.requestContext.identity.cognitoIdentityId value in the create.js lambda@endpoint.

create.js endpoint

export const main = async(event, context) => {
  const data = JSON.parse(event.body);
  const now = new Date().toISOString();

  const params = {
    TableName: process.env.tableName,
    Item: {
      courseId: data.courseId || uuid.v1(),
      userId: event.requestContext.identity.cognitoIdentityId,
      title: data.title
    }
  };

  try {
    await dynamoDbLib.call('put', params);
    return success(params.Item);
  } catch (error) {
    return failure({ error });
  }
};

serverless.yml

functions:
  # Defines an HTTP API endpoint that calls the main function in create.js
  # - path: url path is /courses
  # - method: POST request
  # - cors: enabled CORS (Cross-Origin Resource Sharing) for browser cross
  #     domain api call
  # - authorizer: authenticate using the AWS IAM role
  create:
    handler: course/create.main
    events:
      - http:
          path: courses
          method: post
          cors:
            origin: '*'
            headers:
              - Content-Type
              - X-Amz-Date
              - Authorization
              - X-Api-Key
              - X-Amz-Security-Token
              - X-Amz-User-Agent
            allowCredentials: false

  # Call a Lambda@Edge function before each API request.
  # Invoke the "request" interceptor every time a request is sent to the origin.
  authreq:
    name: 'scb-courses-api-${self:custom.stage}-authreq'
    handler: course/interceptor/authreq.main
    memorySize: 128
    timeout: 1
    lambdaAtEdge:
      distribution: 'ApiGatewayDistribution'
      eventType: 'origin-request'

resources:
...
  # API Gateway Errors
  - ${file(resources/api-gateway-errors.yml)}
  # cloudfront invoke lambda@edge before API Gateway request
  - ${file(resources/api-gateway-distribution.yml)}

resources/api-gateway-distribution.yml

Resources:
  GatewayResponseDefault4XX:
    Type: 'AWS::ApiGateway::GatewayResponse'
    Properties:
      ResponseParameters:
         gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
         gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
         gatewayresponse.header.Access-Control-Allow-Credentials: true
      ResponseType: DEFAULT_4XX
      RestApiId:
        Ref: 'ApiGatewayRestApi'
  GatewayResponseDefault5XX:
    Type: 'AWS::ApiGateway::GatewayResponse'
    Properties:
      ResponseParameters:
         gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
         gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
         gatewayresponse.header.Access-Control-Allow-Credentials: true
      ResponseType: DEFAULT_5XX
      RestApiId:
        Ref: 'ApiGatewayRestApi'

Outputs:
  ServiceEndpointDomain:
    Description: "API Gateway Domain"
    Value:
      Fn::Join:
        - "."
        - - Ref: ApiGatewayRestApi
          - "execute-api"
          - ${self:provider.region}
          - "amazonaws.com/${self:provider.stage}"
    Export:
      Name: ServiceEndpointDomain
FanFataL commented 4 years ago

Hi,

Its because event.requestContext.identity.cognitoIdentityId is availalbe only when you are using aws_iam authorizers. If you want to have information abourt user simply modify request headers and it will be available in the gateway lambda.

const request = event.Records[0].cf.request;
const headers = request.headers || {};
headers['x-cognito-identity-id'] = [{
  key: 'X-Cognito-Identity-Id',
  value: 666
}];
josoroma-zz commented 4 years ago

@FanFataL so... according to this lambda edger code:

headers['x-cognito-identity-id'] = [{
  key: 'X-Cognito-Identity-Id',
  value: 666
}];

should the value be sent by the client request itself. I found no way to get identity id whitin the lambda edge function?

meetbha commented 2 years ago

Hi @josoroma, Can you please share how you solved the 403:Forbidden issue? I am facing this issue while invoking a lambda edge function before any apiGatewayRestApi function request