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.61k stars 3.91k forks source link

support for stage variables in lambda and https integration #6143

Open lyenliang opened 4 years ago

lyenliang commented 4 years ago

:question: General Issue

How to pass a stage variable to lambda function field in API Gateway?

I have an API Gateway that triggers a lambda function specified by a stage variable stageVariables.lbfunc.

image

How can I create this API Gateway with AWS CDK?

It looks like that I should create a special handler for LambdaRestApi.

But I can't find any example code for doing this.

The following is my current code. I wish that LambdaIntegration's handler can be determined by a stage variable.

# dev_lambda_function should be replaced by something else
dev_lambda_function = lambda_.Function(self, "MyServiceHandler",
            runtime=lambda_.Runtime.PYTHON_3_7,
            code=lambda_.Code.asset("resources"),
            handler="lambda_function.lambda_handler",
            description="My dev lambda function"
            )
stage_options = apigateway.StageOptions(stage_name="dev", 
    description="dev environment", 
    variables=dict(lbfunc="my-func-dev")
)
# What should I pass to the handler variable so that LambdaRestApi triggers the lambda function specified by the stage variable "stageVariables.lbfunc"?
api = apigateway.LambdaRestApi(self, "my-api",
            rest_api_name="My Service",
            description="My Service API Gateway",
            handler=dev_lambda_function,    
            deploy_options=stage_options)

Environment

Other information

lava-dukanam commented 4 years ago

Any update on this issue? We are currently blocked with this and would like to know if there are any alternative approach?

rampatina commented 4 years ago

We are blocked with this issue, it is affecting our optimal design decision and forcing us to go for multiple API gateways. Our use case is: 1) Trying to deploy multiple stages for an API through CDK 2) Have one API gateway and multiples stages with stage variables (For example dev, test, and stage) 3) In the Method Integration Request part, we need to have the feasibility to pass stage variables to the Lambda function in the API gateway.

Please let us know what is the ETA?

kling-appfire commented 4 years ago

Agree with rampatina - the inability to pass stage variables negates the ability to utilize API-GW stages as they were designed/intended to be used, forcing sub-optimal design of multiple GWs; one per stage.

nija-at commented 4 years ago

Can someone point me to API Gateway;s documentation that allows this? Unfortunately, I'm not able to locate one easily enough.

I can't also find any direction on how to configure this on the uri parameter via the API or the CloudFormation parameter. The Uri property of the CloudFormation resource type AWS::ApiGateway::Method is where we specify the URI of the lambda function, in the case of lambda proxy integration.

rampatina commented 4 years ago

Here is the docs from API gateway: https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html Another reference link for sample use-case: https://aws.amazon.com/blogs/compute/using-api-gateway-stage-variables-to-manage-lambda-functions/

Now, the same configuration is not available in CDK.

nija-at commented 4 years ago

Thanks for the links to the documentation. It's not clear right away from these how to set these up with CloudFormation, but I would hazard a guess that it should be added as part of the Uri property of AWS::ApiGateway::Method resource type.

Currently, we don't support this as part of the higher level constructs in the cdk. Recording as a feature request here.

You could work around this by using CDK's escape hatches and setting the Uri property mentioned above.

Thanks.

wfarn commented 4 years ago

I had the same issue and managed to get it working using the base Integration construct. Unfortunately I'm working with Typescript so not sure if this will translate over to CDK Python (I imagine it would though).

let integration = new apigateway.Integration({
      type: apigateway.IntegrationType.AWS_PROXY,
      integrationHttpMethod: 'POST',
      uri: arn:aws:apigateway:<aws_region>:lambda:path/2015-03-31/functions/arn:aws:lambda:<aws_region>:<aws_account>:function:${stageVariables.stageFunction}/invocations',
    });

Given lambdaRestApi is a convinience class, you'll have to do additional work setting up a RestAPI, stages, methods etc to get this running. But it is possible to achieve defining the Lambda function via stage variables using the above integration construct.

Hope it helps.

paulsjohnson91 commented 3 years ago

has there been any progress on this? trying to do the same using the java cdk

thovden commented 3 years ago

So the trick I've been using is Function.fromFunctionArn with the regular function Arn plus the stageVariables path. When doing this we cannot use LambdaIntegration directly because CDK will try to add a IAM permission to that Arn automatically, and that will not be a valid permission Arn. So we need to manage the permissions ourselves, as shown below.

// Your regular lambda function
const func: lambda.Function = ...

// Create a Lambda with the dynamic stageVariables path        
const stageLambda = lambda.Function.fromFunctionArn(
  this,
  `${func.functionName}-lambda-stage`,
  `${func.functionArn}:\${stageVariables.environment}`
)

const credentialsRole = new iam.Role(this, 'apigateway-api-role', {
            assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
})
// Add the regular lambda Arns to the credentialsRole
credentialsRole.addToPolicy(
            new PolicyStatement({
                actions: ['lambda:InvokeFunction'],
                resources: [func.functionArn, `${func.functionArn}:*`],
                effect: Effect.ALLOW,
            })
        )
// Add the stageLambda Arn to the integration. 
const integration = new apigateway.AwsIntegration({
            proxy: true,
            service: 'lambda',
            path: `2015-03-31/functions/${stageLambda.functionArn}/invocations`,
            options: {
                credentialsRole,
               /* Additional options here */
            },
})
paulsjohnson91 commented 3 years ago

I took a similar approach:

public class MultiLambdaIntegration extends Integration {
    public MultiLambdaIntegration(Stack stack, String lambdaNameRoot) {
        super(IntegrationProps.builder().integrationHttpMethod("POST")
                .type(IntegrationType.AWS_PROXY)
                .uri(String.format("arn:aws:apigateway:%s:lambda:path/2015-03-31/functions/arn:aws:lambda:%s:%s:function:%s-${stageVariables.lambdaEnv}/invocations", stack.getRegion(), stack.getRegion(), stack.getAccount(), lambdaNameRoot))
                .build());
    }
}

