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

Transform SNS elements and move them to the alias stack. #47

Merged
merged 4 commits into from
May 29, 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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,23 @@ automatically.
Event subscriptions that are defined for a lambda function will be deployed per
alias, i.e. the event will trigger the correct deployed aliased function.

### SNS

Subscriptions to SNS topics can be implicitly defined by adding an `sns` event to
any existing lambda function definition. Serverless will create the topic for you
and add a subscription to the deployed function.

With the alias plugin the subscription will be per alias. Additionally the created
topic is renamed and the alias name is added (e.g. myTopic-myAlias). This is done
because SNS topics are independent per stage. Imagine you want to introduce a new
topic or change the data/payload format of an existing one. Just attaching different
aliases to one central topic would eventually break the system, as functions from
different stages will receive the new data format. The topic-per-alias approach
effectively solves the problem.

If you want to refer to the topic programmatically, you just can add `-${process.env.SERVERLESS_ALIAS}`
to the base topic name.

### Use with global resources

Event subscriptions can reference resources that are available throughout all
Expand Down
6 changes: 6 additions & 0 deletions lib/aliasRestructureStack.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const userResources = require('./stackops/userResources');
const lambdaRole = require('./stackops/lambdaRole');
const events = require('./stackops/events');
const cwEvents = require('./stackops/cwEvents');
const snsEvents = require('./stackops/snsEvents');

module.exports = {

Expand Down Expand Up @@ -50,6 +51,10 @@ module.exports = {
return cwEvents.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate);
},

aliasHandleSNSEvents(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
return snsEvents.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate);
},

aliasFinalize(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
Expand Down Expand Up @@ -84,6 +89,7 @@ module.exports = {
.spread(this.aliasHandleApiGateway)
.spread(this.aliasHandleEvents)
.spread(this.aliasHandleCWEvents)
.spread(this.aliasHandleSNSEvents)
.spread(this.aliasFinalize)
.then(() => BbPromise.resolve());
}
Expand Down
79 changes: 79 additions & 0 deletions lib/stackops/snsEvents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict';

/**
* Handle SNS Lambda subscriptions.
*/

const _ = require('lodash');
const BbPromise = require('bluebird');
const utils = require('../utils');

module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;

this.options.verbose && this._serverless.cli.log('Processing SNS Lambda subscriptions');

const aliasResources = [];

const aliases = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Alias' ]));
const versions = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Version' ]));

// Add alias name to topics to disambiguate behavior
const snsTopics =
_.assign({},
_.pickBy(stageStack.Resources, [ 'Type', 'AWS::SNS::Topic' ]));

_.forOwn(snsTopics, (topic, name) => {
topic.DependsOn = topic.DependsOn || [];
// Remap lambda subscriptions
const lambdaSubscriptions = _.pickBy(topic.Properties.Subscription, ['Protocol', 'lambda']);
_.forOwn(lambdaSubscriptions, subscription => {
const functionNameRef = utils.findAllReferences(_.get(subscription, 'Endpoint'));
const functionName = _.replace(_.get(functionNameRef, '[0].ref', ''), /LambdaFunction$/, '');
const versionName = _.find(_.keys(versions), version => _.startsWith(version, functionName));
const aliasName = _.find(_.keys(aliases), alias => _.startsWith(alias, functionName));

subscription.Endpoint = { Ref: aliasName };

// Add dependency on function version
topic.DependsOn.push(versionName);
topic.DependsOn.push(aliasName);
});

topic.Properties.TopicName = `${topic.Properties.TopicName}-${this._alias}`;

delete stageStack.Resources[name];
});

// Fetch lambda permissions. These have to be updated later to allow the aliased functions.
const snsLambdaPermissions =
_.assign({},
_.pickBy(_.pickBy(stageStack.Resources, [ 'Type', 'AWS::Lambda::Permission' ]),
[ 'Properties.Principal', 'sns.amazonaws.com' ]));

// Adjust permission to reference the function aliases
_.forOwn(snsLambdaPermissions, (permission, name) => {
const functionName = _.replace(name, /LambdaPermission.*$/, '');
const versionName = _.find(_.keys(versions), version => _.startsWith(version, functionName));
const aliasName = _.find(_.keys(aliases), alias => _.startsWith(alias, functionName));

// Adjust references and alias permissions
permission.Properties.FunctionName = { Ref: aliasName };
const sourceArn = _.get(permission.Properties, 'SourceArn.Fn::Join[1]', []);
sourceArn.push(`-${this._alias}`);

// Add dependency on function version
permission.DependsOn = [ versionName, aliasName ];

delete stageStack.Resources[name];
});

// Add all alias stack owned resources
aliasResources.push(snsTopics);
aliasResources.push(snsLambdaPermissions);

_.forEach(aliasResources, resource => _.assign(aliasStack.Resources, resource));

return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
};
2 changes: 2 additions & 0 deletions test/aliasRestructureStack.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ describe('aliasRestructureStack', () => {
const aliasHandleApiGatewaySpy = sandbox.spy(awsAlias, 'aliasHandleApiGateway');
const aliasHandleEventsSpy = sandbox.spy(awsAlias, 'aliasHandleEvents');
const aliasHandleCWEventsSpy = sandbox.spy(awsAlias, 'aliasHandleCWEvents');
const aliasHandleSNSEventsSpy = sandbox.spy(awsAlias, 'aliasHandleSNSEvents');
const aliasFinalizeSpy = sandbox.spy(awsAlias, 'aliasFinalize');

const currentTemplate = require('./data/sls-stack-2.json');
Expand All @@ -107,6 +108,7 @@ describe('aliasRestructureStack', () => {
expect(aliasHandleApiGatewaySpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate),
expect(aliasHandleEventsSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate),
expect(aliasHandleCWEventsSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate),
expect(aliasHandleSNSEventsSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate),
expect(aliasFinalizeSpy).to.have.been.calledWithExactly(currentTemplate, [ aliasTemplate ], currentAliasStackTemplate),
]));
});
Expand Down
Loading