Vote for Pizza with Slack: Python in AWS Lambda

We knew this day would come and we :heart: it. Amazon now supports Python in Lambda.

When we're not building Fugue, frequenting our favorite place for burgers and wings (Rex's Downtown Grill FTW!), or finalizing our plans to colonize Mars, we write Slack bots.

We have a few Slack bots sitting on EC2 instances managed by Fugue but nothing running in Lambda... yet. Our goal for this project:

  • User types a keyword into Slack
  • Slack POSTs to a URL hosted on AWS API Gateway, backed by Lambda
  • Votebot posts voting ballot to Slack, ready for votes via reaction emoji
  • User closes the ballot and bot tallies then posts the results.

In short, this:

Screenshot of Votebot starting up in Slack

(More options were available, but not shown in this screenshot.) Then, once we've battled over which pizza is best, we close voting and votebot does the rest!

Screenshot of Votebot results in Slack

Note: Because votebot seeded each option with a pizza emoji, the totals are (votes - 1) to reflect votes cast by humans.

Meet Our New Bot Overlord

We love pizza. Near Fugue HQ, there are a few good pizza places. It's easy to come to a consensus about whether we're in the mood for NY style or the quirkier toppings of the brick oven pizza place. The hard part is figuring out how many people want meats vs. veggies vs. white pizza and then extrapolating that into a number of pies to order.

Our original plan was just dumping our requests into Slack and letting some unlucky soul figure it out. When Slack released reaction emoji, it was a bit easier to put the pizza options to a vote. But someone still had to enter the menu options line by line manually… like a caveman. (And if they’re paleo, they’re not even eating pizza.)

Thankfully, the Slack API allows bots to post reaction emoji too. So we should be able to craft a bot that allows us to post the pizza list with a seed vote already attached to each option. Here's our shopping list:

  • A Slack Outgoing Webhook
  • A Lambda function written in Python
  • A way to communicate with the Slack API
  • A way to communicate with the AWS control plane

The Design

Using the project goals above, we can design votebot. Long running functions are one possibility but not necessary. Plus, we don't want a function to spin and run up our bill! We will post a ballot when triggered and on closing, retrieve the ballot. No thumb twiddling necessary. Rather than design this as a pizza-exclusive bot, we'll create a general use bot. Then we can call votebot for voting on pizza, other take-out restaurants, party snacks, board games, or anything we want to vote on. (Spoiler: We're going to use this for planning poker.)

We also don't want to update the code every time we want to add a new pizza place or thing we want to vote on. So we'll use DynamoDB to store pizza places and options.

To get data out of Slack, we'll use an Outgoing Webhook. Set this up first and enter a dummy URL. Be sure to grab the token because we'll use this in our code. You can use any trigger word you want, but I've set our company Slack to votebot. The description, custom name, and icon are all up to you. Naturally I used votebot for the bot's name.

The other piece of Slack to set up is a bot integration. This will get you access to the Slack Real Time Messaging API for bots. Once created, grab your API token; you'll need this for authentication.

DynamoDB Table

We're going to set up a simple DynamoDB table for this bot called vote-options. The hash key will simply be called selection. The key we'll use for the options (such as which pizzas to allow everyone to vote on) will be options. The read and write throughput is set to 1. This can be tuned later if necessary.

Code

We'll get data out of Slack by using the Outgoing Webhooks, so now let's look at how to get data back into Slack.

You can do this directly using the Python requests module and the Slack API directly. I'm a fan of slacker, which is available on PyPi.

The last thing we need is a way to talk to AWS. For that, we'll use boto3.

Got that? Awesome. Now we can construct our bot!

Unfortunately, non-humans may be throttled. For now, we will work around this with sleep(). In a future version, we will use the information returned from Slack to perform adequate retries to avoid throttling.

The last step is packaging. Our project votebot has two dependencies: boto3 and slacker. Each has dependencies of its own. Lambda includes the latest version of boto3 and its dependencies. Note: If you need to use a previous version, you must include it with your deployment package. slacker has one dependency, requests. There are a couple of options for packaging, but for this we'll simply pip install the module to the directory where the code lives.

[:~] cd votebot
[:~/votebot] ls
lambda_module.py
requirements.txt
SLACK_BOT_API_TOKEN
SLACK_CHANNEL_TOKEN
[:~/votebot] pip install -r requirements.txt -t .
...
[:~/votebot] ls
lambda_module.py
requests-2.8.0.dist-info
slacker-0.7.3-py2.7.egg-info
requests
slacker
requirements.txt
SLACK_BOT_API_TOKEN
SLACK_CHANNEL_TOKEN

From here, we put all this stuff into a zip file and we have our deployment package! Remember:

[:~/votebot] zip -r lambda.zip *
...
[:~/votebot] ls
lambda.zip
requests
slacker
lambda_module.py
requests-2.8.0.dist-info
slacker-0.7.3-py2.7.egg-info
requirements.txt
SLACK_BOT_API_TOKEN
SLACK_CHANNEL_TOKEN

Deploying

Before deploying, you will need to create the IAM role for your code. There are two things you need - the policy and trust relationship. The policy will need to include DynamoDB permissions for the tables we need. Those are vote-options and vote-open. At some point we may want to create/delete the tables in code, so we'll just restrict the permissions to these tables but allow all operations. We will also allow CloudWatch logs since I have my function logging there now. You can use this or not. It's up to you.

The trust relationship needs to use sts:AssumeRole for the lambda.amazonaws.com service.

Now we can create the Lambda function. Skip the blueprint. Set your function up like the following screenshot, then upload the .zip file you previously created.

Screenshot of AWS Console - How to create a Lambda function

Frontend'ing

When configuring Slack for an integration, the request body is sent as the Content-Type application/x-www-form-urlencoded. This is a problem because the API Gateway currently expects a JSON document. Both are pretty strict with these settings at the time of this writing. Fortunately, there is a workaround. We can use the magic $input variable at the API Gateway to turn our request into a string that we can parse in code.

After setting up your API Gateway entry, open the POST method for the resource (this one is called vote). Click Integration Request and under Mapping Templates enter application/x-www-form-urlencoded as the Content-Type and edit the mapping template to include:

{
  "formparams" : $input.json("$")
}

Screenshot of AWS Console - Configuring API Gateway

Now when your Lambda function executes, event will contain the key formparams.

Next Steps

Now that we've made a neat and tidy way to vote for our favorite pizzas and other stuff, here are some features we'd like to add to this bot:

  • Pizza math or some kind of fuzziness: Using the number of votes and the servings per pie for a particular restaurant, Pizzabot suggests how many of each of the top scoring pies to order.
  • Edit table entries: A way (through Slack) to add/remove/edit entries from the DynamoDB table that hold our voting options. (Example: /votebot flippin add Bacon Explosion Pizza)
  • Voting window: When someone opens voting, create a Lambda timer to fire 15 minutes after voting opens. This will close voting and write the voting results to the channel. As of now there isn't a documented API call to do this.
  • Menu URLs: For things with an online menu (like pizza!) we'd like to display the URL when voting opens.

Check It Out!

You can get the code for Votebot on Github. Let us know what you think!

Want to learn more about Lambda? AWS is offering a webinar entitled AWS Lambda Best Practices: Python, Scheduled Jobs, and More on October 29, 2015 at noon EST.

Go Fast. See Everything.
Get Cloud Right.