Merge pull request #5692 from softprops/deployment-stage-description

Add AWS x-ray support for API Gateway
This commit is contained in:
Philipp Muens 2019-04-15 12:26:01 +02:00 committed by GitHub
commit 90a7adf8f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 368 additions and 2 deletions

View File

@ -46,6 +46,7 @@ layout: Doc
- [Share Authorizer](#share-authorizer)
- [Resource Policy](#resource-policy)
- [Compression](#compression)
- [AWS X-Ray Tracing](#aws-x-ray-tracing)
_Are you looking for tutorials on using API Gateway? Check out the following resources:_
@ -1308,3 +1309,21 @@ provider:
apiGateway:
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
```

View File

@ -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::UsagePlan | ApiGatewayUsagePlan | ApiGatewayUsagePlan |
|ApiGateway::UsagePlanKey | ApiGatewayUsagePlanKey{SequentialID} | ApiGatewayUsagePlanKey1 |
|ApiGateway::Stage | ApiGatewayStage | ApiGatewayStage |
|SNS::Topic | SNSTopic{normalizedTopicName} | SNSTopicSometopic |
|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> |

View File

@ -121,6 +121,7 @@ provider:
foo: bar
baz: qux
tracing:
apiGateway: true
lambda: true # optional, can be true (true equals 'Active'), 'Active' or 'PassThrough'
package: # Optional deployment packaging configuration

View File

@ -254,6 +254,9 @@ module.exports = {
getUsagePlanKeyLogicalId(usagePlanKeyNumber) {
return `ApiGatewayUsagePlanKey${usagePlanKeyNumber}`;
},
getStageLogicalId() {
return 'ApiGatewayStage';
},
// S3
getDeploymentBucketLogicalId() {

View File

@ -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()', () => {
it('should return "ServerlessDeploymentBucket"', () => {
expect(sdk.naming.getDeploymentBucketLogicalId()).to.equal('ServerlessDeploymentBucket');

View File

@ -13,9 +13,11 @@ const compileMethods = require('./lib/method/index');
const compileAuthorizers = require('./lib/authorizers');
const compileDeployment = require('./lib/deployment');
const compilePermissions = require('./lib/permissions');
const compileStage = require('./lib/stage');
const getMethodAuthorization = require('./lib/method/authorization');
const getMethodIntegration = require('./lib/method/integration');
const getMethodResponses = require('./lib/method/responses');
const checkForBreakingChanges = require('./lib/checkForBreakingChanges');
class AwsCompileApigEvents {
constructor(serverless, options) {
@ -39,9 +41,11 @@ class AwsCompileApigEvents {
compileAuthorizers,
compileDeployment,
compilePermissions,
compileStage,
getMethodAuthorization,
getMethodIntegration,
getMethodResponses
getMethodResponses,
checkForBreakingChanges
);
this.hooks = {
@ -62,7 +66,9 @@ class AwsCompileApigEvents {
.then(this.compileApiKeys)
.then(this.compileUsagePlan)
.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

View File

@ -41,6 +41,8 @@ describe('AwsCompileApigEvents', () => {
let compileDeploymentStub;
let compileUsagePlanStub;
let compilePermissionsStub;
let compileStageStub;
let checkForBreakingChangesStub;
let disassociateUsagePlanStub;
beforeEach(() => {
@ -56,6 +58,10 @@ describe('AwsCompileApigEvents', () => {
.stub(awsCompileApigEvents, 'compileUsagePlan').resolves();
compilePermissionsStub = sinon
.stub(awsCompileApigEvents, 'compilePermissions').resolves();
compileStageStub = sinon
.stub(awsCompileApigEvents, 'compileStage').resolves();
checkForBreakingChangesStub = sinon
.stub(awsCompileApigEvents, 'checkForBreakingChanges').resolves();
disassociateUsagePlanStub = sinon
.stub(disassociateUsagePlan, 'disassociateUsagePlan').resolves();
});
@ -67,6 +73,8 @@ describe('AwsCompileApigEvents', () => {
awsCompileApigEvents.compileDeployment.restore();
awsCompileApigEvents.compileUsagePlan.restore();
awsCompileApigEvents.compilePermissions.restore();
awsCompileApigEvents.compileStage.restore();
awsCompileApigEvents.checkForBreakingChanges.restore();
disassociateUsagePlan.disassociateUsagePlan.restore();
});
@ -100,6 +108,8 @@ describe('AwsCompileApigEvents', () => {
expect(compileDeploymentStub.calledAfter(compileMethodsStub)).to.be.equal(true);
expect(compileUsagePlanStub.calledAfter(compileDeploymentStub)).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();
});

View File

@ -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;
});
},
};

View File

@ -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/);
});
});

View File

@ -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();
},
};

View File

@ -0,0 +1,90 @@
'use strict';
const expect = require('chai').expect;
const AwsCompileApigEvents = require('../index');
const Serverless = require('../../../../../../../Serverless');
const AwsProvider = require('../../../../../provider/awsProvider');
describe('#compileStage()', () => {
let serverless;
let provider;
let awsCompileApigEvents;
let stage;
let stageLogicalId;
beforeEach(() => {
const options = {
stage: 'dev',
region: 'us-east-1',
};
serverless = new Serverless();
provider = new AwsProvider(serverless, options);
serverless.setProvider('aws', provider);
serverless.service.provider.compiledCloudFormationTemplate = {
Resources: {},
Outputs: {},
};
awsCompileApigEvents = new AwsCompileApigEvents(serverless, options);
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
awsCompileApigEvents.apiGatewayDeploymentLogicalId = 'ApiGatewayDeploymentTest';
awsCompileApigEvents.provider = provider;
stage = awsCompileApigEvents.provider.getStage();
stageLogicalId = awsCompileApigEvents.provider.naming
.getStageLogicalId();
// setting up AWS X-Ray tracing
awsCompileApigEvents.serverless.service.provider.tracing = {
apiGateway: true,
};
// mocking the result of a Deployment resource since we remove the stage name
// when using the Stage resource
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources[awsCompileApigEvents.apiGatewayDeploymentLogicalId] = {
Properties: {
StageName: stage,
},
};
});
it('should create a dedicated stage resource if tracing is configured', () => awsCompileApigEvents
.compileStage().then(() => {
const resources = awsCompileApigEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources;
expect(resources[stageLogicalId]).to.deep.equal({
Type: 'AWS::ApiGateway::Stage',
Properties: {
RestApiId: {
Ref: awsCompileApigEvents.apiGatewayRestApiLogicalId,
},
DeploymentId: {
Ref: awsCompileApigEvents.apiGatewayDeploymentLogicalId,
},
StageName: 'dev',
TracingEnabled: true,
},
});
expect(resources[awsCompileApigEvents.apiGatewayDeploymentLogicalId]).to.deep.equal({
Properties: {},
});
})
);
it('should NOT create a dedicated stage resource if tracing is not enabled', () => {
awsCompileApigEvents.serverless.service.provider.tracing = {};
return awsCompileApigEvents.compileStage().then(() => {
const resources = awsCompileApigEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources;
// eslint-disable-next-line
expect(resources[stageLogicalId]).not.to.exist;
expect(resources[awsCompileApigEvents.apiGatewayDeploymentLogicalId]).to.deep.equal({
Properties: {
StageName: stage,
},
});
});
});
});