├── .gitignore ├── daemon ├── Dockerfile ├── lambda │ ├── package.json │ └── lambda.js ├── .jshintrc ├── package.json └── lib │ ├── prefer.js │ ├── context.js │ └── lambdad.js ├── example ├── Dockerfile ├── lambda.js └── lambda.js.example └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | NODE_MODULES 3 | -------------------------------------------------------------------------------- /daemon/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:onbuild 2 | 3 | EXPOSE 8080 4 | 5 | -------------------------------------------------------------------------------- /daemon/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda", 3 | "main": "lambda.js" 4 | } 5 | -------------------------------------------------------------------------------- /example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lambda 2 | COPY lambda.js /usr/src/app/lambda/ 3 | EXPOSE 8080 4 | 5 | -------------------------------------------------------------------------------- /example/lambda.js: -------------------------------------------------------------------------------- 1 | exports.handler = (input, context) => { 2 | setImmediate(()=>context.succeed({'a':'b'})); 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /daemon/lambda/lambda.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // var BasicStrategy = require('passport-http').BasicStrategy; 4 | // 5 | // exports.auth = new BasicStrategy( 6 | // function(username, password, done) { 7 | // if (username) { 8 | // done(null,{username:username}); 9 | // } else { 10 | // done('error'); 11 | // } 12 | // } 13 | // ); 14 | 15 | exports.handler = function(input, context) { 16 | context.succeed({'Hello':'World'}); 17 | }; 18 | -------------------------------------------------------------------------------- /example/lambda.js.example: -------------------------------------------------------------------------------- 1 | 2 | //// You can use Passport Strategies for authentication... 3 | // 4 | // var BasicStrategy = require('passport-http').BasicStrategy; 5 | // 6 | // exports.auth = new BasicStrategy( 7 | // function(username, password, done) { 8 | // if (username) { 9 | // done(null,{username:username}); 10 | // } else { 11 | // done('error'); 12 | // } 13 | // } 14 | // ); 15 | 16 | exports.handler = (input, context) => { 17 | setImmediate(()=>context.succeed({'a':'b'})); 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /daemon/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "freeze": true, 3 | "immed": true, 4 | "indent": 2, 5 | "latedef": true, 6 | "newcap": true, 7 | "noempty": true, 8 | "nonbsp": true, 9 | "nonew": true, 10 | "quotmark": "single", 11 | "undef": true, 12 | "unused": "lenient", 13 | "maxlen": 80, 14 | "es5": true, 15 | "esnext": true, 16 | "evil": false, 17 | "expr": false, 18 | "funcscope": true, 19 | "globalstrict": true, 20 | "loopfunc": false, 21 | "shadow": false, 22 | "validthis": true, 23 | "node": true, 24 | "globals": { 25 | "describe": true, 26 | "it": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /daemon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-daemon", 3 | "version": "0.0.2", 4 | "description": "", 5 | "main": "lib/lambdad.js", 6 | "scripts": { 7 | "start": "node lib/lambdad.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "James M Snell ", 11 | "license": "Apache v2.0", 12 | "dependencies": { 13 | "body-parser": "^1.14.0", 14 | "express": "^4.13.3", 15 | "multer": "^1.0.3", 16 | "node-uuid": "^1.4.3", 17 | "passport-anonymous": "^1.0.1", 18 | "passport-http": "^0.3.0" 19 | }, 20 | "private": true 21 | } 22 | -------------------------------------------------------------------------------- /daemon/lib/prefer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function parser(input) { 4 | var collector = []; 5 | var current = '', quoted = false; 6 | for (var n = 0, l = input.length; n < l; n++) { 7 | if (input[n] === ',' && !quoted) { 8 | if (current.trim().length > 0) 9 | collector.push(current.trim()); 10 | current = ''; 11 | } else if (input[n] === '"' && !quoted) { 12 | //current += input[n]; 13 | quoted = true; 14 | } else if (input[n] === '"' && quoted) { 15 | //current += input[n]; 16 | quoted = false; 17 | } else { 18 | current += input[n]; 19 | } 20 | } 21 | if (current.trim().length > 0) 22 | collector.push(current.trim()); 23 | return collector; 24 | } 25 | 26 | function prefer(req, res, next) { 27 | var pref = req.get('Prefer'); 28 | req.prefer = {}; 29 | if (pref !== undefined) { 30 | var tokens = parser(pref); 31 | tokens.forEach(function(item) { 32 | var split = item.split('=',2); 33 | var value = split[1] || true; 34 | req.prefer[split[0].toLowerCase()] = value; 35 | }); 36 | } 37 | next(); 38 | } 39 | 40 | module.exports = prefer; 41 | -------------------------------------------------------------------------------- /daemon/lib/context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var uuid = require('node-uuid'); 5 | var EventEmitter = require('events').EventEmitter; 6 | 7 | var _req = Symbol('req'); 8 | var _timeout = Symbol('timeout'); 9 | 10 | function constant(target, name, value) { 11 | Object.defineProperty(target, name, { 12 | configurable: false, 13 | enumerable: true, 14 | value: value 15 | }); 16 | } 17 | 18 | function Context(name, timeout, req) { 19 | if (!(this instanceof Context)) 20 | return new Context(name, timeout, req); 21 | EventEmitter.call(this); 22 | this[_req] = req; 23 | this[_timeout] = timeout; 24 | constant(this, 'start', process.hrtime()); 25 | constant(this, 'id', uuid.v4()); 26 | constant(this, 'ts', new Date().valueOf()); 27 | constant(this, 'ip', req.ip); 28 | constant(this, 'name', name); 29 | constant(this, 'lenient', req.prefer.handling == 'lenient'); 30 | } 31 | util.inherits(Context,EventEmitter); 32 | 33 | function succeed(body) { 34 | if (!this.complete) { 35 | constant(this, 'succeed', true); 36 | constant(this, 'complete', true); 37 | this.emit('success', body, this); 38 | } 39 | } 40 | 41 | function fail(err) { 42 | constant(this, 'failed', true); 43 | constant(this, 'complete', true); 44 | this.emit('fail', err); 45 | } 46 | 47 | function done(err, body) { 48 | if (err) { 49 | this.fail(err); 50 | return; 51 | } 52 | this.succeed(body); 53 | } 54 | 55 | function cancel() { 56 | constant(this, 'canceled', true); 57 | constant(this, 'complete', true); 58 | this.emit('cancel'); 59 | } 60 | 61 | function getRemainingTimeInMillis() { 62 | var d = new Date(); 63 | return (this.ts + this[_timeout]) - d.valueOf(); 64 | } 65 | 66 | constant(Context.prototype, 'succeed', succeed); 67 | constant(Context.prototype, 'fail', fail); 68 | constant(Context.prototype, 'done', done); 69 | constant(Context.prototype, 'cancel', cancel); 70 | constant( 71 | Context.prototype, 72 | 'getRemainingTimeInMillis', 73 | getRemainingTimeInMillis); 74 | 75 | module.exports = Context; 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A simple example of implementing AWS lambda like functionality 2 | with Node.js and Docker containers. 3 | 4 | Build the lambda daemon image using: 5 | 6 | ``` 7 | cd daemon 8 | docker build -t lambda . 9 | ``` 10 | 11 | Then, from a different location, you can create your own lambda functions, 12 | 13 | lambda.js 14 | ``` 15 | exports.handler = function(input, context) { 16 | context.succeed({abc:123}); 17 | }; 18 | ``` 19 | 20 | Dockerfile 21 | ``` 22 | FROM lambda 23 | COPY lambda.js /usr/src/app/lambda/ 24 | EXPOSE 8080 25 | ``` 26 | 27 | ``` 28 | docker build -t lambda-example . 29 | ``` 30 | 31 | To run: 32 | 33 | ``` 34 | docker run -p=8080:8080 lambda-example 35 | ``` 36 | 37 | * Your lambda function will be running at `http://{dockerhost}:8080/` 38 | * Your lambda function accepts JSON or url-encoded form input. 39 | * Your lambda function can provide a Passport Authentication Strategy. 40 | * You can do a HTTP GET to `http://{dockerhost}:8080/info` to view 41 | basic statistics. 42 | 43 | ## Program model 44 | 45 | The `context` object has the following methods: 46 | 47 | * `succeed(body)` - call to indicate that the function is complete and 48 | successful. The body will be passed to JSON.stringify and returned. 49 | * `fail(error)` - call to indicate that the function has failed. The 50 | error will be passed to JSON.stringify and returned. If the error 51 | object has a `status` property, it will be set as the HTTP Response 52 | status code (e.g. `error.status = 400`) 53 | * `cancel()` - call to indicate that the function has been aborted/canceled. 54 | An empty response will be returned 55 | * `done(error,body)` - alternative to `succeed` and `fail` that uses the 56 | typical Node.js callback pattern. 57 | * `getRemainingTimeInMillis()` - returns the approximate remaining time 58 | before the function times out. The default timeout is 10 seconds 59 | (which means the function will be automatically aborted if it does 60 | not return a result within 10 seconds). The default can be overriden 61 | using the `LAMBDA_TIMEOUT` environment variable. 62 | 63 | The `context` object has the following properties: 64 | 65 | * `id` - unique UUID for the request 66 | * `ts` - Unix timestamp indicating when the request was receieved 67 | * `ip` - The client IP address 68 | * `name` - The name of the lambda function. The name can be set using 69 | the `LAMBDA_NAME` environment variable 70 | * `lenient` - `true` if the lambda function should be run in `lenient` 71 | mode. Defaults to `false`. This is requested by the client using the 72 | HTTP Prefer header (`Prefer: handling=lenient`) in the request. 73 | Lenient mode would indicate that the function should be more tolerant 74 | of possible failure conditions. 75 | 76 | (note, these properties are different than what you see in Amazon's 77 | API. The intent here was to provide an example of similar functionality, 78 | not to fully duplicate it) 79 | 80 | Lambda functions must be written to be as ephemeral as possible, maintaining 81 | no state of their own. Each invocation should be entirely self-contained. 82 | 83 | Your lambda functions can require other modules but those need to be 84 | installed using the Dockerfile. A future iteration of this demo may 85 | try to automate the installation process a bit more for lambda function 86 | dependencies. 87 | -------------------------------------------------------------------------------- /daemon/lib/lambdad.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Each lambda daemon presents a simple HTTP API. 3 | * HTTP GET to root (/) will redirect to /info, returning information about 4 | * the lambda function. 5 | * HTTP POST to used to invoke the lambda by passing data in. 6 | * if the client requests an asynchronous response, a request ID 7 | * will be returned, along with a redirect to the status URI 8 | * HTTP OPTIONS is used to enable CORS-preflight 9 | **/ 10 | 'use strict'; 11 | 12 | var util = require('util'); 13 | var express = require('express'); 14 | var bodyParser = require('body-parser'); 15 | var passport = require('passport'); 16 | var Anonymous = require('passport-anonymous'); 17 | var Context = require('./context'); 18 | var prefer = require('./prefer'); 19 | var lambda = require('../lambda'); 20 | 21 | var name = process.env.LAMBDA_NAME || '(anonymous)'; 22 | //var host = process.env.VCAP_APP_HOST || 'localhost'; 23 | var port = process.env.VCAP_APP_PORT || 8080; 24 | var timeout = parseInt(process.env.LAMBDA_TIMEOUT) || 10 * 1000; 25 | // TODO: Proxy configuration from env 26 | 27 | // init express 28 | var app = express(); 29 | app.use(passport.initialize()); 30 | app.use(prefer); 31 | app.use(bodyParser.json({ 32 | limit: '1024kb' 33 | })); 34 | app.use(bodyParser.json({ 35 | limit: '1024kb', 36 | type: 'application/*+json' 37 | })); 38 | app.use(bodyParser.urlencoded({ extended: true })); 39 | 40 | passport.use('auth', lambda.auth || new Anonymous()); 41 | 42 | var stats = { 43 | count: 0, 44 | success: { 45 | count: 0, 46 | avg: 0.0 47 | }, 48 | fail: { 49 | count: 0, 50 | avg: 0.0 51 | }, 52 | cancel: { 53 | count: 0, 54 | avg: 0.0 55 | }, 56 | }; 57 | function report(context,type) { 58 | var end = process.hrtime(context.start); 59 | var mil = (end[0] * 1e9 + end[1]) / 1000000; 60 | stats[type].count++; 61 | var cma = stats[type].avg; 62 | stats[type].avg = cma + ((mil-cma)/stats[type].count); 63 | } 64 | 65 | app.get('/', function(req, res) { 66 | res.redirect('/info/'); 67 | }); 68 | 69 | app.post('/', 70 | passport.authenticate('auth', { session: false }), 71 | function(req, res) { 72 | var handler = lambda.handler; 73 | var context = new Context(name, timeout, req) 74 | .on('success', function(body) { 75 | try { 76 | if (body) { 77 | res.status(200); 78 | res.json(body); 79 | } else { 80 | res.status(204); 81 | } 82 | res.end(); 83 | report(context, 'success'); 84 | } catch (error) { 85 | res.status(500).end(); 86 | report(context, 'fail'); 87 | } 88 | }) 89 | .on('fail', function(err) { 90 | if (err.status) { 91 | res.status(err.status); 92 | } 93 | res.json(err); 94 | res.end(); 95 | report(context, 'fail'); 96 | }) 97 | .on('cancel', function() { 98 | res.setHeader('X-LAMBDA-STATUS', 'canceled'); 99 | res.status(204).end(); 100 | report(context, 'cancel'); 101 | }); 102 | if (typeof handler !== 'function') { 103 | context.fail({ 104 | status:500, 105 | message:'Lambda function not properly configured' 106 | }); 107 | } else { 108 | var ci = setImmediate(function() { 109 | try { 110 | res.setHeader('X-LAMBDA', name); 111 | handler.call(context, req.body, context); 112 | } catch (err) { 113 | throw err; 114 | context.fail({status:400, message:'Bad Request'}); 115 | } 116 | }); 117 | setTimeout(function() { 118 | clearTimeout(ci); 119 | if (!context.complete) 120 | context.cancel(); 121 | }, timeout); 122 | req.on('close', function() { 123 | clearTimeout(ci); 124 | if(!context.complete) 125 | context.cancel(); 126 | }); 127 | } 128 | }); 129 | 130 | app.options('/', function(req, res) { 131 | 132 | }); 133 | 134 | app.get( 135 | '/info', 136 | passport.authenticate('auth', { session: false }), 137 | function(req, res) { 138 | res.status(200).json(stats); 139 | }); 140 | 141 | app.listen(port); // use configurable port 142 | console.log(util.format('Lambda function %s running on port %d', name, port)); 143 | --------------------------------------------------------------------------------