aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.71k stars 3.94k forks source link

API Gateway and Lambda TokenAuthorizer in different stacks causing cyclic reference error #8241

Open rjoseph-resilient opened 4 years ago

rjoseph-resilient commented 4 years ago

:question: General Issue

The Question

Hi,

I have two separate stacks, one for my API Gateway and another for my Lambda that is used as the token authoriser. The Lambda function is then passed to API Gateway via props and is used as the token authoriser. When building the aws-cdk errors mentioning "would create a cyclic reference." regarding the API Gateway and the token authoriser referencing the Lambda.

I have provided examples and a stack trace under Other information for further details.

Please let me know how this may be resolved (Not sure if this is a bug (seems like a bug) or as designed or user error)?

P.S. My preference is to have my Lambdas in a separate stack (Allows me to deploy/undeploy Lambdas independently from the API Gateway), however if this is not possible at this moment in time, please let me know.

Kind Regards,

RJ

Environment

Other information

Below are examples of the stacks:

Stack 1 - API Gateway

  constructor(stack: cdk.Stack, { integrationLambdaFn, tokenAuthoriserLambdaFn}: Props) {

    const api = new apigateway.RestApi(this, 'RestApi', {
      description: 'My API Gateway REST API',
      restApiName: 'MyApiGatewayRestApi',
      policy: policyDocument,
      deployOptions: {
        stageName: 'stage'
      },
      defaultIntegration: new apigateway.LambdaIntegration(integrationLambdaFn),
      defaultMethodOptions: {
        authorizer: new apigateway.TokenAuthorizer(this, 'MyTokenAuthoriser', {
          handler: tokenAuthoriserLambdaFn
        })
      }
    });

Stack 2 - Lambda - TokenAuthoriser

    this.tokenAuthoriserLambdaFn= new lambda.Function(scope, 'Function', {
    functionName: 'myTokenAuthoriser',
    runtime: lambda.Runtime.NODEJS_12_X,
    code: lambda.Code.fromAsset('./lambda'),
    handler: 'index.myTokenAuthoriserHandler',
    role: myRole,
    timeout: cdk.Duration.seconds(15),
    tracing: lambda.Tracing.ACTIVE,
    vpc,
    securityGroups: [securityGroup1] 
  });
  this.tokenAuthoriserLambdaFn.node.addDependency(policy);

As you can see in Stack 1 above, two Lambdas are passed to the API Gateway construct. The first (integrationLambdaFn) is passed to apigateway.LambdaIntegration(integrationLambdaFn) and does not cause the cyclic dependency error. However the second (tokenAuthoriserLambdaFn) passed to the apigateway.TokenAuthorizer via the apigateway.TokenAuthorizerProps props causes the cyclic dependency error.

Below is an example stack trace:

Error: 'lambda' depends on 'apigateway' (lambda -> apigateway/MyApiGateWay/RestApi/Resource.Ref, lambda -> apigateway/MyApiGateWay/TokenAuthoriser/Resource.Ref). Adding this dependency (apigateway -> lambda/MyTokenAuthoriser/Function/Resource.Arn) would create a cyclic reference.

    at ApiGatewayStack._addAssemblyDependency (/home/builds/project/node_modules/@aws-cdk/core/lib/stack.js:409:19)

    at Object.addDependency (/home/builds/project/node_modules/@aws-cdk/core/lib/deps.js:39:24)

    at ApiGatewayStack.addDependency (/home/builds/project/node_modules/@aws-cdk/core/lib/stack.js:188:16)

    at resolveValue (/home/builds/project/node_modules/@aws-cdk/core/lib/private/refs.js:84:14)

    at Object.resolveReferences (/home/builds/project/node_modules/@aws-cdk/core/lib/private/refs.js:26:30)

    at Object.prepareApp (/home/builds/project/node_modules/@aws-cdk/core/lib/private/prepare-app.js:35:16)

    at App.prepare (/home/builds/project/node_modules/@aws-cdk/core/lib/app.js:81:23)

    at App.onPrepare (/home/builds/project/node_modules/@aws-cdk/core/lib/construct-compat.js:71:14)

    at Node.prepare (/home/builds/project/node_modules/constructs/lib/construct.js:365:20)

    at Node.synthesize (/home/builds/project/node_modules/constructs/lib/construct.js:323:14)
rjoseph-resilient commented 4 years ago

Hi,

For anyone else who may run into this...

I have worked around this by importing the Lambda via its ARN instead and setting up a custom Role.

Example:

import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import * as apigateway from '@aws-cdk/aws-apigateway';

const tokenAuthLambdaFn = lambda.Function.fromFunctionArn(
      this,
      'tokenAuthoriser',
      `arn:aws:lambda:${region}:${account}:function:my-lambda-tokenAuthoriser`
    );

    // Role and policies to allow TokenAuthoriser Lambda to be invoked by API Gateway
    const invokeTokenAuthoriserRole = new iam.Role(this, 'Role', {
      roleName: 'my-api-gateway-role`,
      assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com')
    });
    const invokeTokenAuthoriserPolicyStatement = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      sid: 'AllowInvokeLambda',
      resources: ['*'], // or the ARN of your TokenAuthoriser  Lambda
      actions: ['lambda:InvokeFunction']
    });
    const policy = new iam.Policy(this, 'Policy', {
      policyName: 'my-api-gateway-policy',
      roles: [invokeTokenAuthoriserRole],
      statements: [invokeTokenAuthoriserPolicyStatement ]
    });

    const authorizer = new apigateway.TokenAuthorizer(this, 'TokenAuthoriser', {
      handler: tokenAuthLambdaFn,
      assumeRole: invokeTokenAuthoriserRole
    });

