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.
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.
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'
};
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With