Whilst adding permission to the lambda:

.addPermission(
        String.format("allow-api-lambda-invocation-%s-%s", lambdaNameRoot, environments[i]),
        Permission.builder()
                .action("lambda:InvokeFunction")
                .principal(
                        ServicePrincipal.Builder.create("apigateway.amazonaws.com").build())
                .sourceArn(api.getApi().arnForExecuteApi())
                .build());
baumannalexj commented 3 years ago

my god, thank you @thovden

So the trick I've been using is Function.fromFunctionArn with the regular function Arn plus the stageVariables path. When doing this we cannot use LambdaIntegration directly because CDK will try to add a IAM permission to that Arn automatically, and that will not be a valid permission Arn. So we need to manage the permissions ourselves, as shown below.

// Your regular lambda function
const func: lambda.Function = ...

// Create a Lambda with the dynamic stageVariables path        
const stageLambda = lambda.Function.fromFunctionArn(
  this,
  `${func.functionName}-lambda-stage`,
  `${func.functionArn}:\${stageVariables.environment}`
)

const credentialsRole = new iam.Role(this, 'apigateway-api-role', {
            assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
})
// Add the regular lambda Arns to the credentialsRole
credentialsRole.addToPolicy(
            new PolicyStatement({
                actions: ['lambda:InvokeFunction'],
                resources: [func.functionArn, `${func.functionArn}:*`],
                effect: Effect.ALLOW,
            })
        )
// Add the stageLambda Arn to the integration. 
const integration = new apigateway.AwsIntegration({
            proxy: true,
            service: 'lambda',
            path: `2015-03-31/functions/${stageLambda.functionArn}/invocations`,
            options: {
                credentialsRole,
               /* Additional options here */
            },
})
maddyexplore commented 1 year ago

any updates ?

nicofabre commented 6 months ago

Any updates ? Is so rare use-case ? Why AWS does not address this issue, after more than 4 years ?

InTomas98 commented 5 months ago

Does anyone have any updates regarding this issue?

just4give commented 1 month ago

I was trying to achieve same thing using apigatewayv2. Following some of the approaches above, I was able to get it working. My use-case was -

Below is what I got working. It deploys , creates two aliases. You may use this for reference.

let exampleLambda = new lambdanodejs.NodejsFunction(this, "exampleLambda", {
      functionName: `exampleLambda`,
      runtime: Runtime,
      timeout: cdk.Duration.seconds(30),
      memorySize: 256,
      entry: "lambda/exampleLambda/index.ts",
      handler: "handler",
      environment: {
        AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1",
        REGION: AWS_REGION,
      },
      bundling: {
        nodeModules: [],
        externalModules: [],
      },
      layers: [],
    });

    const exampleLambdadevalias = new lambda.Alias(this, "exampleLambda-dev-alias", {
      aliasName: "dev",
      version: exampleLambda.latestVersion,
    });

    const exampleLambdaprodalias = new lambda.Alias(this, "exampleLambda-prod-alias", {
      aliasName: "prod",
      version: exampleLambda.currentVersion,
    });

    const stageLambda = lambda.Function.fromFunctionArn(
      this,
      `${exampleLambda.functionName}-lambda-stage`,
      `${exampleLambda.functionArn}:\${stageVariables.lambdaAlias}`
    );

    //create new http api
    let exampleApi = new apigwv2.HttpApi(this, `example-api`, {
      description: `example api`,
      apiName: `example-api`,
      corsPreflight: {
        allowHeaders: ["*"],
        allowMethods: [
          apigwv2.CorsHttpMethod.OPTIONS,
          apigwv2.CorsHttpMethod.POST,
          apigwv2.CorsHttpMethod.PUT,
          apigwv2.CorsHttpMethod.GET,
          apigwv2.CorsHttpMethod.DELETE,
          apigwv2.CorsHttpMethod.PATCH,
        ],
        allowOrigins: ["*"],
        allowCredentials: false,
      },
    });
    //enable access log

    exampleApi.addRoutes({
      path: "/todo",
      methods: [apigwv2.HttpMethod.GET],
      integration: new HttpLambdaIntegration("example-lambda-integ", stageLambda),
    });

    const devStage = new apigwv2.CfnStage(this, "DevStage", {
      apiId: exampleApi.httpApiId,
      stageName: "dev",
      autoDeploy: true,
      stageVariables: {
        lambdaAlias: "dev",
      },
    });

    const prodStage = new apigwv2.CfnStage(this, "ProdStage", {
      apiId: exampleApi.httpApiId,
      stageName: "prod",
      autoDeploy: true,
      stageVariables: {
        lambdaAlias: "prod",
      },
    });

    exampleLambdadevalias.addPermission("AllowApiGatewayInvoke", {
      principal: new iam.ServicePrincipal("apigateway.amazonaws.com"),
      action: "lambda:InvokeFunction",
      sourceArn: `arn:aws:execute-api:${AWS_REGION}:${AWS_ACCOUNT}:${exampleApi.httpApiId}/dev/*`,
    });

    exampleLambdaprodalias.addPermission("AllowApiGatewayInvoke", {
      principal: new iam.ServicePrincipal("apigateway.amazonaws.com"),
      action: "lambda:InvokeFunction",
      sourceArn: `arn:aws:execute-api:${AWS_REGION}:${AWS_ACCOUNT}:${exampleApi.httpApiId}/prod/*`,
    });

But I moved away from this approach in the end for few reasons.