Serverless Slack Integrations with node.js, AWS Lambda, and AWS API Gateway

9-29-2015

tl;dr See example on GitHub »

Use node.js, Lambda, and API Gateway to integrate with Slack

We started using Slack at work towards the end of last year and haven't looked back. It was awesome when there were only 4 of us sitting in the same room and is even more awesome now that we have added a bunch more dev's, two of whom work remotely.

One of the best things about Slack are all the ways it integrates with tools and services that we use everyday - anything from GitHub to Snap-CI. And if that isn't cool enough for you, they made it really easy to build custom integrations with Incoming WebHooks, Outgoing WebHooks, Slash Commands, etc. which brings us to this article. The "hardest" part about building the integration is provisioning the server(s) to do all the heavy lifting - and that is where Amazon Webservices Lambda and API Gateway come in.

This tutorial will show you how to build a Slack Slash Command integration using AWS Lambda and API Gateway. We'll take the /weather example right off the Slash Commands page and make it work without provisioning any servers!

AWS Lambda and API Gateway

Lambda is based on EC2 and allows you to deploy and execute your code(Officially node.js or Java 8) without having to provision servers. Literally upload your code and set a couple parameters and your code is ready to go. And not just ready to execute, its ready to scale! As the load starts to increase AWS instantly spins up as many copys of your code as needed to maintain performance. Its pretty awesome - the free tier is pretty generous and will more than cover typical Slack Integrations that you may come up with unless you have a monster team hitting the API constantly.

Lambda responds to events, which can come from a variety of sources. By default Lambda isn't accessable from a URL, but API Gateway allows you to map a URL and an HTTP method to trigger your Lambda code. You can setup GETs, POSTs, PUTs, etc. and map the parameters/body into a JSON payload that Lambda understands. As of this writing the free tier includes 1M requests per month!

How is it going to work?

slack-lambda-weather flow chart
  1. User types command /weather 90210
  2. Slack POSTs data including zip code to our API Gateway URL
  3. API Gateway transforms the form data into JSON and Invokes our Lambda
  4. Lambda validates the request and then makes a call to the openweathermap.org API
  5. Open Weather API gives the weather data back to Lambda
  6. Lambda transforms the JSON into a message to be displayed in Slack and passes it back to API Gateway
  7. API Gateway sends a 200 response back to Slack with the formatted message
  8. Slack displays the message to the user

Slack Slash Command Integration

