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

Stage configuration #59

Merged
merged 6 commits into from
Jun 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 96 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,89 @@ Lambda invocation. This will call the aliased function version.
Deployed stages have the alias stage variable set fixed, so a deployed alias stage is
hard-wired to the aliased Lambda versions.

### Stage configuration (NEW)

The alias plugin supports configuring the deployed API Gateway stages, exactly as
you can do it within the AWS APIG console, e.g. you can configure logging (with
or without data/request tracing), setup caching or throttling on your endpoints.

The configuration can be done on a service wide level, function level or method level
by adding an `aliasStage` object either to `provider`, `any function` or a `http event`
within a function in your _serverless.yml_. The configuration is applied hierarchically,
where the inner configurations overwrite the outer ones.

`HTTP Event -> FUNCTION -> SERVICE`

#### The aliasStage configuration object

All settings are optional, and if not specified will be set to the AWS stage defaults.

```
aliasStage:
cacheDataEncrypted: (Boolean)
cacheTtlInSeconds: (Integer)
cachingEnabled: (Boolean)
dataTraceEnabled: (Boolean) - Log full request/response bodies
loggingLevel: ("OFF", "INFO" or "ERROR")
metricsEnabled: (Boolean) - Enable detailed CW metrics
throttlingBurstLimit: (Integer)
throttlingRateLimit: (Number)
```

There are two further options that can only be specified on a service level and that
affect the whole stage:

```
aliasStage:
cacheClusterEnabled: (Boolean)
cacheClusterSize: (Integer)
```

For more information see the [AWS::APIGateway::Stage](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-stage.html) or [MethodSettings](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html) documentation
on the AWS website.

Sample serverless.yml (partial):

```
service: sls-test-project

provider:
...
# Enable detailed error logging on all endpoints
aliasStage:
loggingLevel: "ERROR"
dataTraceEnabled: true
...

functions:
myFunc1:
...
# myFunc1 should generally not log anything
aliasStage:
loggingLevel: "OFF"
dataTraceEnabled: false
events:
- http:
method: GET
path: /func1
- http:
method: POST
path: /func1/create
- http:
method: PATCH
path: /func1/update
# The update endpoint needs special settings
aliasStage:
loggingLevel: "INFO"
dataTraceEnabled: true
throttlingBurstLimit: 200
throttlingRateLimit: 100

myFunc2:
...
# Will inherit the global settings if nothing is set on function level
```

## Reference the current alias in your service

You can reference the currently deployed alias with `${self:provider.alias}` in
Expand Down Expand Up @@ -174,11 +257,11 @@ functions:
path: /func1
resources:
Resources:
myKinesis:
Type: AWS::Kinesis::Stream
Properties:
Name: my-kinesis
ShardCount: 1
myKinesis:
Type: AWS::Kinesis::Stream
Properties:
Name: my-kinesis
ShardCount: 1
```

When a function is deployed to an alias it will now also listen to the *my-kinesis*
Expand Down Expand Up @@ -214,11 +297,11 @@ functions:
path: /func1
resources:
Resources:
myKinesis${self:provider.alias}:
Type: AWS::Kinesis::Stream
Properties:
Name: my-kinesis-${self.provider.alias}
ShardCount: 1
myKinesis${self:provider.alias}:
Type: AWS::Kinesis::Stream
Properties:
Name: my-kinesis-${self.provider.alias}
ShardCount: 1
```

### Named streams
Expand Down Expand Up @@ -343,7 +426,7 @@ The plugin adds the following lifecycle events that can be hooked by other plugi
* alias:deploy:done

The Alias plugin is successfully finished. Hook this instead of 'after:deploy:deploy'
to make sure that your plugin gets triggered right after the alias plugin is done.
to make sure that your plugin gets triggered right after the alias plugin is done.

* alias:remove:removeStack

Expand All @@ -360,8 +443,8 @@ and _serverless.service.provider.deployedAliasTemplates[]_.

* The master alias for a stage could be protected by a separate stack policy that
only allows admin users to deploy or change it. The stage stack does not have
to be protected individually because the stack cross references prohibit changes
naturally. It might be possible to introduce some kind of per alias policy.
to be protected individually because the stack cross references prohibit changes
naturally. It might be possible to introduce some kind of per alias policy.

## Version history

Expand All @@ -381,19 +464,3 @@ and _serverless.service.provider.deployedAliasTemplates[]_.


* 1.0.0 Support "serverless logs" with aliases. First non-alpha!
* 0.5.1-alpha1 Use separate Lambda roles per alias
* 0.5.0-alpha1 Fixes a bug with deploying event sources introduced with 0.4.0
Use new event model introduced in SLS 1.12. Needs SLS 1.12 or greater from now on.
Add support for CW events.
Set SERVERLESS_ALIAS environment variable on deployed functions.
* 0.4.0-alpha1 APIG support fixed. Support external IAM roles. BREAKING.
* 0.3.4-alpha1 Bugfixes. IAM policy consolitaion. Show master alias information.
* 0.3.3-alpha1 Bugfixes. Allow manual resource overrides. Allow methods attached to APIG root resource.
* 0.3.2-alpha1 Allow initial project creation with activated alias plugin
* 0.3.1-alpha1 Support Serverless 1.6 again with upgrade to 1.7+
* 0.3.0-alpha1 Support lambda event subscriptions
* 0.2.1-alpha1 Alias remove command removes unused resources
* 0.2.0-alpha1 Support custom resources
* 0.1.2-alpha1 Integration with "serverless info"
* 0.1.1-alpha1 Full APIG support
* 0.1.0-alpha1 Lambda function alias support
120 changes: 102 additions & 18 deletions lib/stackops/apiGateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,103 @@ const _ = require('lodash');
const BbPromise = require('bluebird');
const utils = require('../utils');

