Event that triggers bot actions.

Remark: We are only interested in sending and receiving messages to slack channels, in this scenario bot will not be handling messages sent in PM.

Phase 1: Teaching bot how to walk and talk.

At this stage our bot is a tabula rasa, newborn baby, and, as it happens with babies, we obviously need to teach him how to do some cool features for us.

For example, let’s teach “him” how to show, start, stop, restart AWS EC2 instances. And just for fun lets also add a functionality to get smart in replies when we send bot a messages with too many exclamation marks.

At this stage we have our “superbot” configured and its available in our slack channel. So far AWS side configuration is really simple and intuitive. There we have to create and configure a couple of components.

API Gateway.

Lambda Function (Python 3.6)

Scheduled CloudWatch event. I’ll get back to this one later.

Here (Lambda) we are going to have our Python code with slack event handler. We don’t need to import any Python modules that are not installed on AWS, so we will edit our code in built-in editor on Lambda.

Lambda function

Create a new blank Python 3.6 function. Give it a name, something like bot_event handler. Name handler “index.handler”

Add the following environment variables

BOT_TOKEN : Bot User OAuth Access Token taken from ”OAuth & Permissions” slack app settings page. We use this to authenticate our bot in slack. (Outgoing)

Bot User OAuth Access Token taken from ”OAuth & Permissions” slack app settings page. We use this to authenticate our bot in slack. (Outgoing) slack_token : Bot verification key taken from “Basic Information” slack app settings page. Required to verify that requests are actually coming from Slack. (Incomming)

Bot verification key taken from “Basic Information” slack app settings page. Required to verify that requests are actually coming from Slack. (Incomming) aws_access_key_id : Key ID of AWS account with sufficient roles

Key ID of AWS account with sufficient roles aws_secret_access_key : Secret key of above mentioned AWS user

Secret key of above mentioned AWS user region_name : AWS region, where we want our bot to do some cool stuff for us.

Remark: Here you might want to use AWS KMS to encrypt/decrypt values of environment variables. For this function I use clear text values but for another one, later in this blog post, I’m going to make use of AWS KMS.

Lambda function environment variables

As for the function code (index.py) I wrote the following handler, it’s permanently available on my team’s GitHub repo

Bot_Event_Handler.py

Let’s pay attention to some moment in this code.

Importing modules. The one we focus on here is BOTO3, that allows us to interact with AWS services.

We parse JSON payload sent from Slack to our Lambda function through API Gateway resource. We need to have the first ‘ if ‘ (Line#27) to pass event subscription URL verification on slack setting page. We are going to deal with that a little bit later, just don’t give up reading.

Line#61: Here we do kinda important verification:

Message is sent to slack channel by real user (not a slack bot). Just for fun you can try to remove this one and enjoy watching a bot talking to himself in infinite loop in your Slack channel.

Message starts with ‘ bot ’ keywords

’ keywords Token received in JSON payload matches slack_token environment variable.

If at least on of these conditions returns False, message is ignored and not processed by bot event handler. Remark: If bot doesn’t return anything to you channel doesn’t mean it doesn’t “read” what you’ve sent to Slack channel. Lambda function is invoked on all messages with no exceptions and all further verifications are happening inside a handler code. Use already mentioned slash commands if you want to save some $.

Line#68: Just for fun, we create a tuple of possible messages bot will send as a reply if we send him a message with more then one exclamation mark in a row. As a possible option here we can store all variations of reply messages in DynamoDB at Amazon and randomly pull one from there.

If we look a little up in the code we have defined a function “verb” that uses BOTO3 module to do some actions with AWS EC2 instance (Start/Stop/Restart) and returns result based on what’s happened inside the function.

The rest are showing EC2 instance (BOTO3), parsing, pattern matching, firing up messages back to slack (appending BOT_TOKEN env variable), and all other stuff required to cover all bugs discovered during an anti-humanistic testing and testing by dividing by zero, etc. 🙂

API Gateway

This one is going to be very simple.

Create new API, let’s say named “bot-trigger”

Create new resource, I named mine “superbot”

Create POST method withing early created resource. Integration type should be “Lambda Function” . Select Lambda region, then just start typing name on newly created function and select the one that popped up, Save -> OK.

. Select Lambda region, then just start typing name on newly created function and select the one that popped up, Save -> OK. Don’t forget to deploy API to a new (if this is a first time you are deploying API) stage once you are done configuring it’s resources and methods.

Go to “Stages” -> “name_of_the_stage” and note down Invoke URL, we gonna need it in a next step.

And some screenshots:

API Post method workflow

Stages and Invoke URL

Finishing slack configuration

Done configuring AWS side (at this stage we think so at this moment), one minor thing is still left to make the whole process work.

Lets get back to Slack app settings page, go to “Event Subscriptions”, enable it and paste Invoke URL we just created in AWS Api Gateway.

What happens under the hood here is Slack fires up HTTP POST request to a URL we entered. It sends verification token and randomly generated “challenge”, it expects to get value of “challenge” back in a body of HTTPS reply. This verification message looks like this.

{

"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",

"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",

"type": "url_verification"

}

Remember we have mentioned “challenge” in Line#27 of our code, so now we understand why we need that.

Keep lambda function warm or Postman Always Rings Twice

Assuming we passed verification of subscription URL and finally we should be good to go with all the setup and bot is ready to accept commands, furthermore do some actions and send us answers. Lets send a first message, and let screenshots talk for us:

First reply from bot

Second reply from bot

Just like “Postman Always Rings Twice” our COLD bot always replies twice. The next message we send to bot got reaction from bot only once. And this is not what we really like and what we really want to have due to a number of reasons.

After some research I found it to be a Lambda Function warmup problem. Such Lambda behavior is well described in this blog post by Sam Corcos.

So I set rule in CloudWatch Events that pings my function every 15 minutes, and in this way always keeps it warm.

CloudWatch Events rule

Problem solved and my little bot finally works as expected.

Phase1: Finally done, bot, what can you do?

Let me send my bot some messages and post screenshots here. Show AWS instances in a region specified in environment variables of Lambda Function

List instances

Ok we can see an instance is stopped, let’s start it up.

Oops, I forgot a syntax of “Start” command, and AWS could not find an instance with instaceID = “instance”. At least we can see that exceptions handling worked fine. Trying over

Lets see if it has really started up

Running instance

OK, start command worked. Should we try “dividing by zero and sending command “restart” with no arguments?

Restart/start/stop command syntax

Lets fire up some non existing command.

Listing known commands

And at the end that “getting smart” functionality.