By Ben Downey
Amazon’s recent announcement about AWS Lambda support for Ruby has a lot Rubyists excited. There’s a lot of buzz around serverless architecture. Lambda enables developers to generate apps that can be spun up on request and that evaporate once they’re no longer in use. This makes it ideal for microservices or any small apps that don’t warrant using a beefy framework like Rails.
In this tutorial, we’ll create a basic Slack bot and run it on Lambda. The goal is to get you familiar running small Ruby projects on Lambda by creating something that’s a little more sophisticated then what you’ll find in the AWS tutorial that’s provided in the AWS announcement (linked above).
We’ll write a tiny Sinatra app that performs a basic web request to the Slack API. We’ll wrap the Sinatra app in some code to make it AWS Lambda-friendly. The Slack bot will be simple: when users type a command, we’ll post a button for them to press.
At Mutually Human, like many other consultancies, we track the time spent on projects. It’s easy to forget to do this, so we’ll create a Slack bot to help remind us. Our Slack bot will enable users to type something like a custom slash command like “/logtime” and then see a clickable button that people can use to log their time.
In all honesty, the amount of work required to create this bot isn’t worth the extremely minimal time you’ll save but, hey, it’s a tutorial. We’re just trying to stretch our knowledge of some new technologies.
If you just want to cut to the chase, clone this repo and try it out on Lambda: https://github.com/bnd5k/time_tracker_bot. If you’re ready to go step-by-step, then read on.
Before we get started, we’ll need to get our environment ready for some AWS work. There are 4 things we need to do. If you’ve already completed the AWS tutorial mentioned in the link above, then you can skip this section.
Create an AWS Account
I won’t go into much detail on this one since this tutorial is pretty detailed: https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account.
Create an IAM User
AWS’s IAM manages identities in the AWS ecosystem. We’re going to create an IAM user capable of interacting with Lambda and related AWS services.
IAM offers the ability to create users with fine-grained control over exactly the AWS resources they need. However, for the sake of simplicity, in this tutorial, we’ll just create user with broad admin privileges.
In your browser, navigate to IAM: https://console.aws.amazon.com/iam. Click on “Users” in the side menu and when the next page renders, click the “Add User” button.
On the user creation page, give the user a name and programmatic access to AWS.
On the Set Permissions Page, click on the box marked “Attach existing policies directly” (#1 in the screenshot below). Then find the policy named “AdministratorAccess” and attach it (#2 below).
On the Tags step, do not attach any tags.
On the review step, verify that everything looks correct.
On the final step, you’ll see some information about your new user. Copy the Access Key ID (#1 below) and the Secret Access Key (#2). You’ll need them when we set up the AWS CLI (coming up next).
Install the AWS CLI
If you’re on a mac and using Homebrew, just run “brew install awscli.” If you’re new to Homebrew, it’s a phenomenal package manager. Check out the Homebrew homepage or, if you’d prefet to do this without Homebrew, then follow the steps outlined on the AWS CLI’s Github page, located at https://github.com/aws/aws-cli. The main thing you need to do once the CLI is installed is to configure it. Run “aws configure” in Terminal. This is where you’ll add the Access Key ID and the Secret Access Key from the IAM user you created.
Install the SAM CLI
If you’re on a mac and using Homebrew, just run “brew tap aws/tap” and then “brew install aws-sam-cli”. For more info, see the documentation at https://github.com/awslabs/aws-sam-cli. We’ll talk more about what SAM is and does later.
You’re now ready for AWS development on your local machine. That’s no small feat. And you’re now in a good position to play more with the myriad of services that Amazon provides.
Writing the Ruby Code
Now that our development environment is ready, let’s write some code! There will be two parts: creating the Sinatra app and then making it Lamba-friendly.
Writing the Sinatra app
First, we’ll write a very basic Sinatra app. The Sinatra app will take an incoming request from Slack (triggered when a user types “/logtime”) and then generate a new request back to Slack. The new request will tell slack to add a button with a link in it to whatever channel we’re using.
Let’s start by making the directory for our project.
Next, set the version of Ruby you want. Since AWS Lambda currently only supports Ruby 2.5, pick something in the 2.5.x range. Most Rubyists have many versions of Ruby on their machine and there’s no guarantee that your current Ruby is 2.5.x since that’s currently the only version that AWS Lambda currently supports. If you don’t specify the Ruby version in your Gemfile and you’re running a different version than 2.5 on your local machine, Lambda will see your code but not be able to find any of the code that belongs to your app’s dependencies.
Install the Bundler gem by running “gem install bundler.” Once you have bundler installed, run “bundle init” to create a Gemfile.
The Gemfile that contains our dependencies. Update yours to look like the one below (with the caveat that you may be running a different patch of Ruby 2.5).
Next, install the dependencies. In Terminal, run “bundle install” twice–the second time add the “deployment” flag so that all your dependencies can be found in “vendor” directory at the root of your app. The commands are shown below.
Create a “.env” file that can house the environment variables we’ll use. In my app, I’m using the link for our company’s Harvest site but you can use any URL.
Next, we need to write our app. First, create an “app” directory. We’ll store the Sintra code there. We’ll start with a config.ru file that contains instructions on how to run the app. We’re using the “dotenv” gem to manage environment variables. If you’d prefer to hardcode them in, you’d don’t need the”dotenv/load” below. (My motivation for using “dotenv” is because the repo is stored on GitHub and I’m not a fan of saving environment variables in the code base, especially a public project).
Next, we’ll create an “app.rb”, which will handle web requests. The code here relies on our SlackBot class to make a request to the Slack API. We’ll just return the status of that Slack API request. Ideally, it’s a 200 but if not, our Sintra app will tell us how things went with that Slack API request.
So what does the SlackBot code look like?
Creating an AWS-friendly Sintra App
At this point, we’ve written the core logic of the Slack bot. But now we need to make this code run on Lambda. This involves two files:
- Lambda Handler
- SAM template.
The Lambda Handler will act as an entry point for Lambda to interact with our code. The below is copy/pasted wholesale from the AWS lambda tutorial. The main thing to know about this code is that it’s translating all incoming data and passing it along to an instance of our Sinatra app.
In the root directory, create “lambda.rb.”
Next, we’ll write a SAM template. SAM stands for Serverless Application Model. SAM helps provide Lamda with a blueprint for your project, so it knows what to expect when it runs your code. Your SAM template will have information about what environment you’re running the app in (production vs staging), whether there’s an API endpoint associated with your code, or if it needs to connect with a database. For more info, check this out: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html.
Our template will look very similar to the template in the AWS tutorial. The “template.yaml” need to be in the root directory (rather than the “app” directory where we’ve been storing our Sinatra code).
One difference from the AWS tutorial is that we’re making a POST request (the original tutorial made a GET). In addition, we’re not listing any variables here. The AWS-way would be to specify all environment variables as part of the SAM template. However, since this code will also be available on Github, we’re going to store this code in a .env file that won’t be committed to the git repo.
Create the Slack App
Now that we’re written our Ruby code, it’s time to create a Slack App. We’re going to build a Slack app that enables users to click enter a command in a slack channel and Slack will render a button link. Pressing the link will launch open a tab in the default browser where they can log their time.
In your web browser, head to https://api.slack.com/apps and press “Create an App.” We’ll give the app a name, specify the workspace and press “Create App.”
We’ll configure our bot to accept web requests from our app. Find “Basic Information” on the side menu and click on “Incoming Webhooks.”
This will bring up a new page where you’ll have to turn on the webhooks. Slack defaults this to “off,” so click it to turn it on and then scroll down to the bottom of the page until you find the button marked “Add New Webhook to Workspace.” Click it.
Slack will ask you to authorize the bot to interact with a specific channel. We’ll choose “General.”
Let’s test the Slack app out and verify that it’s working. Look for the section marked “Sample curl request to post to a channel.” On your machine, open Terminal and paste in that command. If everything is working properly, you should see “Hello World” posted to your “General” slack channel.
Back in the Slack App Page, copy the URL from the area marked “Webhook URL.” In your text editor, open the “.env” file and set a SLACK_WEBHOOK_URL variable to be the webhook URL.
Send that Code to AWS!
We’re now ready to package and deploy our code. First, we’ll create an S3 bucket to store our code. Note: using these exact commands will not work for you. Amazon requires bucket names to be unique across the region where they exist. AWS will not permit you to name your bucket “time-tracker-bot” if you anyone else in the region has already done this tutorial and used that bucket name. If you attempt to create a bucket that’s already been created in your selected region, you may end up with an error like this: https://github.com/aws/aws-cli/issues/3567.
Next, we’ll package our app using SAM and push it to our S3 bucket. Packaging involves interpolating some of the values in the “template.yaml” file and storing the interpolated values in a new “packaged-template.yaml” file. Your command will look similar to what’s below, except with your bucket name.
And finally, we’ll use SAM to deploy our code to Lambda. Your stack-name does not have to be the same name as your s3 bucket but I make them the same just for simplicity’s sake. Also, note that specifying CAPABILITY_IAM explicitly permits the template to create IAM resources in your AWS account. The code we’ve written will create an IAM role associated with our app. (This is different than the IAM user you created when setting up the AWS CLI.)
You should see some output that looks like this. Note that your S3 bucket name and stack name will be different than what you see below.
If the SAM packaging or deployment fails and AWS rolls back, you may need to run a command to delete the stack.
In your browser, log into the AWS console and head to the Lambda console and click “Applications” from the left side menu. If you don’t see your application in Lambda > Applications, then double check that you’re looking in the correct region. The region will show in in the top black header of the AWS console, between your name and the “Support” link. In the screenshot below, it’s set to “Oregon.” If you uploaded to an s3 bucket that in a different region then what the AWS console remembers you as last using, then your application won’t appear in the console. Use the drop-down menu next to the Region indicator to find the correct region.
Once you have your application appears in Lambda > Applications, click on it. It should pull up a screen that looks like the image below. You can tell where you’re at by the navigation at the top of the page (see arrow). Under “Resources,” you should see two items: “SintraAPI,” which will route web requests to our Sinatra app and “TimeTrackerBotFunction,” which will run our code.
If you get stuck in this area or any of the next few sections, feel free to jump ahead to the section on troubleshooting (toward the end of this post).
At this point, we’ve got a Sinatra app that’s capable of making web requests to our Slack channel.
Customizing our Slack Bot
Next, let’s register a key command so that users can trigger our app by entering a single command.
In your browser, find the link in the side menu marked “Slash Commands.”
Once on the “Slash Commands” page, click “Create New Command.” Enter “/logtime” as the command.
To populate the Request URL, we’ll need the URL associated with our Sintra App. Our SAM template indicated that our app has a serverless API endpoint. We just need to get the URL associated with that endpoint and then Slack will know where to make its POST request when someone uses the custom slash command.
In your AWS console, head to Lambda > Applications > time-tracker-bot (which is where we left off in the previous step).
Scroll down to “Resources” and click on “SinatraAPI.” This will launch the AWS API Gateway.
Once you’re in the API Gateway, find “Dashboard” on the side menu and click it (See #1 in the screenshot below.). At the top of the page, find the URL for your app (See #2.).
Copy this URL over to the browser window where you’re adding the slash command. Paste in the AWS API Gateway URL as the Slack Request URL.
Your Slack command form will look similar to this.
Once you click “Save,” the Slack app will be complete.f
Slack may ask you to re-authorize your app since it now also does contains a slash command. Go ahead and do this.
Putting it all Together
At this point, everything should be set up so let’s test it out and see if it works as expected. In Slack, head to the General channel and type “/logtime”. We should see something like this.
If you click the “Log Time” button, the link should open in their browser. On my app, it looks like this.
Internally, here’s what’s happening. Slack Recognizes the “/logtime” custom slash command and makes a POST request to the URL we specified–the URL listed on the API Gateway. When the request comes into AWS, it’s routed to Lambda, which spins up our app. Lambda then runs the “handler” method found in our “lambda.rb” file. The code in there runs our Sinatra app. Our Sinatra app makes a POST request over to the Slack API. That request contains a payload that described how to generate a link button in the General Slack channel. And after that request completes, our app just disappears. There’s no more uptime until another request comes through.
I don’t think I’ve ever followed a tutorial where things didn’t go awry at some point. If you’ve been following this tutorial and it it’s not going how you’d expect, then you can lean on AWS to help out. If you throw a print statement in your code and then run your slash command, you scour the logs on CloudWatch. The AWS console is packed with stuff and getting to the logs you need is not exactly intuitive. The screenshots below explain how to get to them from the API Gateway dashboard. But if you get lost, you can always use the AWS console search bar.
To get to the Cloudwatch logs, head to Lambda > Applications > time-tracker-bot. Scroll down to “Resources” and click on “SinatraAPI.” Once you’re on the AWS API Gateway, find the “Dashboard” on the side menu and click it (See #1 below). Click on the API Calls graph (See #2.).
Next, click on Logs on the side menu.
Under the Low Groups, click the relevant record (e.g “time-tracker-bot-TimeTrackerBotFunction-2OXFQOLZ3188”).
That should render a page showing our logs, which now contain our “print” statements. Note that if you have any errors (i.e. not just your own “print” statements), they will also appear in these logs.
Congrats! You have successfully created a Slack bot that is fueled by Ruby code run on AWS Lambda! Although this is a pretty basic bot, we touched a lot of different technologies: Lambda, API Gateway, CloudWatch, SAM, and Slack’s API system. There’s a lot more to learn in all of these areas, but now at least you’re comfortable with how these work and interact with each other. Happy developing!