As I mentioned earlier, Slack makes it ridiculously easy to create custom integrations. In this post we'll cover a Slash Command example which is taken right from their example. But first you need to setup the slash command for your slack account.

  1. Goto Slack settings - something like https://YOUR_SUBDOMAIN.slack.com/services/new
  2. Scroll to bottom and click on Slash Commands
  3. Choose a Command - for this example enter "/weather" in the command name input.
  4. Click Add Slash Command Integration button.
  5. Now you should be on the settings page - scroll down and copy the token(NOTE: you don't want to expose the token to the public!) - we'll need it in a moment.
  6. Leave this page open because we'll need to come back and add a few things later.

Lets code already

Clone the repo. The repo is based on aws-lambda-starter which gives us some provisioning commands out of the box.

$ git clone https://github.com/ryanray/slack-lambda-weather.git
$ cd slack-lambda-weather && npm install

Setup your config.json. Copy or rename config.sample.json to config.json. Then update slashCommandToken with the value you got during the slash command setup.lambda.js

// config containing our slash command api token and any other "private" data you don't want to check in to git
var config = require('./config.json');

Now you can take a look at the lambda entry point.lambda.js

// config containing our slash command api token
var config = require('./config.json');

// our own library that fetches data from the api
var weather = require('./lib/weather.js');

// sample payload from slack command sent as application/x-www-form-urlencoded
// token=asdfghjklzxcvbnm
// team_id=T0001
// team_domain=example
// channel_id=C2147483705
// channel_name=test
// user_id=U2147483697
// user_name=Steve
// command=/weather
// text=94070

// Lambda needs JSON so API Gateway will need to transofrm it - we will
// cover that later.

// Entrypoint for AWS Lambda
// `event` is the JSON payload that API Gateway transformed from slack's 
//         application/x-www-form-urlencoded request
// `context` has methods to let Lambda know when we're done - similar 
//           to http/express modules `response.send()`
exports.handler = function(event, context) {
  var zipCode = event.text ? event.text.trim() : null;
  
  // verify request came from slack - could also check that event.command === /weather
  if(event.token !== config.slashCommandToken) {
    return context.fail('Unauthorized request. Check config.slashCommandToken.');
  }
  
  // validate zip code
  if(!isValidZip(zipCode)) {
    return context.fail('Must be a valid 5 digit zip code.');
  }
  
  // get weather && respond to slack request
  weather.byZip(zipCode).then(function(response) {
    // send the response back to API Gateway
    context.succeed(response);
  }).catch(function(reason) {
    // fail if we can't get the weather
    context.fail('Unable to get weather. Try again later.');
  });
};

function isValidZip(zip) {
  return zip && zip.length === 5 && !isNaN(zip);
}

Now that our AWS Lambda entry point is setup everything else is standard node.js! So based on the users zip code input we want to fetch the latest weather data. Luckily there is already an open api available to do this: OpenWeatherMap. We'll make the call in lib/weather.js

// standard `request` library wrapped with promises!
var rp = require('request-promise');

// expose our internal api - this way its easy to add additional methods in the future
module.exports = {
  byZip: getWeather
};

function getWeather(zipCode) {
  var url = 'http://api.openweathermap.org/data/2.5/weather?zip='
                + zipCode + ',us&units=imperial';
                
  // make request to open weather api - http://openweathermap.org/current
  return rp(url)
      .then(function(body) {
        // body is JSON as text - so tranform it into actual JSON
        var response = JSON.parse(body);
        
        // format the message that gets sent back to slack /weather 90210 ->
        // "Beverly Hills Weather - few clouds - Now:78 High: 82 Low: 69"
        var message = [
          response.name + ' Weather',
          '-',response.weather[0].description,'-',
          'Now:' + Math.floor(response.main.temp),
          'High: ' + Math.floor(response.main.temp_max),
          'Low: ' + Math.floor(response.main.temp_min)
        ].join(' ');
        
        
        return message;
      });
}

Deploy and Test

Thats it for the Lambda node.js portion! Simple huh? Now lets create the Lambda on aws. As long as you have an AWS account and the AWS CLI setup you can run the following commands. If you don't have the CLI setup you can login the console and upload the dist/lambda.zip file manually.

Create runs npm install, unit tests, bundles all the code including node_modules, and then deploys your code to AWS.

// create lambdaWeather on AWS
$ npm run create
// -> confirmation from AWS

Now that its created we can similate the JSON payload that it will see from API Gateway by providing a zip code and the slash command token and using the invoke command provided by aws-lambda-starter. Without the CLI setup you can configure a sample event in the AWS console with the same JSON.

// Invoke slackWeather Lambda on AWS
$ npm run invoke "{\"token\": \"SLACK_SLASH_COMMAND_TOKEN\", \"text\": \"90210\"}"
// -> "Beverly Hills Weather - few clouds - Now:76 High: 82 Low: 66"

We aren't going to use this one today but if you wanted to make some changes to lambda.js or weather.js all you would have to do is make your changes and then run the deploy command. Just like Create - Deploy runs npm install, unit tests, bundles all the code including node_modules, and then deploys your code to AWS.

// Deploy updates after Lambda has already been created
$ npm run deploy
// -> confirmation from AWS

AWS API Gateway Setup

If you've browsed around the Lambda Console you may have noticed a tab that says "API Endpoints". It allows you to create an endpoint from the tab but it didn't work for me so I prefer to navigate directly to the API Gateway Service. Then we need to create an endpoint that will invoke our Lambda when Slack POSTs the slash command data to it. It's probably not a bad idea to go through the Getting Started guide for API Gateway and Lambda Functions.

So now follow these steps

  1. Create new API - name it whatever you want - we'll do LambdaTest for now
  2. Create a new resource - you can name it whatever you'd like - we'll use slack-weather for the name and path.
  3. Now create a method under your slack-weather resource. We'll do POST since we're passing the slack token around.
  4. Now you should see a set of radio buttons for Integration type - select Lambda Function, then Lambda Region us-west-2, and enter slackWeather for the Lambda Function name
  5. Hit save and you'll be prompted with a message like "You are about to give API Gateway permission to invoke your Lambda function" - click OK.

Okay, great! Now we have an API endpoint setup. But there is still another important step. Earlier I mentioned that slack POSTs the data as application/x-www-form-urlencoded which is problematic for Lambda because Lambda needs the event to be JSON. API Gateway can't transform the form data to JSON on its own so we need to specify a template. Luckily the awesome community has a solution - specifically a guy that goes by avilewin on the AWS forums came up with a generic freemarker template to transform all the form data to JSON.

  1. Copy template from gist
  2. Go back to your API Gateway -> LambdaTest -> Resources -> /slack-weather -> POST
  3. Click on Integration Request(upper right rectangle)
  4. Click on Mapping Templates
  5. Add mapping Template for "application/x-www-form-urlencoded" and click checkmark
  6. Now change input passthrough to mapping template and paste in template from gist
  7. Click the checkmark to save
  8. IMPORTANT! None of your changes are live until you click the Deploy API button! So click it! You may have to create a new stage(such as prod) the first time you deploy it.
  9. Note the invoke url - should be something like "https://asdf1234.execute-api.us-west-2.amazonaws.com/prod"
// Now the template helps the form data transfrom from this
token=asdfghjklzxcvbnm
team_id=T0001
user_name=Steve
command=/weather
text=90210

// to JSON like this when our Lambda is invoked
{
  "token": "asdfghjklzxcvbnm",
  "team_id": "T0001",
  "user_name": "Steve",
  "command": "/weather",
  "text": 90210
}

Test API Gateway Resource

Now that everything is setup we should be able to simulate the POST to our API that Slack will send. Given an invoke URL such as "https://asdf1234.execute-api.us-west-2.amazonaws.com/prod" with a "/slack-weather" resource we will need to POST our form data to "https://asdf1234.execute-api.us-west-2.amazonaws.com/prod/slack-weather".

// simulate POST to API Gateway which will transform our form data to JSON and pass it to our Lambda and return the result.
$ curl -X POST --data "token=SLACK_SLASH_COMMAND_TOKEN&text=95614" https://xp9k81sf4b.execute-api.us-west-2.amazonaws.com/prod/slack-weather
// -> "Auburn Lake Trails Weather - sky is clear - Now:73 High: 82 Low: 68"

Almost done...

Congrats if you've made it this far. Just one last step really... Go back to the Slack Slash Commands integration page and paste in your API Gateway slack-weather URI(ex: "https://asdf1234.execute-api.us-west-2.amazonaws.com/prod/slack-weather") and make sure the Method is set to POST. Then hit Save Integration and go try it out in slack!

Now what?

You could start off easy by tweaking the message or take a look at the JSON resposne that comes back from the OpenWeatherMap API and add some extra fields to the message like humidity or wind speed. Or learn a little more by taking a stab at implementing all the unit tests that I was too lazy to do.

Conclusion

This is just one small use case that demonstrated how simple it is to deploy your code without having to provision servers. I really think the future of Lambda looks bright! We haven't even scratched the surface of the things that can be accomplished with Lambda and I think it will only get better as Amazon keeps iterating on it!

Feel free to contact me or post down below if you have any comments or questions!