Serverless Slack Bot using AWS Chalice

By Paweł Hajduk | October 30, 2018

Slack Bot using AWS Chalice

Today I would like to show you how to build Slack Bot using serverless approach on AWS infrastructure. We are going to support our efforts using AWS Chalice framework. Our Slack Bot is going to be a dummy one. It will respond with a message we send to it. However this article is not about implementing sophisticated bot behavior. We want to setup whole stack which will be a foundation for further development.

Introduction

AWS lambda in chatbot context

“Why should I use AWS lambda to implement chatbot?” you might ask. Well, chatbots respond to events. Each event could be a message sent by a human being, or it could be a scheduled action which triggers sending a message to human beings without being asked. AWS Lambda is an ideal case for it as is supports both cases.

AWS infrastructure and Chalice framework

Chalice is a framework developed by AWS which helps writing serverless REST APIs. It is similar to Flask. The main advantage of Chalice is that we focus on writing Python code, which represents our API. Chalice handles deployment to AWS Lambda and API Gateway for us. It also has handy features like local development, printing API URL, deleting whole stack etc.

Thanks to Chalice, we will not touch AWS CLI nor AWS web console. However in this blog post, I assumed that you already have AWS account and you have your AWS CLI set. Chalice will use it indirectly. If you do not have it, please follow this tutorial in order to set it up.

Proposed AWS solution

How our solution is going to work:

Basicaly we need to divide our problem into two smaller tasks:

  • Task#1 Slack Bot definition using Slack UI
  • Task#2 AWS infrastructure definition using AWS Chalice framework

Step #1: Slack Bot definition

At the very beginning we need to define our Slack Bot. It’s custom thing so it can be found under Customize Slack option in Slack main menu.

Bots, similarly to custom integrations, in Slack nomenclature are called apps so please click Configure apps
In the new screen with apps, instead of searching for custom integration, we want to build new app. You can find the following banner in the upper right corner.
After this step, you will see a new fancy Slack screen where you should click Start Building button in the middle.
We landed in a new place with probably most difficult step… you have to name your bot :) We called our test bot Bender.
At this stage, our bot should be defined and we need to extract from Slack a security token. It will allow us to call Slack API. Under OAuth & Permissions you have to click Install App to Workspace. This will generate an unique token which will be used by the lambda.
Copy the Bot User OAuth Access Token which can be found inside OAuth & Permissions section:
At this point please do not close Slack web UI, we will come back here in a minute. Generated Bot User OAuth Access Token will be used in the next step.

Step #2: AWS Chalice code

Virtual environment preparation

Let’s prepare our Python3 dev environment using virtualenv for Chalice and AWS Lambda:

> mkdir benderbot
> cd benderbot
> virtualenv --python $(which python3) venv
> source venv/bin/activate
> pip install chalice
> chalice new-project benderbot
> cd benderbot

Coding solution

Under benderbot/app.py:

import urllib
from chalice import Chalice

app = Chalice(app_name='benderbot')
app.debug = True
BOT_OAUTH_TOKEN = "xoxb-048444305-....set_your_token_as_this_const"

@app.route('/', methods=['POST'])
def event_handler():
    request = app.current_request
    if "challenge" in request.json_body:
        return request.json_body["challenge"]
    slack_event = request.json_body['event']
    if "bot_id" in slack_event:
        logging.warn("Ignore integration bot message")
    else:
        text = slack_event["text"]
        response = "Bender says: " + text
        respond(response, slack_event)

    return "200 OK"


def respond(response, slack_event):
    channel_id = slack_event["channel"]
    data = urllib.parse.urlencode(
        (
            ("token", BOT_OAUTH_TOKEN),
            ("channel", channel_id),
            ("text", response)
        )
    )
    data = data.encode("ascii")
    request = urllib.request.Request(
        "https://slack.com/api/chat.postMessage",
        data=data,
        method="POST"
    )
    request.add_header(
        "Content-Type",
        "application/x-www-form-urlencoded"
    )
    urllib.request.urlopen(request).read()

Let’s break our solution down into smaller pieces:

import urllib
from chalice import Chalice

app = Chalice(app_name='benderbot')
app.debug = True

Besides boring imports, we define Chalice app and enable debugging. We will not debug our solution in this tutorial but you may want to do this during some feature development. I have used it here for reference.

BOT_OAUTH_TOKEN = "xoxb-048444305-....set_your_token_as_this_const"
This is your token which Slack generated for you in Slack web UI. Paste your token here.

I skipped obvious parts as method definition and extracting current request. Pretty interesting part is the following one:

  if "challenge" in request.json_body:
      return request.json_body["challenge"]
To ensure that events are being delivered to a server under your direct control, Slack must verify your ownership by issuing you a challenge request. This behavior is documented in official Slack documentation.

  if "bot_id" in slack_event:
      logging.warn("Ignore integration bot message")
Our bot is going to listen for all incoming messages. Sometimes other Slack bots will send messages, so we want to ignore those messages.

Warning: if you omit this part, your slack bot will respond to its own messages. It will fire a recursive loop of lambda calls.

  text = slack_event["text"]
  response = "Bender says: " + text
  respond(response, slack_event)
Above code and respond method itself are self explaining. We are sending simple HTTP request to Slack API.

Deployment

OK so at this point we have our simple bot implemented. Now it is time to deploy it to the cloud:

> chalice deploy
Creating deployment package.
Updating policy for IAM role: benderbot-dev
Creating lambda function: benderbot-dev
Creating Rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:us-east-1:<our_account_id>:function:benderbot-dev
  - Rest API URL: https://jqjrwn2kpd.execute-api.us-east-1.amazonaws.com
That’s all. We have deployed our code into serverless infrastructure. In order to check the URL for our API you can use following handy Chalice call:
> chalice url
https://jqjrwn2kp.execute-api.us-east-1.amazonaws.com/api/

Step #3: Connecting AWS with Slack API

In the first step, we defined our Slack Bot in Slack web UI. Then, in step 2, we implemented bot and deployed it into AWS Gateway and Lambda. Now it’s time to make those two talk to each other.

We need to enable events which will be sent to our Lambda code. Back in Slack web UI select Event Subscriptions and enable events:

In the Request URL you should paste output from chalice url command. After you paste it, the green mark should indicate that you setup everything correctly.

Now it’s time to setup events. We’ll instruct Slack to send events to API Gateway. In our case, those will be direct messages. As an event select message.im and save changes:

Let make some test:
It worked! Bender responded as expected. You can extend your lambda by some additional functionality.

Bonus: advanced monitoring

We used following statement in our code:

app.debug = True
This enables logging in Chalice framework and we can check logs in CloudWatch. Additionally, in API Gateway settings we can enable detailed logging of API itself:
With logging enabled in Chalice and API Gateway, you will be able to debug your serverless stack easily.

Now go and write some bots!

Make ChatOps and DevOps a Competetive Advantage of Your Business

ChatOps and Serverless are our core expertise. Partner with our experienced and pragmatic builders that help you innovate move quickly and be cost-effective with use of cloud platform and chatbots.

Schedule a call with our expert
comments powered by Disqus