├── .gitignore ├── Dockerfile ├── README.md ├── _container.js ├── bin └── ndl └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | index.js -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:base 2 | # FROM mhart/alpine-node:base-0.10 3 | # FROM mhart/alpine-node 4 | 5 | WORKDIR /src 6 | ADD . . 7 | 8 | # See mhart/apline-node for more info on the following. 9 | # Most likely you already have all of your dependencies in 10 | # `node_modules` because AWS Lambda does not run npm install 11 | # either and you likely were testing locally. 12 | # 13 | # If you have native dependencies, you'll need extra tools 14 | # RUN apk add --update make gcc g++ python 15 | 16 | # If you need npm, don't use a base tag 17 | # RUN npm install 18 | 19 | # If you had native dependencies you can now remove build tools 20 | # RUN apk del make gcc g++ python && \ 21 | # rm -rf /tmp/* /var/cache/apk/* /root/.npm /root/.node-gyp 22 | 23 | ENTRYPOINT ["node", "_container.js"] 24 | # {} is the default parameter passed, but it can be overwritten. 25 | # docker run param 26 | # 27 | # For example: docker run lambda-container '{"my": "event message"}' 28 | # 29 | # `container.js` will use JSON.parse(). If it fails, it will exit with an error. 30 | # If there is no parameter passed, that's ok. It uses "{}" by default. 31 | # 32 | # Note: base64 stringified JSON is supported here too. 33 | # For example: docker run lambda-container 'eyJteSI6ICJldmVudCBtZXNzYWdlIn0=' 34 | CMD ["{}"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-docker-lambda 2 | 3 | Ever want to run your AWS Lambda functions somewhere other than AWS Lambda? Did you create some awesome Lambda only to find out it takes 4 | longer than 5 minutes to run? Not a problem! You can easily run your AWS Lambda function anywhere you can run Docker. 5 | 6 | This is designed as a command line tool to help you wrap your existing (Node.js) AWS Lambda functions within a Docker container to run. 7 | You can also simply look at the `Dockerfile` and `_container.js` file here for examples in building your own Docker container. 8 | 9 | It's useful if you want to sometimes run code in AWS Lambda while other times in a Docker container on your own servers or perhaps through ECS. 10 | 11 | It assumes your AWS Lambda function exists within an `index.js` file as `exports.handler()` (for now). If you need to change this or change 12 | some of the options for the Docker container, you can use the copy command instead of the build command. This will give you the opportunity 13 | to change everything. If your needs are basic, you may be able to simply run the build command. 14 | 15 | ## Installation 16 | 17 | `npm install -g node-docker-lambda` 18 | 19 | ## Usage 20 | 21 | You'll want to run these commands from where your AWS Lambda code exists. 22 | 23 | To just copy the boilerplate files over (`Dockerfile` and `_container.js`) to your current working directory, run: 24 | 25 | `ndl copy` 26 | 27 | This way you can alter the Dockerfile and even the little Lambda "emulator" as you see fit. Then manually build the Docker image. 28 | 29 | If you want to build the Docker container right away, just run: 30 | 31 | `ndl build ` 32 | 33 | Where `` is the repository name/tag of your container (it's the `-t` flag for Docker). 34 | 35 | ## Running & Deploying 36 | 37 | To run the container: 38 | 39 | `docker run container-name '{"event": "message"}'` 40 | 41 | You can deploy your Lambda Docker container to anywhere you'd normally deploy your Docker containers. Perhaps it's ECS. Especially 42 | if you are using other Amazon services. Note: Unlike AWS Lambda, you will not have an AWS session - so you will need to pass some 43 | credentials. Setting environment variables is a good way to do this. Edit the `Dockerfile` (you'll need to use the copy command 44 | and manually build the container) or use Amazon ECS to do it as an override in the Task. 45 | 46 | Note: Amazon ECS lets you run Tasks directly from their web console. However, passing a JSON string to their form for the CMD 47 | override presented some challenges. When that data was passed along and eventually got handed to `_container.js` as an argument, 48 | it could not be parsed. At least for now, you can pass a base64 encoded JSON string which won't have any problems with escaping. 49 | 50 | Don't forget, `_container.js` logs the results out to console (for context `done()`, `fail()` and `succeed()`) so use `docker logs` 51 | to see the output from your docker containers if you aren't running them from the command line yourself. -------------------------------------------------------------------------------- /_container.js: -------------------------------------------------------------------------------- 1 | // We won't have the same exact context object as AWS Lambda of course, but there will be some useful info here. 2 | var pjson = require('./package.json'); 3 | var context = { 4 | // Not present in Lambda, we will make available the package.json to our context. 5 | package: pjson, 6 | // Not exactly the same thing, but close. 7 | functionName: pjson.name, 8 | functionVersion: pjson.version, 9 | memoryLimitInMB: 0, 10 | invokedFunctionArn: "", 11 | 12 | done: function(err, msg) { if(err){ console.log(msg); } process.exit(); }, 13 | fail: function(err) { if(err){ console.log(err); } process.exit(); }, 14 | succeed: function(msg) { if(msg){ console.log(msg); } process.exit(); }, 15 | // Docker containers don't have a max execution time like Lambda, but we'll still provide this function for compatibility. 16 | getRemainingTimeInMillis: function() { return 0; } 17 | }; 18 | // The event JSON is passed as a command line argument (for now, perhaps reading from a file or an HTTP listener may also be good ideas). 19 | // node _container.js '{"foo": "bar"}' 20 | // ^0 ^1 ^2 21 | if(process.argv[2] !== undefined) { 22 | var event = ""; 23 | // docker run container-name '{"event": "message"}' 24 | // ^ is the command to make this happen... 25 | // But when you override the CMD value from AWS console in your web browser, when running the ECS task, 26 | // the form input field seems to create problems with passing along the JSON string. The JSON.parse() 27 | // here can't parse the message due various escaping issues. Converting to base64 seemed to work. 28 | // So both are supported to make life a little easier (until I can find a better solution). 29 | var base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; 30 | if(base64regex.test(process.argv[2])) { 31 | // If base64 encoded. 32 | try{ 33 | event = new Buffer(process.argv[2], 'base64').toString('ascii'); 34 | event = JSON.parse(event) || {}; 35 | require("./index.js").handler(event, context); 36 | } catch(e) { 37 | console.log("Error parsing event message (tried to decode base64).", e); 38 | } 39 | } else { 40 | // Not base64 encoded. 41 | try{ 42 | event = JSON.parse(process.argv[2]) || {}; 43 | require("./index.js").handler(event, context); 44 | } catch(e) { 45 | console.log("Error parsing event message.", e); 46 | } 47 | } 48 | } else { 49 | console.log("No event message passed."); 50 | process.exit(); 51 | } 52 | 53 | // Just in case your Lambda doesn't catch some error. This ensures the container exits, freeing things up. 54 | process.on('uncaughtException', function (err) { 55 | console.log(err, err.stack); 56 | process.exit(1); 57 | }); -------------------------------------------------------------------------------- /bin/ndl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var program = require('commander') 4 | , path = require('path') 5 | , fs = require('fs') 6 | , exec = require('child_process').exec 7 | , commandExists = require('command-exists'); 8 | 9 | var packageJson = require(path.resolve(__dirname, '..', 'package.json')); 10 | 11 | // The two files we need for everything. 12 | var containerFile = path.resolve(__dirname, '..', '_container.js'); 13 | var dockerFile = path.resolve(__dirname, '..', 'Dockerfile'); 14 | var copiedContainerFile = path.resolve(process.cwd(), '_container.js'); 15 | var copiedDockerFile = path.resolve(process.cwd(), 'Dockerfile'); 16 | 17 | // Copies the files required to build the Docker image over to the current working directory. 18 | var copyFilesToCwd = function(verbose, cb) { 19 | cb = (cb === undefined) ? function(){}:cb; 20 | 21 | if(!fs.existsSync(copiedContainerFile)) { 22 | fs.writeFileSync(copiedContainerFile, fs.readFileSync(containerFile)); 23 | if(verbose) { 24 | console.log("Copied `_container.js`"); 25 | } 26 | } else { 27 | if(verbose) { 28 | console.log("`_container.js` already exists, skipping."); 29 | } 30 | } 31 | 32 | if(!fs.existsSync(copiedDockerFile)) { 33 | fs.writeFileSync(copiedDockerFile, fs.readFileSync(dockerFile)); 34 | if(verbose) { 35 | console.log("Copied `Dockerfile`"); 36 | } 37 | } else { 38 | if(verbose) { 39 | console.log("`Dockerfile` already exists, skipping."); 40 | } 41 | } 42 | 43 | return cb(); 44 | }; 45 | 46 | // Removes or cleans up the working files used to build the Docker image. This runs if the files are no longer needed/wanted 47 | // by the user (depends on the command executed). 48 | var deleteFilesFromCwd = function() { 49 | if(fs.existsSync(copiedDockerFile)) { 50 | fs.unlinkSync(copiedDockerFile); 51 | } 52 | 53 | if(fs.existsSync(copiedContainerFile)) { 54 | fs.unlinkSync(copiedContainerFile); 55 | } 56 | }; 57 | 58 | // Builds the Docker image (requires the `docker` command). 59 | var buildDockerImage = function(verbose, options, cb) { 60 | cb = (cb === undefined) ? function(){}:cb; 61 | return exec("docker build -t " + options.name + " " + process.cwd(), cb); 62 | }; 63 | 64 | // TODO: This 65 | // program 66 | // .version(lambda.version) 67 | // .command('deploy') 68 | // .description('Deploy your Dockerized AWS Lambda code to ECS.') 69 | // // I think many of these can be added by the user to their Dockerfile which is generated for them. 70 | // // 71 | // // 72 | // //.option('-a, --accessKey [' + AWS_ACCESS_KEY_ID + ']', 'AWS Access Key', AWS_ACCESS_KEY_ID) 73 | // //.option('-s, --secretKey [' + AWS_SECRET_ACCESS_KEY + ']', 'AWS Secret Key', AWS_SECRET_ACCESS_KEY) 74 | // //.option('-p, --profile [' + AWS_PROFILE + ']', 'AWS Credentials Profile', AWS_PROFILE) 75 | // //.option('-k, --sessionToken [' + AWS_SESSION_TOKEN + ']', 'AWS Session Token', AWS_SESSION_TOKEN) 76 | // //.option('-r, --region [' + AWS_REGION + ']', 'AWS Region', AWS_REGION) 77 | // //.option('-n, --functionName [' + AWS_FUNCTION_NAME + ']', 'Lambda FunctionName', AWS_FUNCTION_NAME) 78 | // //.option('-h, --handler [' + AWS_HANDLER + ']', 'Lambda Handler {index.handler}', AWS_HANDLER) 79 | // //.option('-o, --role [' + AWS_ROLE + ']', 'Amazon Role ARN', AWS_ROLE) 80 | // //.option('-m, --memorySize [' + AWS_MEMORY_SIZE + ']', 'Lambda Memory Size', AWS_MEMORY_SIZE) 81 | // //.option('-v, --version [' + AWS_FUNCTION_VERSION + ']', 'Lambda Function Version', AWS_FUNCTION_VERSION) 82 | // .action(function (prg) { 83 | // lambda.deploy(prg); 84 | // }); 85 | 86 | // Assuming Docker is installed, this command builds the default container to run a Lambda. 87 | // This assumes there is an `index.js` file with an `exports.handler()` function to call. 88 | program 89 | .version(packageJson.version) 90 | .command('build ') 91 | .description('Dockerize your AWS Lambda code without any customizations to the container.') 92 | .action(function(imageName) { 93 | commandExists('docker', function(err, commandExists) { 94 | if(commandExists) { 95 | // proceed confidently knowing this command is available 96 | copyFilesToCwd(false, function() { 97 | buildDockerImage(true, {name: imageName}, function(err, stdout, stderr) { 98 | deleteFilesFromCwd(); 99 | if(!err) { 100 | console.log("You should now see your image (" + imageName + ") by running `docker images`"); 101 | } else { 102 | console.log(stdout); 103 | } 104 | }); 105 | }); 106 | } else { 107 | console.log("You'll need to have Docker installed. It's very easy to do, go visit https://www.docker.com"); 108 | } 109 | }); 110 | }); 111 | 112 | // This command will simply copy the files needed to build a Docker container over to the current working directory. 113 | // So one can run it in the directory with their Lambda code and then manually build the Docker container from there. 114 | // The reason someone may want to run this is so that they can alter the `Dockerfile` and even the `_container.js` if needed. 115 | // This could allow a user to add additional things to the container, add environment variables, etc. 116 | program 117 | .version(packageJson.version) 118 | .command('copy') 119 | .description('Copy the Dockerfile and _container.js, to handle your Lambda, to the current directory. Then, edit as needed and build the Docker image manually.') 120 | .action(function() { 121 | copyFilesToCwd(true); 122 | }); 123 | 124 | program.parse(process.argv); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-docker-lambda", 3 | "version": "0.3.1", 4 | "description": "A Docker container that mimics AWS Lambda, allowing you to run your Lambda code within Docker (and ECS).", 5 | "main": "_container.js", 6 | "author": "Tom Maiaroto", 7 | "email": "tom@shift8creative.com", 8 | "url": "https://github.com/tmaiaroto/node-docker-lambda", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/tmaiaroto/node-docker-lambda.git" 12 | }, 13 | "license": "MIT", 14 | "devDependencies": {}, 15 | "dependencies": { 16 | "command-exists": "^0.1.0", 17 | "commander": "^2.9.0" 18 | } 19 | } 20 | --------------------------------------------------------------------------------