aws-cdk icon indicating copy to clipboard operation
aws-cdk copied to clipboard

support for stage variables in lambda and https integration

Open lyenliang opened this issue 5 years ago • 15 comments

: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

  • CDK CLI Version: 1.22.0 (build 309ac1b)
  • Module Version:
  • OS: OSX Mojave
  • Language: Python

Other information

lyenliang avatar Feb 06 '20 05:02 lyenliang

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

lava-dukanam avatar Mar 11 '20 13:03 lava-dukanam

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?

rampatina avatar Mar 11 '20 13:03 rampatina

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.

kling-appfire avatar Mar 11 '20 14:03 kling-appfire

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.

nija-at avatar Mar 14 '20 14:03 nija-at

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.

rampatina avatar Mar 16 '20 06:03 rampatina

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.

nija-at avatar Mar 16 '20 11:03 nija-at

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.

wfarn avatar May 13 '20 22:05 wfarn

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

paulsjohnson91 avatar Oct 29 '20 13:10 paulsjohnson91

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 */
            },
})

thovden avatar Nov 02 '20 12:11 thovden

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());

paulsjohnson91 avatar Nov 02 '20 12:11 paulsjohnson91

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 */
            },
})

baumannalexj avatar Mar 11 '21 03:03 baumannalexj

any updates ?

maddyexplore avatar Dec 06 '22 17:12 maddyexplore

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

nicofabre avatar Mar 29 '24 11:03 nicofabre

Does anyone have any updates regarding this issue?

InTomas98 avatar May 08 '24 10:05 InTomas98

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 -

  • Two environments, dev & prod
  • Deploy lambda to dev ( using dev alias pointing to $LATEST)
  • After testing, promote same lambda by creating a new version from $LATEST and attach to prod alias.

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.

  • managing different environment variables for different aliases - leads to more complexity
  • promotion ( create new version ) has to be outside of the cdk ( after testing ) but that changes the CFN template and next cdk deploy fails

just4give avatar Aug 29 '24 02:08 just4give