├── .gitignore ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-plugin-sns", 3 | "version": "0.5.13", 4 | "engines": { 5 | "node": ">=4.0" 6 | }, 7 | "description": "Serverless SNS Plugin.", 8 | "author": "https://github.com/martinlindenberg/", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/martinlindenberg/serverless-plugin-sns.git" 13 | }, 14 | "keywords": [ 15 | "serverless plugin sns", 16 | "serverless framework plugin", 17 | "serverless applications", 18 | "serverless plugins", 19 | "api gateway", 20 | "lambda", 21 | "sns", 22 | "aws", 23 | "aws lambda", 24 | "amazon", 25 | "amazon web services" 26 | ], 27 | "main": "index.js", 28 | "bin": {}, 29 | "scripts": {}, 30 | "devDependencies": {}, 31 | "dependencies": { 32 | "aws-sdk": "^2.2.33", 33 | "bluebird": "^3.0.6" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/martinlindenberg/serverless-plugin-sns/issues" 37 | }, 38 | "homepage": "https://github.com/martinlindenberg/serverless-plugin-sns#readme" 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Serverless Plugin SNS 2 | ===================== 3 | 4 | [![NPM](https://nodei.co/npm/serverless-plugin-sns.png?downloads=true)](https://nodei.co/npm/serverless-plugin-sns/) 5 | 6 | This plugin easily subscribes your lambda functions to SNS notifications. If the required SNS-Topics don't exist, they will be created automatically during the deployment. 7 | 8 | *Note*: This plugin supports Serverless 0.5.* (please see previous versions for older sls versions) 9 | 10 | 11 | ### Installation 12 | 13 | - make sure that aws and serverless are installed 14 | - @see http://docs.aws.amazon.com/cli/latest/userguide/installing.html 15 | - @see http://www.serverless.com/ 16 | 17 | - install this plugin to your projects node_modules folder 18 | 19 | ``` 20 | cd projectfolder 21 | npm install serverless-plugin-sns 22 | ``` 23 | 24 | - add the plugin to your s-project.json file 25 | 26 | ``` 27 | "plugins": [ 28 | "serverless-plugin-sns" 29 | ] 30 | ``` 31 | 32 | ### Run the Plugin 33 | 34 | - the plugin uses a hook that is called after each deployment of a function 35 | - you only have to deploy your function as usual `sls function deploy` 36 | - add the following attribute to the s-function.json in your functions folder 37 | 38 | ``` 39 | ... 40 | "sns": { 41 | "topic": "your-dev-sns-topic" 42 | }, 43 | ... 44 | ``` 45 | 46 | - the topic will be created automatically, if not yet done 47 | - topicnames can use the following dynamic template-names: 48 | 49 | ``` 50 | ${project} 51 | ${stage} 52 | ${functionName} 53 | 54 | example: 55 | "sns": { 56 | "topic": "${project}-sns" 57 | }, 58 | ``` 59 | 60 | ### Subscribe a lambda to multiple SNS Topics 61 | 62 | - put an array of topics to the sns attribute 63 | 64 | ``` 65 | ... 66 | "sns": [ 67 | {"topic": "your-dev-sns-topic1"}, 68 | {"topic": "your-dev-sns-topic2"} 69 | ] 70 | ... 71 | ``` 72 | 73 | ### Next Steps 74 | 75 | - create notifications that push events to sns topics 76 | - for example: cloudwatch alerts can submit notifications to sns topics 77 | - @see https://github.com/martinlindenberg/serverless-plugin-alerting :) 78 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(S) { 4 | 5 | const AWS = require('aws-sdk'), 6 | SCli = require(S.getServerlessPath('utils/cli')), 7 | BbPromise = require('bluebird'); // Serverless uses Bluebird Promises and we recommend you do to because they provide more than your average Promise :) 8 | 9 | class ServerlessPluginSNS extends S.classes.Plugin { 10 | constructor(S) { 11 | super(S); 12 | } 13 | 14 | static getName() { 15 | return 'com.serverless.' + ServerlessPluginSNS.name; 16 | } 17 | 18 | registerHooks() { 19 | 20 | S.addHook(this._addSNSAfterDeploy.bind(this), { 21 | action: 'functionDeploy', 22 | event: 'post' 23 | }); 24 | S.addHook(this._addSNSAfterDeploy.bind(this), { 25 | action: 'dashDeploy', 26 | event: 'post' 27 | }); 28 | 29 | return BbPromise.resolve(); 30 | } 31 | 32 | /** 33 | * adds alerts after the deployment of a function 34 | * 35 | * @param object evt 36 | * 37 | * @return promise 38 | */ 39 | _addSNSAfterDeploy(evt) { 40 | let _this = this; 41 | 42 | return new BbPromise(function(resolve, reject) { 43 | for(var region in evt.data.deployed) { 44 | _this._manageSNS(evt, region); 45 | } 46 | 47 | return resolve(evt); 48 | }); 49 | } 50 | 51 | /** 52 | * Handles the Creation of an alert and the required topics 53 | * 54 | * @param object evt Event 55 | * @param string region 56 | * 57 | * @return promise 58 | */ 59 | _manageSNS (evt, region) { 60 | let _this = this; 61 | 62 | _this.stage = evt.options.stage; 63 | _this._initAws(region) 64 | .then(function() { 65 | 66 | _this.functionSNSSettings = _this._getFunctionsSNSSettings(evt, region); 67 | 68 | // no sns.json found 69 | if (_this.functionSNSSettings.length == 0) { 70 | return; 71 | } 72 | 73 | return _this._manageTopics(_this.functionSNSSettings) 74 | }) 75 | .then(function(){ 76 | let _this = this; 77 | _this._bindFunctions(_this.functionSNSSettings); 78 | }.bind(_this)) 79 | .catch(function(e){ 80 | SCli.log('error in manage topics', e) 81 | }); 82 | } 83 | 84 | /** 85 | * Binds functions to topics 86 | */ 87 | _bindFunctions (settings) { 88 | let _this = this; 89 | 90 | for (var i in settings) { 91 | var functionName = settings[i].deployed.Arn; 92 | var functionArn = settings[i].deployed.Arn; 93 | functionArn = functionArn.split(':'); 94 | functionArn.pop(); 95 | functionArn = functionArn.join(':'); 96 | 97 | var sns = _this._getTopicNamesBySettings(settings[i]); 98 | 99 | for (var j in sns) { 100 | var topicArn = _this._getTopicArnByFunctionArn(functionArn, sns[j]); 101 | 102 | SCli.log('binding function ' + settings[i].deployed.functionName + ' to topic ' + sns[j]); 103 | _this.sns.subscribeAsync({ 104 | 'Protocol': 'lambda', 105 | 'TopicArn': topicArn, 106 | 'Endpoint': functionArn + ":" + _this.stage, 107 | }) 108 | .then(function(result){ 109 | var topicName = sns[j]; 110 | topicName = topicName.replace(/:/g, '_'); 111 | var statementId = settings[i].deployed.functionName + '_' + topicName; 112 | return new BbPromise(function(resolve, reject) { 113 | _this.lambda.removePermission( 114 | { 115 | FunctionName: functionName, 116 | StatementId: statementId 117 | }, 118 | function (err, data) { 119 | if (err && err.code != 'ResourceNotFoundException') { 120 | reject(err); 121 | } else { 122 | _this.lambda.addPermission({ 123 | FunctionName: functionName, 124 | StatementId: statementId, 125 | Action: 'lambda:InvokeFunction', 126 | Principal: 'sns.amazonaws.com', 127 | SourceArn: topicArn, 128 | }, function callback(err, data) { 129 | if (err) { 130 | reject(err); 131 | } else { 132 | resolve(data); 133 | } 134 | }); 135 | } 136 | } 137 | ); 138 | }); 139 | }) 140 | .then(function(result) { 141 | SCli.log('done'); 142 | }); 143 | } 144 | } 145 | } 146 | 147 | /** 148 | * returns the topic that needs to be created replaces keys 149 | * 150 | * @param object settings 151 | * 152 | * @return array 153 | */ 154 | _getTopicNamesBySettings (settings) { 155 | var replacements = []; 156 | replacements['project'] = S.getProject().name; 157 | replacements['stage'] = this.stage; 158 | replacements['functionName'] = settings.deployed.functionName; 159 | 160 | var topics = []; 161 | 162 | if (Object.prototype.toString.call(settings.sns) === '[object Array]') { 163 | for (var i in settings.sns) { 164 | topics[settings.sns[i].topic] = settings.sns[i].topic 165 | } 166 | } else { 167 | topics = [settings.sns.topic]; 168 | } 169 | 170 | for (var i in replacements) { 171 | for (var j in topics) { 172 | topics[j] = topics[j].replace('${' + i + '}', replacements[i]); 173 | } 174 | } 175 | 176 | return topics; 177 | } 178 | 179 | _getTopicArnByFunctionArn(functionArn, topicName){ 180 | if (topicName.indexOf('arn:aws:sns') >= 0) { 181 | return topicName; 182 | } 183 | 184 | var start = functionArn.split(':function:'); 185 | var topicArn = start[0] + ':' + topicName; 186 | 187 | topicArn = topicArn.replace(':lambda:', ':sns:'); 188 | return topicArn; 189 | } 190 | 191 | /** 192 | * collects the topics and calls create topcs 193 | * 194 | * @param array settings 195 | * 196 | * @return BpPromise 197 | */ 198 | _manageTopics(settings) { 199 | var _this = this; 200 | 201 | var topics = []; 202 | var topicslist = []; 203 | for (var i in settings) { 204 | topicslist = _this._getTopicNamesBySettings(settings[i]); 205 | 206 | for (var j in topicslist) { 207 | topics[topicslist[j]] = topicslist[j] 208 | } 209 | } 210 | 211 | return _this._createTopics(topics); 212 | } 213 | 214 | /** 215 | * creates topics if not yet done 216 | * 217 | * @param array topics 218 | * 219 | * @return BpPromise 220 | */ 221 | _createTopics (topics) { 222 | var _this = this; 223 | _this.topics = topics; 224 | 225 | return _this.sns.listTopicsAsync() 226 | .then(function(topicListResult){ 227 | var _this = this; 228 | //create fast checkable topiclist['topic1'] = 'topic1' 229 | var topicList = []; 230 | if (topicListResult['Topics']) { 231 | for (var i in topicListResult.Topics) { 232 | var arnParts = topicListResult.Topics[i].TopicArn.split(':') 233 | var topicName = arnParts[arnParts.length - 1]; 234 | topicList[topicName] = topicName; 235 | } 236 | } 237 | 238 | var topicCreatePromises = []; 239 | 240 | for (var i in this.topics) { 241 | if (i.indexOf('arn:aws:sns') >= 0) { 242 | continue; 243 | } 244 | 245 | if (!topicList[i]) { 246 | SCli.log('topic ' + i + ' does not exist. it will be created now'); 247 | topicCreatePromises.push( 248 | _this.sns.createTopicAsync({ 249 | 'Name': i 250 | }) 251 | .then(function(){ 252 | SCli.log('topic created'); 253 | }) 254 | .catch(function(e){ 255 | SCli.log('error during creation of the topic !', e) 256 | }) 257 | ); 258 | } else { 259 | SCli.log('topic ' + i + ' exists.'); 260 | } 261 | } 262 | 263 | if (topicCreatePromises.length > 0) { 264 | return BbPromise.all(topicCreatePromises); 265 | } else { 266 | return BbPromise.resolve(); 267 | } 268 | }.bind(this)); 269 | } 270 | 271 | /** 272 | * initializes aws 273 | * 274 | * @param string region 275 | * 276 | * @return void 277 | */ 278 | _initAws (region) { 279 | let _this = this, 280 | credentials = S.getProvider('aws').getCredentials(_this.stage, region); 281 | 282 | return BbPromise.resolve() 283 | .then(function() { 284 | // Will handle this if it's a promise or an object 285 | return credentials; 286 | }) 287 | .then(function(creds) { 288 | _this.sns = new AWS.SNS({ 289 | region: region, 290 | accessKeyId: creds.accessKeyId, 291 | secretAccessKey: creds.secretAccessKey, 292 | sessionToken: creds.sessionToken 293 | }); 294 | 295 | BbPromise.promisifyAll(_this.sns); 296 | 297 | _this.lambda = new AWS.Lambda({ 298 | region: region, 299 | accessKeyId: creds.accessKeyId, 300 | secretAccessKey: creds.secretAccessKey, 301 | sessionToken: creds.sessionToken 302 | }); 303 | }) 304 | } 305 | 306 | 307 | /** 308 | * parses the sns-function.json file and returns the data 309 | * 310 | * @param object evt 311 | * @param string region 312 | * 313 | * @return array 314 | */ 315 | _getFunctionsSNSSettings(evt, region){ 316 | let _this = this; 317 | var settings = []; 318 | for (var deployedIndex in evt.data.deployed[region]) { 319 | let deployed = evt.data.deployed[region][deployedIndex], 320 | functionName = deployed['functionName'], 321 | config = S.getProject()['functions'][functionName]; 322 | 323 | if (!config.sns) { 324 | continue; 325 | } 326 | 327 | settings.push({ 328 | "deployed": deployed, 329 | "sns": config.sns 330 | }); 331 | } 332 | 333 | return settings; 334 | } 335 | } 336 | 337 | return ServerlessPluginSNS; 338 | }; 339 | --------------------------------------------------------------------------------