28

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.

Darragh Enright
  • 13,676
  • 7
  • 41
  • 48

1 Answers1

17

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'
  };
}
dmahapatro
  • 49,365
  • 7
  • 88
  • 117
  • Hey @dmahapatro — ah, this looks great! Thanks a million. I'll try this out ASAP. – Darragh Enright Jun 20 '20 at 05:31
  • This works a treat! Fantastic. Thanks again. It's a lot more straightforward than I anticipated, I'm wondering if the `grantInvoke` part was obvious but I overlooked it, or if it's just not really documented? – Darragh Enright Jun 20 '20 at 12:27
  • 1
    Great question. I believe that detail is missing because `api.deploymentStage = prodStage` takes cares of granting invoke implicitly, thus we end up proper invoke access for only one stage. If `RestApi` could provide a way to provision multiple stages at the same time, then `grantInvoke` will not be required explicitly. `api.deploymentStages = [prodStage, devStage, testStage]` could have been helpful. – dmahapatro Jun 21 '20 at 16:15
  • 1
    Cool, thanks for the update. Great answer, learned a lot from this! – Darragh Enright Jun 22 '20 at 10:48
  • 1
    @DarraghEnright AWS released AWS Solutions Construct today. Take a look if you can leverage any of their higher order constructs https://docs.aws.amazon.com/solutions/latest/constructs/walkthrough-part-1.html – dmahapatro Jun 23 '20 at 21:59
  • Interesting! Good timing too. Thanks! – Darragh Enright Jun 24 '20 at 00:10
  • 4
    Thanks for the answer, but I came into another issue about this matters. When assigning the same deployment variable (class Deployment) to all the Stages created, makes them all receive the lates deploy structure. The question provided was that one of the Stages (for example 'dev') would receive the latest deployment and the other 2 would remain on their acctual deployments. This way it would be possible to keep the production in a stable structure and keep working on releases to dev or test Stages so it can be tested without affecting the production Stage. – Eldad Hauzman May 23 '21 at 03:49
  • 5
    does this mean all stages pointing to the same lambda? – Khoa Sep 08 '21 at 09:37
  • do we still end up with `/prod` segment in the URL or does the construct know it should be removed or must I name the stage *$default* to prevent having it (or any other way)? – Greg Wozniak Mar 09 '23 at 20:51