mirror of
https://github.com/serverless/serverless.git
synced 2026-02-01 16:07:28 +00:00
Merge pull request #5692 from softprops/deployment-stage-description
Add AWS x-ray support for API Gateway
This commit is contained in:
commit
90a7adf8f6
@ -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
|
||||
```
|
||||
|
||||
@ -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> |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -254,6 +254,9 @@ module.exports = {
|
||||
getUsagePlanKeyLogicalId(usagePlanKeyNumber) {
|
||||
return `ApiGatewayUsagePlanKey${usagePlanKeyNumber}`;
|
||||
},
|
||||
getStageLogicalId() {
|
||||
return 'ApiGatewayStage';
|
||||
},
|
||||
|
||||
// S3
|
||||
getDeploymentBucketLogicalId() {
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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;
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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/);
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user