├── .gitignore ├── LICENSE ├── README.md ├── env.yml.example ├── functions ├── snsConsume.js └── snsPublish.js ├── lib └── jsonResponse.js ├── mocha.opts ├── package-lock.json ├── package.json ├── scripts └── sns-publish.js ├── serverless.yml ├── test ├── bootstrap.test.js ├── integration │ └── post-snsPublish.test.js └── support │ └── getSlsOfflinePort.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # vim 40 | .*.sw* 41 | Session.vim 42 | 43 | # Serverless 44 | .webpack 45 | .serverless 46 | 47 | # env 48 | env.yml 49 | .env 50 | 51 | # Jetbrains IDEs 52 | .idea 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anomaly Innovations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Lambda SNS Example 2 | 3 | Example illustrating the flow: 4 | 5 | Lambda (publisher) => SNS => Lambda (consumer) 6 | 7 | ## Setup 8 | 9 | - Install Node 8.10 (latest runtime supported by AWS Lambda) 10 | 11 | - Install serverless (tested against serverless v1.28.0) 12 | ```` 13 | $ npm i -g serverless 14 | ```` 15 | - Install node modules 16 | ```` 17 | $ npm i 18 | ```` 19 | - Initialize env variables file 20 | ```` 21 | $ touch env.yml 22 | ```` 23 | - Run tests 24 | ```` 25 | $ npm test 26 | ```` -------------------------------------------------------------------------------- /env.yml.example: -------------------------------------------------------------------------------- 1 | # HOW TO USE: 2 | # 1 Add cross-stage environment variables to the Globals section 3 | # 2 Add environment variables for the various stages here 4 | # 3 Rename this file to env.yml and uncomment it's usage in serverless.yml. 5 | # 4 Make sure to not commit this file. 6 | 7 | Globals: &globals 8 | foo: BAR 9 | 10 | test: 11 | <<: *globals 12 | MY_API_KEY: TEST_123 13 | 14 | dev: 15 | <<: *globals 16 | MY_API_KEY: DEV_123 17 | 18 | prod: 19 | <<: *globals 20 | MY_API_KEY: TOP_123 -------------------------------------------------------------------------------- /functions/snsConsume.js: -------------------------------------------------------------------------------- 1 | module.exports.handler = async (event, context) => { 2 | let message = event.Records[0].Sns.Message; 3 | 4 | console.log("Received MESSAGE: " + message); 5 | 6 | return message; 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /functions/snsPublish.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); // must be npm installed to use 2 | 3 | const jsonResponse = require("../lib/jsonResponse"); 4 | 5 | module.exports.handler = async (event, context) => { 6 | let snsOpts = { 7 | region: "us-east-1", 8 | }; 9 | 10 | if (process.env.IS_OFFLINE) { 11 | snsOpts.endpoint = "http://127.0.0.1:4002"; 12 | } 13 | 14 | let sns = new AWS.SNS(snsOpts); 15 | 16 | let messageData = { 17 | Message: event.body, 18 | TopicArn: process.env.mySnsTopicArn, 19 | }; 20 | 21 | console.log("PUBLISHING MESSAGE TO SNS:", messageData); 22 | 23 | try { 24 | await sns.publish(messageData).promise(); 25 | console.log("PUBLISHED MESSAGE TO SNS:", messageData); 26 | return jsonResponse.ok({}); 27 | } 28 | catch (err) { 29 | console.log(err); 30 | return jsonResponse.error(err); 31 | } 32 | 33 | }; -------------------------------------------------------------------------------- /lib/jsonResponse.js: -------------------------------------------------------------------------------- 1 | function ok(data) { 2 | return json(200, data); 3 | } 4 | 5 | function error(data) { 6 | return json(400, data); 7 | } 8 | 9 | function serverError(data) { 10 | return json(500, data); 11 | } 12 | 13 | function json(statusCode, data, headers = {}) { 14 | return ({ 15 | statusCode, 16 | headers: Object.assign({ 17 | 'Access-Control-Allow-Origin': '*', 18 | 'Access-Control-Allow-Credentials': true 19 | }, headers), 20 | body: JSON.stringify(data), 21 | }); 22 | } 23 | 24 | module.exports = { 25 | ok, 26 | error, 27 | serverError, 28 | json 29 | }; -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 5000 2 | --reporter list 3 | --ui bdd 4 | --exit 5 | test/bootstrap.test.js 6 | test/**/*.test.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-lambda-sns-example", 3 | "version": "0.5.0", 4 | "description": "Serverless Lambda SNS example", 5 | "main": "", 6 | "engines": { 7 | "node": "8.10" 8 | }, 9 | "scripts": { 10 | "test": "NODE_ENV=test PORT=9100 ./node_modules/.bin/mocha --opts mocha.opts", 11 | "offline": "sls offline start" 12 | }, 13 | "author": "Adil H", 14 | "license": "MIT", 15 | "repository": {}, 16 | "devDependencies": { 17 | "aws-sdk": "^2.275.1", 18 | "chai": "^4.1.2", 19 | "mocha": "^5.2.0", 20 | "serverless-offline": "^3.25.6", 21 | "serverless-webpack": "^5.2.0", 22 | "serverless-offline-sns": "^0.46.0", 23 | "sinon": "^6.1.3", 24 | "supertest": "^3.1.0", 25 | "webpack": "^4.16.0", 26 | "webpack-node-externals": "^1.7.2" 27 | }, 28 | "dependencies": { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/sns-publish.js: -------------------------------------------------------------------------------- 1 | let AWS = require("aws-sdk"); // must be npm installed to use 2 | let sns = new AWS.SNS({ 3 | endpoint: "http://127.0.0.1:4002", 4 | region: "us-east-1", 5 | }); 6 | sns.publish({ 7 | Message: "hello!", 8 | MessageStructure: "json", 9 | TopicArn: "arn:aws:sns:us-east-1:123456789012:test-topic", 10 | }, () => { 11 | console.log("ping"); 12 | }); -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | # NOTE: update this with your service name 2 | service: serverless-lambda-sns-example 3 | 4 | plugins: 5 | - serverless-webpack 6 | - serverless-offline 7 | - serverless-offline-sns 8 | 9 | # serverless-webpack configuration 10 | # Enable auto-packing of external modules 11 | custom: 12 | webpack: 13 | includeModules: 14 | forceExclude: 15 | - aws-sdk 16 | serverless-offline-sns: 17 | port: 4002 # a free port for the sns server to run on 18 | debug: true 19 | mySnsTopic: "${self:service}-${self:provider.stage}-sns-consume" 20 | mySnsTopicArn: 21 | local: 22 | "arn:aws:sns:us-east-1:123456789012:${self:custom.mySnsTopic}" 23 | dev: 24 | { "Fn::Join" : ["", ["arn:aws:sns:${self:provider.region}:", { "Ref" : "AWS::AccountId" }, ":${self:custom.mySnsTopic}" ] ] } 25 | prod: 26 | { "Fn::Join" : ["", ["arn:aws:sns:${self:provider.region}:", { "Ref" : "AWS::AccountId" }, ":${self:custom.mySnsTopic}" ] ] } 27 | 28 | provider: 29 | name: aws 30 | runtime: nodejs8.10 31 | stage: ${opt:stage,'dev'} 32 | region: ${opt:region, 'us-east-1'} 33 | environment: ${file(env.yml):${self:provider.stage}} 34 | iamRoleStatements: 35 | - Effect: Allow 36 | Action: 37 | - SNS:Publish 38 | Resource: { "Fn::Join" : ["", ["arn:aws:sns:${self:provider.region}:", { "Ref" : "AWS::AccountId" }, ":${self:custom.mySnsTopic}" ] ] } 39 | 40 | functions: 41 | snsConsume: 42 | handler: functions/snsConsume.handler 43 | events: 44 | - sns: ${self:custom.mySnsTopic} 45 | snsPublish: 46 | handler: functions/snsPublish.handler 47 | events: 48 | - http: 49 | path: snsPublish 50 | method: post 51 | cors: true 52 | environment: 53 | mySnsTopicArn: ${self:custom.mySnsTopicArn.${self:provider.stage}} 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /test/bootstrap.test.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | const getSlsOfflinePort = require('./support/getSlsOfflinePort'); 3 | 4 | let slsOfflineProcess; 5 | 6 | before(function (done) { 7 | // increase mocha timeout for this hook to allow sls offline to start 8 | this.timeout(30000); 9 | 10 | console.log("[Tests Bootstrap] Start"); 11 | 12 | startSlsOffline(function (err) { 13 | if (err) { 14 | return done(err); 15 | } 16 | 17 | console.log("[Tests Bootstrap] Done"); 18 | done(); 19 | }) 20 | }); 21 | 22 | after(function () { 23 | console.log("[Tests Teardown] Start"); 24 | 25 | stopSlsOffline(); 26 | 27 | console.log("[Tests Teardown] Done"); 28 | }); 29 | 30 | 31 | // Helper functions 32 | 33 | function startSlsOffline(done) { 34 | slsOfflineProcess = spawn("sls", ["offline", "start", "-s", "local", "--port", getSlsOfflinePort()]); 35 | 36 | console.log(`Serverless: Offline started with PID : ${slsOfflineProcess.pid}`); 37 | 38 | // allows checking output in test results 39 | global.slsOfflineProcess = slsOfflineProcess; 40 | 41 | slsOfflineProcess.stdout.on('data', (data) => { 42 | if (data.includes("Offline listening on")) { 43 | console.log(data.toString().trim()); 44 | 45 | // clean up 46 | slsOfflineProcess.stdout.removeAllListeners(); 47 | slsOfflineProcess.stderr.removeAllListeners(); 48 | 49 | done(); 50 | } 51 | }); 52 | 53 | slsOfflineProcess.stderr.on('data', (errData) => { 54 | console.log(`Error starting Serverless Offline:\n${errData}`); 55 | done(errData); 56 | }); 57 | } 58 | 59 | 60 | function stopSlsOffline() { 61 | slsOfflineProcess.kill(); 62 | console.log("Serverless Offline stopped"); 63 | } 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /test/integration/post-snsPublish.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const expect = require('chai').expect; 3 | const getSlsOfflinePort = require('../support/getSlsOfflinePort'); 4 | 5 | describe('postSnsPublish', function postSnsPublishTest() { 6 | 7 | it('ok', function it(done) { 8 | global.global.slsOfflineProcess.stdout.on('data', (data) => { 9 | if (data.includes("Received MESSAGE: {\"msg\":\"stub message\"}")) { 10 | done(); 11 | } 12 | }); 13 | 14 | request(`http://localhost:${getSlsOfflinePort()}`) 15 | .post(`/snsPublish`) 16 | .send({msg: "stub message"}) 17 | .expect(200) 18 | .end(function (error, result) { 19 | if (error) { 20 | return done(error); 21 | } 22 | }); 23 | }); 24 | 25 | }); -------------------------------------------------------------------------------- /test/support/getSlsOfflinePort.js: -------------------------------------------------------------------------------- 1 | function getSlsOfflinePort() { 2 | return process.env.PORT || "3005"; 3 | } 4 | 5 | module.exports = getSlsOfflinePort; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const slsw = require("serverless-webpack"); 2 | const nodeExternals = require("webpack-node-externals"); 3 | 4 | module.exports = { 5 | entry: slsw.lib.entries, 6 | target: "node", 7 | // Generate sourcemaps for proper error messages 8 | devtool: 'source-map', 9 | // Since 'aws-sdk' is not compatible with webpack, 10 | // we exclude all node dependencies 11 | externals: [nodeExternals()], 12 | mode: slsw.lib.webpack.isLocal ? "development" : "production", 13 | optimization: { 14 | // We do not want to minimize our code. 15 | minimize: false 16 | }, 17 | performance: { 18 | // Turn off size warnings for entry points 19 | hints: false 20 | }, 21 | 22 | module: { 23 | rules: [ 24 | 25 | ] 26 | } 27 | }; 28 | 29 | --------------------------------------------------------------------------------