const stageMethodConfigMappings = {
cacheDataEncrypted: { prop: 'CacheDataEncrypted', validate: _.isBoolean, default: false },
cacheTtlInSeconds: { prop: 'CacheTtlInSeconds', validate: _.isInteger },
cachingEnabled: { prop: 'CachingEnabled', validate: _.isBoolean, default: false },
dataTraceEnabled: { prop: 'DataTraceEnabled', validate: _.isBoolean, default: false },
loggingLevel: { prop: 'LoggingLevel', validate: value => _.includes([ 'OFF', 'INFO', 'ERROR' ], value), default: 'OFF' },
metricsEnabled: { prop: 'MetricsEnabled', validate: _.isBoolean, default: false },
throttlingBurstLimit: { prop: 'ThrottlingBurstLimit', validate: _.isInteger },
throttlingRateLimit: { prop: 'ThrottlingRateLimit', validate: _.isNumber }
};

/**
* Namespace for APIG processing internal functions
*/
const internal = {
/**
* Creates a stage resource and configures it depending on the project settings.
* @this The current instance of the alias plugin
* @param restApiRef {String} Stack reference to rest API id
* @param deploymentName {String} Current deployment.
* @returns {Object} - AWS::ApiGateway::Stage
*/
createStageResource(restApiRef, deploymentName) {
// Create stage resource
const stageResource = {
Type: 'AWS::ApiGateway::Stage',
Properties: {
StageName: this._alias,
DeploymentId: {
Ref: deploymentName
},
RestApiId: {
'Fn::ImportValue': restApiRef
},
Variables: {
SERVERLESS_ALIAS: this._alias,
SERVERLESS_STAGE: this._stage
}
},
DependsOn: [ deploymentName ]
};

// Set a reasonable description
const serviceName = _.get(this.serverless.service.getServiceObject() || {}, 'name');
stageResource.Properties.Description = `Alias stage '${this._alias}' for ${serviceName}`;

// Configure stage (service level)
const serviceLevelConfig = _.cloneDeep(_.get(this.serverless.service, 'provider.aliasStage', {}));
if (serviceLevelConfig.cacheClusterEnabled === true) {
stageResource.Properties.CacheClusterEnabled = true;
if (_.has(serviceLevelConfig, 'cacheClusterSize')) {
stageResource.Properties.CacheClusterSize = serviceLevelConfig.cacheClusterSize;
}
}
delete serviceLevelConfig.cacheClusterEnabled;
delete serviceLevelConfig.cacheClusterSize;

// Configure methods/functions
const methodSettings = [];
const functions = this.serverless.service.getAllFunctions();
_.forEach(functions, funcName => {
const func = this.serverless.service.getFunction(funcName);
const funcStageConfig = _.defaults({}, func.aliasStage, serviceLevelConfig);
const funcHttpEvents = _.compact(_.map(this.serverless.service.getAllEventsInFunction(funcName), event => event.http));

_.forEach(funcHttpEvents, httpEvent => {
const eventStageConfig = _.defaults({}, httpEvent.aliasStage, funcStageConfig);
if (!_.isEmpty(eventStageConfig)) {
const methodType = _.toUpper(httpEvent.method);
const methodSetting = {};
_.forOwn(eventStageConfig, (value, key) => {
if (!_.has(stageMethodConfigMappings, key)) {
throw new this.serverless.classes.Error(`Invalid stage config '${key}' at method '${methodType} /${httpEvent.path}'`);
} else if (!stageMethodConfigMappings[key].validate(value)) {
throw new this.serverless.classes.Error(`Invalid value for stage config '${key}: ${value}' at method '${methodType} /${httpEvent.path}'`);
}
if (!_.has(stageMethodConfigMappings[key], 'default') || stageMethodConfigMappings[key].default !== value) {
methodSetting[stageMethodConfigMappings[key].prop] = value;
}
});
if (!_.isEmpty(methodSetting)) {
methodSetting.HttpMethod = methodType;
methodSetting.ResourcePath = '/' + _.replace('/' + _.trimStart(httpEvent.path, '/'), /\//g, '~1');
methodSettings.push(methodSetting);
}
}
});
});

if (!_.isEmpty(methodSettings)) {
stageResource.Properties.MethodSettings = methodSettings;
}

return stageResource;
}
};

module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
const stackName = this._provider.naming.getStackName();
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
Expand Down Expand Up @@ -74,25 +171,9 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
delete stageStack.Resources[deploymentName];

// Create stage resource
const stageResource = {
Type: 'AWS::ApiGateway::Stage',
Properties: {
StageName: this._alias,
DeploymentId: {
Ref: deploymentName
},
RestApiId: {
'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`
},
Variables: {
SERVERLESS_ALIAS: this._alias,
SERVERLESS_STAGE: this._stage
}
},
DependsOn: [ deploymentName ]
};
this.options.verbose && this._serverless.cli.log('Configuring stage');
const stageResource = internal.createStageResource.call(this, `${stackName}-ApiGatewayRestApi`, deploymentName);
aliasResources.push({ ApiGatewayStage: stageResource });

}

// Fetch lambda permissions, methods and resources. These have to be updated later to allow the aliased functions.
Expand Down Expand Up @@ -207,3 +288,6 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac

return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
};

// Exports to make internal functions available for unit tests
module.exports.internal = internal;
Loading