Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AWS x-ray support for API Gateway #5692

Merged
merged 24 commits into from
Apr 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
80a672a
add support for apigw xray traces by way of stageDescription
softprops Jan 13, 2019
cfafd3c
fix 'npm run lint' errors
softprops Jan 13, 2019
a30e6db
Link to a better xray gateway reference link
softprops Jan 13, 2019
c2a4e93
add aws provider test for unsupported apigw stage desc fields
softprops Jan 13, 2019
6b2853b
correct documentation
softprops Jan 13, 2019
3517b1f
type in test description
softprops Jan 14, 2019
ac20a02
Merge branch 'master' of github.com:serverless/serverless into deploy…
softprops Feb 6, 2019
1863942
Merge branch 'master' into deployment-stage-description
pmuens Feb 27, 2019
9549b0a
Update implementation to match Lambda X-Ray config
pmuens Feb 27, 2019
9e046b1
Rewrite logic to conditionally add a Stage resource
pmuens Feb 28, 2019
06cfc51
Merge branch 'master' into deployment-stage-description
pmuens Mar 4, 2019
879da30
Remove suffixing of stage name in stage logical id
pmuens Mar 4, 2019
6b130f4
Update docs
pmuens Mar 4, 2019
d2a930b
Remove unnecessary passing-in of stage name to naming function
pmuens Mar 4, 2019
92a9189
Merge branch 'master' into deployment-stage-description
pmuens Mar 13, 2019
4143573
Merge branch 'master' into deployment-stage-description
pmuens Apr 10, 2019
f646853
Update docs with note about CloudFormation limitations
pmuens Apr 10, 2019
27bc1bd
Add custom error message when X-Ray tracing deployments fail
pmuens Apr 10, 2019
527a906
Add strict mode declarative
pmuens Apr 11, 2019
d7dc5ab
Remove custom stack error messages
pmuens Apr 12, 2019
dd48f2d
Add check for breaking changes
pmuens Apr 12, 2019
76269bb
Merge branch 'master' into deployment-stage-description
pmuens Apr 12, 2019
43da8b3
Update docs
pmuens Apr 12, 2019
838c9dd
Fix tests
pmuens Apr 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/providers/aws/events/apigateway.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ layout: Doc
- [Share Authorizer](#share-authorizer) - [Share Authorizer](#share-authorizer)
- [Resource Policy](#resource-policy) - [Resource Policy](#resource-policy)
- [Compression](#compression) - [Compression](#compression)
- [AWS X-Ray Tracing](#aws-x-ray-tracing)


_Are you looking for tutorials on using API Gateway? Check out the following resources:_ _Are you looking for tutorials on using API Gateway? Check out the following resources:_


Expand Down Expand Up @@ -1308,3 +1309,21 @@ provider:
apiGateway: apiGateway:
minimumCompressionSize: 1024 minimumCompressionSize: 1024
``` ```

## AWS X-Ray Tracing

**IMPORTANT:** Due to CloudFormation limitations it's not possible to enable AWS X-Ray Tracing on existing deployments. Please remove your old API Gateway and re-deploy it with enabled tracing if you want to use AWS X-Ray Tracing for API Gateway. Once tracing is enabled you can re-deploy your service anytime without issues.

Disabling tracing might result in unexpected behavior. We recommend to remove and re-deploy your service if you want to disable tracing.

API Gateway supports a form of out of the box distributed tracing via [AWS X-Ray](https://aws.amazon.com/xray/) though enabling [active tracing](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-xray.html). To enable this feature for your serverless
application's API Gateway add the following to your `serverless.yml`

```yml
# serverless.yml

provider:
name: aws
tracing:
apiGateway: true
```
1 change: 1 addition & 0 deletions docs/providers/aws/guide/resources.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ If you are unsure how a resource is named, that you want to reference from your
|ApiGateway::ApiKey | ApiGatewayApiKey{SequentialID} | ApiGatewayApiKey1 | |ApiGateway::ApiKey | ApiGatewayApiKey{SequentialID} | ApiGatewayApiKey1 |
|ApiGateway::UsagePlan | ApiGatewayUsagePlan | ApiGatewayUsagePlan | |ApiGateway::UsagePlan | ApiGatewayUsagePlan | ApiGatewayUsagePlan |
|ApiGateway::UsagePlanKey | ApiGatewayUsagePlanKey{SequentialID} | ApiGatewayUsagePlanKey1 | |ApiGateway::UsagePlanKey | ApiGatewayUsagePlanKey{SequentialID} | ApiGatewayUsagePlanKey1 |
|ApiGateway::Stage | ApiGatewayStage | ApiGatewayStage |
|SNS::Topic | SNSTopic{normalizedTopicName} | SNSTopicSometopic | |SNS::Topic | SNSTopic{normalizedTopicName} | SNSTopicSometopic |
|SNS::Subscription | {normalizedFunctionName}SnsSubscription{normalizedTopicName} | HelloSnsSubscriptionSomeTopic | |SNS::Subscription | {normalizedFunctionName}SnsSubscription{normalizedTopicName} | HelloSnsSubscriptionSomeTopic |
|AWS::Lambda::EventSourceMapping | <ul><li>**DynamoDB:** {normalizedFunctionName}EventSourceMappingDynamodb{tableName}</li><li>**Kinesis:** {normalizedFunctionName}EventSourceMappingKinesis{streamName}</li></ul> | <ul><li>**DynamoDB:** HelloLambdaEventSourceMappingDynamodbUsers</li><li>**Kinesis:** HelloLambdaEventSourceMappingKinesisMystream</li></ul> | |AWS::Lambda::EventSourceMapping | <ul><li>**DynamoDB:** {normalizedFunctionName}EventSourceMappingDynamodb{tableName}</li><li>**Kinesis:** {normalizedFunctionName}EventSourceMappingKinesis{streamName}</li></ul> | <ul><li>**DynamoDB:** HelloLambdaEventSourceMappingDynamodbUsers</li><li>**Kinesis:** HelloLambdaEventSourceMappingKinesisMystream</li></ul> |
Expand Down
1 change: 1 addition & 0 deletions docs/providers/aws/guide/serverless.yml.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ provider:
foo: bar foo: bar
baz: qux baz: qux
tracing: tracing:
apiGateway: true
lambda: true # optional, can be true (true equals 'Active'), 'Active' or 'PassThrough' lambda: true # optional, can be true (true equals 'Active'), 'Active' or 'PassThrough'


package: # Optional deployment packaging configuration package: # Optional deployment packaging configuration
Expand Down
3 changes: 3 additions & 0 deletions lib/plugins/aws/lib/naming.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ module.exports = {
getUsagePlanKeyLogicalId(usagePlanKeyNumber) { getUsagePlanKeyLogicalId(usagePlanKeyNumber) {
return `ApiGatewayUsagePlanKey${usagePlanKeyNumber}`; return `ApiGatewayUsagePlanKey${usagePlanKeyNumber}`;
}, },
getStageLogicalId() {
return 'ApiGatewayStage';
},


// S3 // S3
getDeploymentBucketLogicalId() { getDeploymentBucketLogicalId() {
Expand Down
6 changes: 6 additions & 0 deletions lib/plugins/aws/lib/naming.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -426,6 +426,12 @@ describe('#naming()', () => {
}); });
}); });


describe('#getStageLogicalId()', () => {
it('should return the API Gateway stage logical id', () => {
expect(sdk.naming.getStageLogicalId()).to.equal('ApiGatewayStage');
});
});

describe('#getDeploymentBucketLogicalId()', () => { describe('#getDeploymentBucketLogicalId()', () => {
it('should return "ServerlessDeploymentBucket"', () => { it('should return "ServerlessDeploymentBucket"', () => {
expect(sdk.naming.getDeploymentBucketLogicalId()).to.equal('ServerlessDeploymentBucket'); expect(sdk.naming.getDeploymentBucketLogicalId()).to.equal('ServerlessDeploymentBucket');
Expand Down
10 changes: 8 additions & 2 deletions lib/plugins/aws/package/compile/events/apiGateway/index.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ const compileMethods = require('./lib/method/index');
const compileAuthorizers = require('./lib/authorizers'); const compileAuthorizers = require('./lib/authorizers');
const compileDeployment = require('./lib/deployment'); const compileDeployment = require('./lib/deployment');
const compilePermissions = require('./lib/permissions'); const compilePermissions = require('./lib/permissions');
const compileStage = require('./lib/stage');
const getMethodAuthorization = require('./lib/method/authorization'); const getMethodAuthorization = require('./lib/method/authorization');
const getMethodIntegration = require('./lib/method/integration'); const getMethodIntegration = require('./lib/method/integration');
const getMethodResponses = require('./lib/method/responses'); const getMethodResponses = require('./lib/method/responses');
const checkForBreakingChanges = require('./lib/checkForBreakingChanges');


class AwsCompileApigEvents { class AwsCompileApigEvents {
constructor(serverless, options) { constructor(serverless, options) {
Expand All @@ -39,9 +41,11 @@ class AwsCompileApigEvents {
compileAuthorizers, compileAuthorizers,
compileDeployment, compileDeployment,
compilePermissions, compilePermissions,
compileStage,
getMethodAuthorization, getMethodAuthorization,
getMethodIntegration, getMethodIntegration,
getMethodResponses getMethodResponses,
checkForBreakingChanges
); );


this.hooks = { this.hooks = {
Expand All @@ -62,7 +66,9 @@ class AwsCompileApigEvents {
.then(this.compileApiKeys) .then(this.compileApiKeys)
.then(this.compileUsagePlan) .then(this.compileUsagePlan)
.then(this.compileUsagePlanKeys) .then(this.compileUsagePlanKeys)
.then(this.compilePermissions); .then(this.compilePermissions)
.then(this.compileStage)
.then(this.checkForBreakingChanges);
}, },


// TODO should be removed once AWS fixes the removal via CloudFormation // TODO should be removed once AWS fixes the removal via CloudFormation
Expand Down
10 changes: 10 additions & 0 deletions lib/plugins/aws/package/compile/events/apiGateway/index.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ describe('AwsCompileApigEvents', () => {
let compileDeploymentStub; let compileDeploymentStub;
let compileUsagePlanStub; let compileUsagePlanStub;
let compilePermissionsStub; let compilePermissionsStub;
let compileStageStub;
let checkForBreakingChangesStub;
let disassociateUsagePlanStub; let disassociateUsagePlanStub;


beforeEach(() => { beforeEach(() => {
Expand All @@ -56,6 +58,10 @@ describe('AwsCompileApigEvents', () => {
.stub(awsCompileApigEvents, 'compileUsagePlan').resolves(); .stub(awsCompileApigEvents, 'compileUsagePlan').resolves();
compilePermissionsStub = sinon compilePermissionsStub = sinon
.stub(awsCompileApigEvents, 'compilePermissions').resolves(); .stub(awsCompileApigEvents, 'compilePermissions').resolves();
compileStageStub = sinon
.stub(awsCompileApigEvents, 'compileStage').resolves();
checkForBreakingChangesStub = sinon
.stub(awsCompileApigEvents, 'checkForBreakingChanges').resolves();
disassociateUsagePlanStub = sinon disassociateUsagePlanStub = sinon
.stub(disassociateUsagePlan, 'disassociateUsagePlan').resolves(); .stub(disassociateUsagePlan, 'disassociateUsagePlan').resolves();
}); });
Expand All @@ -67,6 +73,8 @@ describe('AwsCompileApigEvents', () => {
awsCompileApigEvents.compileDeployment.restore(); awsCompileApigEvents.compileDeployment.restore();
awsCompileApigEvents.compileUsagePlan.restore(); awsCompileApigEvents.compileUsagePlan.restore();
awsCompileApigEvents.compilePermissions.restore(); awsCompileApigEvents.compilePermissions.restore();
awsCompileApigEvents.compileStage.restore();
awsCompileApigEvents.checkForBreakingChanges.restore();
disassociateUsagePlan.disassociateUsagePlan.restore(); disassociateUsagePlan.disassociateUsagePlan.restore();
}); });


Expand Down Expand Up @@ -100,6 +108,8 @@ describe('AwsCompileApigEvents', () => {
expect(compileDeploymentStub.calledAfter(compileMethodsStub)).to.be.equal(true); expect(compileDeploymentStub.calledAfter(compileMethodsStub)).to.be.equal(true);
expect(compileUsagePlanStub.calledAfter(compileDeploymentStub)).to.be.equal(true); expect(compileUsagePlanStub.calledAfter(compileDeploymentStub)).to.be.equal(true);
expect(compilePermissionsStub.calledAfter(compileUsagePlanStub)).to.be.equal(true); expect(compilePermissionsStub.calledAfter(compileUsagePlanStub)).to.be.equal(true);
expect(compileStageStub.calledAfter(compilePermissionsStub)).to.be.equal(true);
expect(checkForBreakingChangesStub.calledAfter(compileStageStub)).to.be.equal(true);


awsCompileApigEvents.validate.restore(); awsCompileApigEvents.validate.restore();
}); });
Expand Down
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';

const BbPromise = require('bluebird');

// NOTE: the checks here are X-Ray specific. However the error messages can be updated
// to reflect the general problem which occurrs when upgrading / downgrading the
// Stage resource / Deplyment resource

module.exports = {
checkForBreakingChanges() {
const StackName = this.provider.naming.getStackName();
return this.provider.request('CloudFormation',
'getTemplate', { StackName }).then((res) => {
if (res) {
const oldResources = JSON.parse(res.TemplateBody).Resources;
const newResources = this.serverless.service.provider
.compiledCloudFormationTemplate.Resources;
const deploymentLogicalIdRegex =
new RegExp(this.provider.naming.generateApiGatewayDeploymentLogicalId(''));
const oldDeploymentLogicalId = Object.keys(oldResources)
.filter(elem => elem.match(deploymentLogicalIdRegex)).shift();
const newDeploymentLogicalId = Object.keys(newResources)
.filter(elem => elem.match(deploymentLogicalIdRegex)).shift();
const stageLogicalId = this.provider.naming.getStageLogicalId();

// 1. if the user wants to upgrade to use the new AWS::APIGateway::Stage resource but
// the old state still uses the stage defined on the AWS::ApiGateway::Deployment resource
if (oldResources[oldDeploymentLogicalId] && oldResources[oldDeploymentLogicalId].Properties.StageName && newResources[stageLogicalId]) { // eslint-disable-line max-len
const msg = [
'NOTE: Enabling API Gateway X-Ray Tracing for existing ',
'deployments requires a remove and re-deploy of your API Gateway. ',
'\n\n ',
'Please refer to our documentation for more information.',
].join('');
throw new this.serverless.classes.Error(msg);
}

// 2. if the user wants to downgrade from a dedicated AWS::ApiGateway::Stage resource
// to the stage being embedded in the AWS::ApiGateway::Deployment resource
if (oldResources[stageLogicalId] && newResources[newDeploymentLogicalId] && newResources[newDeploymentLogicalId].Properties.StageName) { // eslint-disable-line
if (!this.options.force) {
const msg = [
'NOTE: Disabling API Gateway X-Ray Tracing for existing ',
'deployments might result in unexpected behavior.',
'\n ',
'We recommend to remove and re-deploy your API Gateway. ',
'Use the --force option if you want to proceed with the deployment. ',
'\n\n ',
'Please refer to our documentation for more information.',
].join('');
throw new this.serverless.classes.Error(msg);
}
}
}
}).catch((error) => {
// in this case it's the first deployment so there's no template available to fetch
if (error.providerError && error.providerError.code === 'ValidationError') {
return BbPromise.resolve();
}
throw error;
});
},
};
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,125 @@
'use strict';

const chai = require('chai');
const sinon = require('sinon');
const AwsCompileApigEvents = require('../index');
const Serverless = require('../../../../../../../Serverless');
const AwsProvider = require('../../../../../provider/awsProvider');

chai.use(require('chai-as-promised'));
const expect = require('chai').expect;

describe('#checkForBreakingChanges()', () => {
let serverless;
let options;
let awsCompileApigEvents;
let stageLogicalId;
let deploymentLogicalId;
let getTemplateStub;

beforeEach(() => {
serverless = new Serverless();
serverless.setProvider('aws', new AwsProvider(serverless));
awsCompileApigEvents = new AwsCompileApigEvents(serverless);
serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} };
options = {
stage: 'dev',
region: 'us-east-1',
};
awsCompileApigEvents.serverless = serverless;
awsCompileApigEvents.provider = new AwsProvider(serverless, options);
awsCompileApigEvents.options = options;
stageLogicalId = awsCompileApigEvents
.provider.naming.getStageLogicalId();
deploymentLogicalId = awsCompileApigEvents
.provider.naming.generateApiGatewayDeploymentLogicalId('');
getTemplateStub = sinon
.stub(awsCompileApigEvents.provider, 'request');
});

afterEach(() => {
getTemplateStub.restore();
});

it('should resolve when Stage / Deployment resources are used', () => {
const oldTemplate = JSON.stringify({
Resources: {},
});
getTemplateStub.resolves({
TemplateBody: oldTemplate,
});

return expect(awsCompileApigEvents.checkForBreakingChanges()).to.be.fulfilled;
});

describe('when upgrading to use the new, dedicated AWS::ApiGateway::Stage resource', () => {
it('should throw with a helpul error message', () => {
// the old state
const oldTemplate = JSON.stringify({
Resources: {
[deploymentLogicalId]: {
Properties: {
StageName: 'dev',
},
},
},
});
getTemplateStub.resolves({
TemplateBody: oldTemplate,
});

// the new state
awsCompileApigEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources[stageLogicalId] = {};

return awsCompileApigEvents.checkForBreakingChanges()
.should.be.rejectedWith(/NOTE: Enabling/);
});
});

describe('when downgrading to use AWS::ApiGateway::Deployment embedded stage', () => {
beforeEach(() => {
// the old state
const oldTemplate = JSON.stringify({
Resources: {
[stageLogicalId]: {},
},
});
getTemplateStub.resolves({
TemplateBody: oldTemplate,
});

// the new state
awsCompileApigEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources[deploymentLogicalId] = {
Properties: {
StageName: 'dev',
},
};
});

it('should throw with a helpul error message', () => awsCompileApigEvents
.checkForBreakingChanges().should.be.rejectedWith(/NOTE: Disabling/)
);

it('should resolve if the user uses the --force option', () => {
options.force = true;

return expect(awsCompileApigEvents.checkForBreakingChanges()).to.resolve;
});
});

it('should resolve when no stack can be found', () => {
getTemplateStub.rejects({
providerError: {
code: 'ValidationError',
},
});
return expect(awsCompileApigEvents.checkForBreakingChanges()).to.resolve;
});

it('should re-throw an error when a stack can be found but something went wrong', () => {
getTemplateStub.rejects('Whoops... Something went wrong');
return awsCompileApigEvents.checkForBreakingChanges().should.be.rejectedWith(/Whoops/);
});
});
42 changes: 42 additions & 0 deletions lib/plugins/aws/package/compile/events/apiGateway/lib/stage.js
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

const _ = require('lodash');
const BbPromise = require('bluebird');

module.exports = {
compileStage() {
// NOTE: right now we're only using a dedicated Stage resource if AWS X-Ray
// tracing is enabled. We'll change this in the future so that users can
// opt-in for other features as well
const tracing = this.serverless.service.provider.tracing;

if (!_.isEmpty(tracing) && tracing.apiGateway) {
// NOTE: the DeploymentId is random, therefore we rely on prior usage here
const deploymentId = this.apiGatewayDeploymentLogicalId;
this.apiGatewayStageLogicalId = this.provider.naming
.getStageLogicalId();

_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[this.apiGatewayStageLogicalId]: {
Type: 'AWS::ApiGateway::Stage',
Properties: {
DeploymentId: {
Ref: deploymentId,
},
RestApiId: this.provider.getApiGatewayRestApiId(),
StageName: this.provider.getStage(),
TracingEnabled: true,
},
},
});

// we need to remove the stage name from the Deployment resource
delete this.serverless.service.provider.compiledCloudFormationTemplate
.Resources[deploymentId]
.Properties
.StageName;
}

return BbPromise.resolve();
},
};