Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deploy multiple API Gateway stages with AWS CDK

API Gateway has the concept of stages (e.g: dev, test, prod), and deploying multiple stages via the AWS Console is very straightforward.

Is it possible to define and deploy multiple stages with AWS CDK?

I've tried but so far it does not seem possible. The following is an abridged example of a very basic stack that constructs an API Gateway RestApi to serve a lambda function:

export class TestStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Define stage at deploy time; e.g: STAGE=prod cdk deploy
    const STAGE = process.env.STAGE || 'dev'

    // First, create a test lambda
    const testLambda = new apilambda.Function(this, 'test_lambda', {
      runtime: apilambda.Runtime.NODEJS_10_X,    
      code: apilambda.Code.fromAsset('lambda'),  
      handler: 'test.handler',
      environment: { STAGE }
    })

    // Then, create the API construct, integrate with lambda and define a catch-all method
    const api = new apigw.RestApi(this, 'test_api', { deploy: false });
    const integration = new apigw.LambdaIntegration(testLambda);

    api.root.addMethod('ANY', integration)

    // Then create an explicit Deployment construct
    const deployment  = new apigw.Deployment(this, 'test_deployment', { api });

    // And, a Stage construct
    const stage = new apigw.Stage(this, 'test_stage', { 
      deployment,
      stageName: STAGE
    });

    // There doesn't seem to be a way to add more than one stage...
    api.deploymentStage = stage
  }
}

I'm not using LambdaRestApi because there's a bug that doesn't allow an explicit Deployment, which is apparently necessary to explicitly define a Stage. This approach requires the extra LambdaIntegration step.

This stack works well enough — I can deploy a new stack and define the API Gateway stage with an environment variable; e.g: STAGE=my_stack_name cdk deploy.

I hoped this would allow me to add stages by doing the following:

STAGE=test cdk deploy
STAGE=prod cdk deploy
# etc.

However, this does not work — in the above example the test stage is overwritten by the prod stage.

Prior to trying the above approach, I figured one would simply create one or more Stage construct objects and assign them to the same deployment (which already takes the RestApi as an argument).

However, it's necessary to explicitly assign a stage to the api via api.deploymentStage = stage and it looks like only one can be assigned.

This implies that it's not possible, instead you would have to create a different stack for test, prod etc. Which implies multiple instances of the same API Gateway and Lambda function.

Update

After further tinkering, I've discovered that it appears to possible to deploy more than one stage, although I am not quite out of the woods yet...

Firstly, revert to the default behaviour of RestApi — remove prop deploy: false which automatically creates a Deployment:

const api = new apigw.RestApi(this, 'test_api');

Then, as before, create an explicit Deployment construct:

const deployment  = new apigw.Deployment(this, 'test_deployment', { api });

At this point, it's important to note that a prod stage is already defined, and cdk deploy will fail if you explicitly create a Stage construct for prod.

Instead, create a Stage construct for every other stage you want to create; e.g:

new apigw.Stage(this, 'stage_test', { deployment, stageName: 'test' });
new apigw.Stage(this, 'stage_dev', { deployment, stageName: 'dev' });
// etc.

This deploys and prod works as expected. However, both test and dev will fail with 500 Internal Server Error and the following error message:

Execution failed due to configuration error: Invalid permissions on Lambda function

Manually reassigning the lambda in AWS Console applies the permissions. I have not yet figured out how to resolve this in CDK.

like image 397
Darragh Enright Avatar asked Jun 18 '20 11:06

Darragh Enright


1 Answers

This should do the trick. Note that I have renamed resources from test_lambda to my_lambda to avoid confusion with stage names. Also note that I have removed the environment variable to lambda for brevity.

import * as cdk from '@aws-cdk/core';
import * as apigw from '@aws-cdk/aws-apigateway';
import * as lambda from '@aws-cdk/aws-lambda';
import { ServicePrincipal } from '@aws-cdk/aws-iam';

export class ApigwDemoStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // First, create a test lambda
    const myLambda = new lambda.Function(this, 'my_lambda', {
      runtime: lambda.Runtime.NODEJS_10_X,    
      code: lambda.Code.fromAsset('lambda'),  
      handler: 'test.handler'
    });

    // IMPORTANT: Lambda grant invoke to APIGateway
    myLambda.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com'));

    // Then, create the API construct, integrate with lambda
    const api = new apigw.RestApi(this, 'my_api', { deploy: false });
    const integration = new apigw.LambdaIntegration(myLambda);
    api.root.addMethod('ANY', integration)

    // Then create an explicit Deployment construct
    const deployment  = new apigw.Deployment(this, 'my_deployment', { api });

    // And different stages
    const [devStage, testStage, prodStage] = ['dev', 'test', 'prod'].map(item => 
      new apigw.Stage(this, `${item}_stage`, { deployment, stageName: item }));

    api.deploymentStage = prodStage
  }
}

Important part to note here is:

myLambda.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com'));

Explicitly granting invoke access to API Gateway allows all of the other stages (that are not associated to API directly) to not throw below error:

Execution failed due to configuration error: Invalid permissions on Lambda function

I had to test it out by explicitly creating another stage from console and enabling log tracing. API Gateway execution logs for the api and stage captures this particular error.

I have tested this out myself. This should resolve your problem. I would suggest to create a new stack altogether to test this out.

My super simple Lambda code:

// lambda/test.ts
export const handler = async (event: any = {}) : Promise <any> => {
  console.log("Inside Lambda");
  return { 
    statusCode: 200, 
    body: 'Successfully Invoked Lambda through API Gateway'
  };
}
like image 151
dmahapatro Avatar answered Sep 22 '22 21:09

dmahapatro