├── .gitignore ├── .travis.yml ├── README.md ├── Vagrantfile ├── examples ├── copy_instance_tags_to_volume │ ├── index.js │ └── package.json ├── copy_volume_tags_to_snapshot │ ├── index.js │ └── package.json ├── echo │ ├── handler.js │ ├── package.json │ └── publish.sh ├── instance_count │ ├── index.js │ └── package.json └── tag_check │ ├── index.js │ └── package.json ├── index.coffee ├── init.sh ├── package.json └── src └── lambda.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | *.un~ 4 | .vagrant 5 | .env 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hubot-lambda 2 | [![Build Status](https://travis-ci.org/davemkirk/hubot-lambda.svg?branch=master)](https://travis-ci.org/davemkirk/hubot-lambda) 3 | 4 | [![NPM](https://nodei.co/npm/hubot-lambda.png?downloads=true)](https://nodei.co/npm/hubot-lambda/) 5 | 6 | A [Hubot](https://hubot.github.com/) script for invoking [AWS Lambda](http://aws.amazon.com/lambda/) functions 7 | 8 | ## Why 9 | 10 | - Separate invocation privileges from execution privileges. 11 | - New languages will almost certainly be added to AWS Lambda, in this case hubot-lambda would enable easy cross language Hubot integrations. (Amazon recently announced that support for Java Lambda functions are coming soon. https://aws.amazon.com/blogs/aws/aws-lambda-update-production-status-and-a-focus-on-mobile-apps/) 12 | - Potentially a robust mechanism for enabling ad-hoc hubot script additions/modifications without recycling the hubot process. (I'd like some feedback on this one) 13 | 14 | ## Installation 15 | 16 | Add `hubot-lambda` to your package.json, run `npm install` and add hubot-lambda to `external-scripts.json`. 17 | 18 | Add hubot-lambda to your `package.json` dependencies. 19 | 20 | ``` 21 | "dependencies": { 22 | "hubot-lambda": "0.0.0" 23 | } 24 | ``` 25 | 26 | Add `hubot-lambda` to `external-scripts.json`. 27 | 28 | ``` 29 | > cat external-scripts.json 30 | > ["hubot-lambda"] 31 | ``` 32 | 33 | ##### Required ENV Variables 34 | 35 | ``` 36 | > export HUBOT_LAMBDA_AWS_ACCESS_KEY_ID="XXXX" 37 | > export HUBOT_LAMBDA_AWS_SECRET_ACCESS_KEY="XXXX" 38 | ``` 39 | 40 | ##### Required AWS User Policy 41 | ``` 42 | { 43 | "Version": "2012-10-17", 44 | "Statement": [ 45 | { 46 | "Effect": "Allow", 47 | "Action": [ 48 | "lambda:InvokeFunction" 49 | ], 50 | "Resource": "*" 51 | } 52 | ] 53 | } 54 | ``` 55 | 56 | 57 | Usage 58 | ----- 59 | 60 | - `hubot lambda ` 61 | 62 | Example 63 | ----- 64 | 65 | - `hubot lambda helloWorld Yo` 66 | 67 | 68 | 69 | ## Developing hubot-lambda 70 | 71 | ##### Running from working dir 72 | ----- 73 | ``` 74 | vagrant up 75 | vagrant ssh 76 | cd hubot 77 | ./bin/hubot 78 | ``` 79 | 80 | ##### Troubleshooting 81 | ----- 82 | >HUBOT_LOG_LEVEL=debug ./bin/hubot 83 | 84 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | 9 | config.vm.box = "ubuntu-14.04" 10 | config.vm.network "forwarded_port", guest: 8080, host: 8080 11 | config.vm.provider "virtualbox" do |vb| 12 | vb.memory = 256 13 | end 14 | 15 | config.vm.provision "shell", path: "init.sh", run: "once", privileged: false 16 | 17 | end 18 | -------------------------------------------------------------------------------- /examples/copy_instance_tags_to_volume/index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var async = require('async'); 3 | var _ = require('lodash'); 4 | 5 | // Copy the tags from the instance to the volume 6 | function copyInstanceTags(volume, callback) { 7 | var tags = []; 8 | for (var name in volume.tags) { 9 | tags.push({ Key: name, Value: volume.tags[name] }); 10 | } 11 | 12 | var params = { 13 | Resources: [ volume.volumeId ], 14 | Tags: tags 15 | }; 16 | 17 | (new AWS.EC2({ region: volume.region })).createTags(params, function(err) { 18 | if (err) { 19 | callback(err); 20 | } 21 | else { 22 | callback(); 23 | } 24 | }); 25 | } 26 | 27 | // Grab all of the volumes that are "in-use" for a region 28 | function getAttachedVolumesByRegion(region, callback) { 29 | (new AWS.EC2({region: region})).describeVolumes({ 30 | Filters: [{ 31 | Name: "status", 32 | Values: [ 33 | "in-use" 34 | ] 35 | }] 36 | }, function(err, data) { 37 | if (err) { 38 | callback(err, null); 39 | } 40 | else { 41 | callback(null, { 42 | region: region, 43 | volumes: data.Volumes 44 | }); 45 | } 46 | }); 47 | } 48 | 49 | // Grab all instance tags for all of the volumes of a region 50 | function getInstanceTagsForVolumeByRegion(region, callback) { 51 | var instances = _(region.volumes) 52 | .pluck("Attachments") 53 | .flatten() 54 | .value() 55 | .map(function(volume) { 56 | return { 57 | region: region.region, 58 | volumeId: volume.VolumeId, 59 | instanceId: volume.InstanceId 60 | }; 61 | }); 62 | 63 | if (instances.length > 0) { 64 | (new AWS.EC2({ region: region.region })).describeInstances({ 65 | InstanceIds: _(instances).pluck("instanceId").value() 66 | }, function(err, data) { 67 | if (err) { 68 | callback(err, null); 69 | } 70 | else { 71 | instances = _.map(instances, function(instance) { 72 | var ec2Instance = _(data.Reservations) 73 | .pluck("Instances") 74 | .flatten() 75 | .value() 76 | .filter(function(i) { 77 | return i.InstanceId == instance.instanceId; 78 | }); 79 | 80 | var tags = _(ec2Instance) 81 | .pluck("Tags") 82 | .flatten() 83 | .filter(function (tag) { 84 | return tag.Key == "Project" 85 | || tag.Key == "Name" 86 | || tag.Key == "Owner"; 87 | }) 88 | .value(); 89 | 90 | instance.tags = []; 91 | _.each(tags, function(tag) { 92 | instance.tags[tag.Key] = tag.Value; 93 | }); 94 | 95 | return instance; 96 | }); 97 | 98 | callback(null, instances); 99 | } 100 | }); 101 | } 102 | else { 103 | // region has no instances with attached volumes 104 | callback(); 105 | } 106 | } 107 | 108 | exports.handler = function(event, context) { 109 | 110 | (new AWS.EC2()).describeRegions({}, function(err, data) { 111 | if (err) { 112 | context.fail(err); 113 | } 114 | else { 115 | var regions = data.Regions.map(function(region) { 116 | return region.RegionName; 117 | }); 118 | 119 | async.map(regions, getAttachedVolumesByRegion, function(err, regions) { 120 | if (err) { 121 | context.fail(err); 122 | } 123 | else { 124 | async.map(regions, getInstanceTagsForVolumeByRegion, 125 | function(err, volumes) { 126 | if (err) { 127 | context.fail(err); 128 | } 129 | else { 130 | async.each(_(volumes).compact().flatten().value(), 131 | copyInstanceTags, function(err) { 132 | if (err) { 133 | context.fail(err); 134 | } 135 | else { 136 | context.succeed("All required instance tags have been applied."); 137 | } 138 | }); 139 | } 140 | }); 141 | } 142 | }); 143 | } 144 | }); 145 | } 146 | -------------------------------------------------------------------------------- /examples/copy_instance_tags_to_volume/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples_copy_instance_tags_to_volume", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "John Jelinek IV ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "async": "^1.2.1", 13 | "aws-sdk": "^2.1.35", 14 | "lodash": "^3.9.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/copy_volume_tags_to_snapshot/index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var async = require('async'); 3 | var _ = require('lodash'); 4 | 5 | // Copy the tags from the volume to the snapshot 6 | function copyVolumeTags(snapshot, callback) { 7 | var params = { 8 | Resources: [snapshot.snapshotId], 9 | Tags: snapshot.tags 10 | }; 11 | 12 | (new AWS.EC2({ 13 | region: snapshot.region 14 | })).createTags(params, function(err) { 15 | if (err) { 16 | callback(err); 17 | } else { 18 | callback(); 19 | } 20 | }); 21 | } 22 | 23 | // Grab all of the volumes that are tagged with required tags for a region 24 | function getTaggedVolumesByRegion(region, callback) { 25 | (new AWS.EC2({ 26 | region: region 27 | })).describeVolumes({ 28 | Filters: [{ 29 | Name: "tag-key", 30 | Values: [ 31 | "Name", 32 | "Project", 33 | "Owner" 34 | ] 35 | }] 36 | }, function(err, data) { 37 | if (err) { 38 | callback(err, null); 39 | } else { 40 | callback(null, { 41 | region: region, 42 | volumes: data.Volumes 43 | }); 44 | } 45 | }); 46 | } 47 | 48 | // Grab all snapshots for a volume 49 | function getSnapshotsForVolumeByRegion(region, callback) { 50 | var volumeIds = _(region.volumes) 51 | .pluck("Attachments") 52 | .flatten() 53 | .value() 54 | .map(function(volume) { 55 | return volume.VolumeId; 56 | }); 57 | 58 | if (volumeIds.length > 0) { 59 | (new AWS.EC2({ 60 | region: region.region 61 | })).describeSnapshots({ 62 | Filters: [{ 63 | Name: "volume-id", 64 | Values: volumeIds 65 | }] 66 | }, function(err, data) { 67 | if (err) { 68 | callback(err, null); 69 | } else { 70 | var snapshots = data.Snapshots 71 | .map(function(snapshot) { 72 | var tags = _.pluck(_.where(region.volumes, { 73 | VolumeId: snapshot.VolumeId 74 | }), "Tags"); 75 | 76 | return { 77 | snapshotId: snapshot.SnapshotId, 78 | volumeId: snapshot.VolumeId, 79 | region: region.region, 80 | tags: _(tags).flatten().value() 81 | }; 82 | }); 83 | 84 | callback(null, snapshots); 85 | } 86 | }) 87 | } else { 88 | // region has no tagged volumes 89 | callback(); 90 | } 91 | } 92 | 93 | exports.handler = function(event, context) { 94 | (new AWS.EC2()).describeRegions({}, function(err, data) { 95 | if (err) { 96 | context.fail(err); 97 | } else { 98 | var regions = data.Regions.map(function(region) { 99 | return region.RegionName; 100 | }); 101 | 102 | async.map(regions, getTaggedVolumesByRegion, function(err, regions) { 103 | if (err) { 104 | context.fail(err); 105 | } else { 106 | async.map(regions, getSnapshotsForVolumeByRegion, 107 | function(err, volumes) { 108 | if (err) { 109 | context.fail(err); 110 | } else { 111 | async.each(_.flatten(_.compact(volumes)), copyVolumeTags, 112 | function(err) { 113 | if (err) { 114 | context.fail(err); 115 | } else { 116 | context.succeed("All volume tags have been applied"); 117 | } 118 | }); 119 | } 120 | }); 121 | } 122 | }); 123 | } 124 | }); 125 | } 126 | -------------------------------------------------------------------------------- /examples/copy_volume_tags_to_snapshot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples_copy_volume_tags_to_snapshot", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "John Jelinek IV ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "async": "^1.2.1", 13 | "aws-sdk": "^2.1.35", 14 | "lodash": "^3.9.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/echo/handler.js: -------------------------------------------------------------------------------- 1 | 2 | exports.handler = function(event, context) { 3 | 4 | var message = event.message; 5 | console.log(message); 6 | 7 | if(message == null){ 8 | console.log("ERROR: expected input missing \n" + JSON.stringify(event)); 9 | context.done(null); 10 | return; 11 | } 12 | 13 | context.succeed(message); // Echo back the message 14 | }; 15 | 16 | -------------------------------------------------------------------------------- /examples/echo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-lambda-echo-handler", 3 | "version": "0.0.0", 4 | "description": "Example AWS Lambda script that responds to hubot-lambda", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "aws", 11 | "lambda", 12 | "hubot" 13 | ], 14 | "author": "David Kirk ", 15 | "license": "MIT", 16 | "dependencies": { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/echo/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 3 | 4 | #make sure dependencies are installed 5 | npm install 6 | 7 | #Prompt for Lambda Execution Role ARN 8 | echo -n "Enter your lambda execution role ARN: " 9 | read role 10 | 11 | #prompt for AWS Region 12 | region="us-east-1" 13 | echo -n "Please enter the AWS Region ($region): " 14 | read input 15 | region="${input:-$region}" 16 | 17 | 18 | #Zip for upload 19 | pushd $dir 20 | zip -r -q echo.zip * 21 | popd 22 | 23 | #push to AWS 24 | aws lambda upload-function \ 25 | --region $region \ 26 | --function-name "echo" \ 27 | --function-zip "$dir/echo.zip" \ 28 | --role "$role" \ 29 | --mode event \ 30 | --handler handler.handler \ 31 | --runtime nodejs \ 32 | --timeout 3 \ 33 | --memory-size 128 34 | #--debug 35 | 36 | #Clean up 37 | rm "$dir/echo.zip" 38 | -------------------------------------------------------------------------------- /examples/instance_count/index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | 3 | // Set your region for future requests. 4 | AWS.config.region = 'us-east-1'; 5 | 6 | exports.handler = function(event, context){ 7 | 8 | var ec2 = new AWS.EC2(); 9 | 10 | // I use describeInstanceStatus here instead of describeInstance because 11 | // it seems about twice as fast in my cursory testing. 12 | ec2.describeInstanceStatus({'IncludeAllInstances':false}, function(err, data) { 13 | if (err){ 14 | console.log(err, err.stack); // an error occurred 15 | }else{ 16 | var message = "running instance count = " + data.InstanceStatuses.length; 17 | context.succeed(message); 18 | } 19 | }); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /examples/instance_count/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples_instance-count", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "David Kirk ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "aws-sdk": "^2.1.23" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/tag_check/index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var async = require('async'); 3 | var _ = require('lodash'); 4 | 5 | exports.handler = function(event, context){ 6 | 7 | var regions = [ 8 | "us-east-1", 9 | "us-west-1", 10 | "us-west-2", 11 | "eu-west-1", 12 | "eu-central-1", 13 | "ap-southeast-1", 14 | "ap-southeast-2", 15 | "ap-northeast-1", 16 | "sa-east-1" 17 | ]; 18 | 19 | var requiredTags = ['Project', 'Name', 'Owner']; 20 | var untaggedInstances = []; 21 | 22 | async.each(regions, function(r,cb){ 23 | 24 | var ec2 = new AWS.EC2({region:r}); 25 | ec2.describeInstances({}, function(err, data){ 26 | 27 | if(err){ 28 | cb(err); 29 | } else { 30 | 31 | //select instances 32 | var instances = _.map(data.Reservations, function(reservation){ return reservation.Instances; }); 33 | instances = _.flatten(instances); 34 | 35 | //Filter to instances missing requiredTags 36 | var filtered = 37 | _.filter(instances, function(instance){ 38 | var tagKeys = _.map(instance.Tags, function(tag){ return tag.Key; } ); 39 | for(var k = 0; k < requiredTags.length; k++){ 40 | if(!_.includes(tagKeys, requiredTags[k])){ 41 | return true; 42 | } 43 | } 44 | return false; 45 | }); 46 | 47 | //append results to the array of all untagged instances 48 | untaggedInstances = untaggedInstances.concat(filtered); 49 | 50 | cb(); 51 | } 52 | }); 53 | 54 | 55 | }, function(err){ 56 | if(err){ 57 | console.log(err, err.stack); // an error occurred 58 | } else { 59 | 60 | //build reponse message 61 | var message = ''; 62 | message = ':sadpanda: Untagged instances discovered:\n'; 63 | message += '```'; 64 | for(var i = 0; i < untaggedInstances.length; i++){ 65 | var inst = untaggedInstances[i]; 66 | message += _.padRight(inst.Placement.AvailabilityZone, 16) + '\t' + inst.InstanceId + ' (' + inst.State.Name + ')\t['; 67 | var tags = _.map(inst.Tags, function(tag){ return tag.Key + ':' + tag.Value; }); 68 | message += tags.join('; ') + ']\n'; 69 | } 70 | message += '```'; 71 | 72 | context.succeed(message); 73 | } 74 | }); 75 | 76 | } 77 | -------------------------------------------------------------------------------- /examples/tag_check/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples_tag_check", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "David Kirk ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "async": "^0.9.0", 13 | "aws-sdk": "^2.1.23", 14 | "lodash": "^3.8.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | module.exports = (robot, scripts) -> 5 | scriptsPath = path.resolve(__dirname, 'src') 6 | fs.exists scriptsPath, (exists) -> 7 | if exists 8 | for script in fs.readdirSync(scriptsPath) 9 | if scripts? and '*' not in scripts 10 | robot.loadFile(scriptsPath, script) if script in scripts 11 | else 12 | robot.loadFile(scriptsPath, script) 13 | -------------------------------------------------------------------------------- /init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt-get update 4 | sudo apt-get install -y nodejs npm=1.3.10~dfsg-1 redis-server=2:2.8.4-2 5 | ## note: use apt-show-versions to check install version 6 | 7 | ##link nodejs => node to correct path weirdness 8 | sudo ln -s /usr/bin/nodejs /usr/local/bin/node 9 | 10 | #setup hubot just like any user would 11 | sudo npm install -g coffee-script hubot@2.11 yo generator-hubot 12 | sudo rm -rf ~/tmp #clean up after yo 13 | 14 | mkdir ~/hubot -p 15 | cd ~/hubot 16 | sudo yo hubot --defaults 17 | sudo rm -rf ~/tmp #clean up after yo 18 | 19 | #install hubot-lambda package by adding it to external-scripts.json 20 | sed -i "1s/.*/[\n \"hubot-lambda\",/" external-scripts.json 21 | 22 | #link hubot-lambda to the vagrant working dir 23 | ln -nsf /vagrant ~/hubot/node_modules/hubot-lambda 24 | 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-lambda", 3 | "version": "0.0.4", 4 | "description": "A Hubot script for invoking AWS Lambda functions", 5 | "main": "index.coffee", 6 | "scripts": { 7 | "test": "echo test runner goes here" 8 | }, 9 | "keywords": [ 10 | "Hubot", 11 | "AWS", 12 | "Lambda" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/davemkirk/hubot-lambda.git" 17 | }, 18 | "author": "David Kirk ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "aws-sdk": "^2.1.23" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lambda.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Invokes an AWS Lambda function 3 | # 4 | # Commands: 5 | # hubot lambda - Invokes an AWS lambda function with the given args 6 | # 7 | # Notes: 8 | # 9 | # Author: 10 | # davemkirk@gmail.com 11 | 12 | region = process.env.HUBOT_LAMBDA_REGION ? "us-east-1" 13 | accessKeyId = process.env.HUBOT_LAMBDA_AWS_ACCESS_KEY_ID 14 | secretAccessKey = process.env.HUBOT_LAMBDA_AWS_SECRET_ACCESS_KEY 15 | 16 | AWS = require("aws-sdk") 17 | lambda = new AWS.Lambda( 18 | apiVersion: "2015-03-31" 19 | accessKeyId: accessKeyId 20 | secretAccessKey: secretAccessKey 21 | region: region 22 | sslEnabled: true 23 | ) 24 | 25 | module.exports = (robot) -> 26 | 27 | robot.respond /lambda ([a-zA-Z0-9-]+)\s?(.*)/i, (msg) -> 28 | 29 | func = msg.match[1] 30 | args = msg.match[2] 31 | 32 | parsed_args = {} 33 | args.replace /([^\s:]+):([^\s]+)/g, ($0, param, value) -> 34 | parsed_args[param] = value 35 | return 36 | 37 | payload = JSON.stringify(parsed_args) 38 | 39 | params = 40 | FunctionName: func 41 | InvocationType: 'RequestResponse' 42 | LogType: 'None' 43 | Payload: payload 44 | 45 | lambda.invoke( 46 | params 47 | ).on("success", (response) -> 48 | 49 | msg.send func + " invoked with " + payload 50 | 51 | response_payload = JSON.parse(response.data.Payload) 52 | msg.send response_payload 53 | # console.log(response.data.StatusCode, response.data.Payload) 54 | 55 | ).on("error", (response) -> 56 | msg.send "error: " + response 57 | 58 | ).send() 59 | 60 | --------------------------------------------------------------------------------