Finally I apply the authorizer to the required method:

rest.addResource('endpoint').addMethod('POST', lambdaIntegration, {
        requestModels: { 'application/json': new apigateway.Model() },
        requestValidator,
        authorizer // This is the TokenAuthorizer from above example
      });
    }

Hope you find this helpful.

Kind Regards,

Ricardo

nija-at commented 4 years ago

This is likely happening because the AWS::Lambda::Permission resource type depends on the the ARN of the authorizer in order to set up the correct permissions, while the authorizer depends on the lambda function -

                                   +----------------+
                                   |Stack 2         |
                                   |                |
      +----------------+           |                |
      |Stack 1         |           | +----------+   |
      |                |           | |          |   |
      | +-----------+  |           | |  Lambda  |   |
      | |           |  |     +------>| Function |   |
      | | Rest API  |  |     |     | |          |   |
      | |           |  |     |     | +----------+   |
      | +-----+-----+  |     |     |       ^        |
      |       |        |     |     |       |        |
      |       |        |     |     |       |        |
      |       v        |     |     |       |        |
      | +------------+ |     |     | +-----+------+ |
      | |            +-------+     | |            | |
      | | Authorizer | |           | |   Lambda   | |
      | |            |<--------------+ Permission | |
      | +------------+ |           | |            | |
      +----------------+           | +------------+ |
                                   |                |
                                   +----------------+

This causes the two stacks to depend on each other.

The fix likely involves moving the Lambda Permission object to the source stack (i.e., Stack 1) by setting the correct scope property on the addPermissions() API.

outlander24 commented 4 years ago

@nija-at Any updates/ETA on this issue?

sureshmnv commented 3 years ago

@nija-at: looks like this is still an unresolved issue. I'm still running into it with CDK version (1.120.0 (build 6c15150)). I'm not fond of workaround mentioned above. thanks...

nija-at commented 3 years ago

Sorry, we are unable to attend to this issue right now.

We are accepting pull requests if anyone is interested in submitting a fix.

miekassu commented 3 years ago

This is problematic, I would love to see this issue fixed.

OGoodness commented 2 years ago

+1

phamhuy commented 2 years ago

Facing same problem. Not sure how, but it works when I use HttpLambdaAuthorizer + Http API with the same dependency graph as stated in this issue. But when I switched to TokenAuthorizer + Rest API, I got the issue.

dsolowitz commented 1 year ago

This is still an issue, is there any timeline on this?