February 15, 2017 Yu Ling Cheng 13 min read

I always feel guilty when I suddenly motivate myself to go to the gym, then have all the painful thoughts like going out in the cold, being sweaty and feeling stiff afterward and decide that I'd rather stay in bed watching my favorite series.

I have the same mixed feelings when I get an idea of the project and then get discouraged —even before getting started— thinking about having to provision a server and deploy my code to see it live and used by others than myself.

But recently, I discovered Serverless, a framework based on Amazon Lambda that helps you deploy your code in seconds.

The framework is said to relieve Lambda functions of its main pain points (the AWS console, the heavy configuration), to allow developers to work with more familiar standards.

Looks like a really nice promise that would dismiss all my excuses not to go on with any of my ideas:

Focus on coding, deploy single functions in the cloud

Don't manage any server. AWS handles provisioning and scaling

Pay only when the functions are running

I decided to test it on a fun project and experience how promising it actually is.

I ended up creating a chatbot game on Facebook Messenger, to help my colleagues learn the name of everybody in the company.

I started with this tutorial: Building a Facebook Messenger Chatbot with Serverless, which quickly allowed me to play with my phone talking to my chatbot.

But I have to admit I had to struggle a little to fully understand all the magic behind the framework and diverge from the tutorial to do what I wanted.

In this article, you'll find:

What cool projects can you do with Serverless?

Lambda functions are handy for:

A cron job running without having a full server dedicated to it Ex: a custom IFTTT or Zapier

An automatic data processing job Ex: create thumbnails for profile pictures uploaded to your website

Funnier: a backend for a chatbot

Building a chatbot is a great mean to test an idea and develop an MVP.

The advantage is that you only have to focus on the backend, since the frontend and delivery is granted by the messaging service you'll be using.

And with Serverless,

Only code the logic of the bot

Quickly iterate as deploying is super fast

Spend little money while testing

Keep focusing on your service if you get successful as AWS handles scaling

Having a Chatbot Running in Prod in 15 Minutes with Serverless

Requirements

Before starting the tutorial, make sure you have:

An account set on AWS ~3min + 24h validation Be aware that a credit card is required to sign up You'll have the free tier for one year You'll need to wait 24 hours to have your account validated Be patient, you can watch your favorite series or go to the gym while you wait ;)

Node v4 or higher to install Serverless ~1min npm install -g serverless will do the job

The API Key & Secret of an IAM user ~2min With programmatic access and AdministratorAccess. The paragraph Creating AWS Access Keys in the Serverless doc is fairly explicit for that.

Configured Serverless with your AWS credentials ~1min I recommend using the serverless config credentials command.

You'll avoid having to install aws-cli or managing environment variables

For the chatbot, a Facebook Developer account ~1min 3min if you don't have a Facebook account yet

For the chatbot, a Facebook page that you own ~2min The page gives an identity to your chatbot, you can't have one without it.

Tutorial

Init your project

$ sls create --template aws-nodejs --path my-first-chatbot

This creates two files in the directory my-first-chatbot :

├── my-first-chatbot │ ├── handler.js │ └── serverless.yml

I used Node.js for my bot. If you prefer Python, use aws-python instead.

First take a look at the serverless.yml file.

It is the configuration file of your project.

Lots of options are commented in the file, all you need for now is the following:

service : my - first - chatbot provider : name : aws runtime : nodejs4.3 region : eu - central - 1 functions : hello : handler : handler.hello

So far you have declared the hello Lambda function which will be deployed somewhere in the Frankfort AWS cloud.

You can already invoke it locally from your shell to check that it works:

$ sls invoke local -f hello { "statusCode" : 200 , "body" : "{ \" message \" : \" Go Serverless v1.0! Your function executed successfully! \" , \" input \" : \" \" }" }

Notice that you can pass an input when you invoke your Lambda function, either inline or with a .json or .yml file

$ sls invoke local -f hello -d "my data" { "statusCode" : 200 , "body" : "{ \" message \" : \" Go Serverless v1.0! Your function executed successfully! \" , \" input \" : \" my data \" }" } $ sls invoke local -f hello -p "path_to_my_data_file.yml" { "statusCode" : 200 , "body" : "{ \" message \" : \" Go Serverless v1.0! Your function executed successfully! \" , \" input \" : \" { \" data \" : \" Content of my data file as json \" } \" }" }

Now you can take a look at the handler.js file to see that the hello function simply returns a JSON 200 response.

'use strict' ; module . exports . hello = ( event , context , callback ) => { const response = { statusCode : 200 , body : JSON . stringify ( { message : 'Go Serverless v1.0! Your function executed successfully!' , input : event , } ) , } ; callback ( null , response ) ; } ;

The event variable contains all data from the event that triggered your function.

variable contains all data from the event that triggered your function. The context variable contains runtime information of the Lambda function that is executing.

We won't need it here but if you are curious you can check the documentation about the context object on AWS

Code the logic to communicate with your Facebook chat.

You need a webhook (aka web callback or HTTP push API) to first exchange credentials with your Messenger app so that you can start receiving events from it (incoming messages, postback ...) and responding to them.

Credentials exchange is done through an HTTP GET event set for your Lambda function.

The HTTP GET event requires an endpoint.

Luckily, Serverless allows you to create one simply by writing a few lines of configuration.

Rename your hello function to webhook and add the following config to your serverless.yml :

... functions : webhook : handler : handler.webhook events : - http : path : webook method : GET integration : Lambda

Then update your handler.js file to enable authorisation:

module . exports . webhook = ( event , context , callback ) => { if ( event . method === 'GET' ) { if ( event . query [ 'hub.verify_token' ] === 'SECRET_TOKEN' && event . query [ 'hub.challenge' ] ) { return callback ( null , parseInt ( event . query [ 'hub.challenge' ] ) ) ; } else { const response = { statusCode : 403 , body : JSON . stringify ( { message : 'Invalid Token' , input : event , } ) , } ; return callback ( null , response ) ; } } else { const response = { statusCode : 400 , body : JSON . stringify ( { message : 'Bad Request' , input : event , } ) , } ; return callback ( null , response ) ; } } ;

Don't forget to rename your exported Lambda function webhook !

! Make sure to choose a strong SECRET_TOKEN

It is the token that you will have to declare to your Messenger app to enable communication with the chat

It is the token that you will have to declare to your Messenger app to enable communication with the chat The hub.challenge is an integer code that Messenger sends you along with the token

Test your handler locally:

$ sls invoke local -f webhook -p -d "{ \" method \" : \" GET \" , \" query \" :{ \" hub.verify_token \" : \" SECRET_TOKEN \" , \" hub.challenge \" :123456}}" 123456 $ sls invoke local -f webhook -p -d "{ \" method \" : \" GET \" , \" query \" :{ \" hub.verify_token \" : \" BAD_TOKEN \" , \" hub.challenge \" :123456}}" { "statusCode" : 403 , "body" : "{ \" message \" : \" Invalid Token \" , \" input \" :{ \" method \" : \" GET \" , \" query \" :{ \" hub.verify_token \" : \" BAD_TOKEN \" , \" hub.challenge \" :123456}}}" }

I recommend you create .yml or .json files to invoke your Lambda function locally. It will make your life easier :)

Now that you'll be able to receive events from Messenger, let's update your Lambda function to actually handle them.

Add HTTP POST config to your serverless.yml :

... functions : webhook : handler : handler.webhook events : - http : path : webook method : GET integration : Lambda - http : path : webook method : POST integration : Lambda

To handle the POST requests we will need some more preparation:

Create your Messenger app For that, Create an app from your Facebook developer account Add the Messenger product to your app

You can access all Facebook products from the left menu "Add a product"

Get a page token to be able to post messages on behalf of your page (and have your chatbot respond automatically) Once you have added Messenger to your app, configure Messenger parameters (accessible from left menu also): Under "Token Generation" , select your Facebook page Grant access to it with your Facebook account Save the token you get for later

Add axios to your project to be able to send responses from your bot $ npm install axios

Now you can edit your `handler.js`:

const axios = require ( 'axios' ) ; const fbPageToken = 'YOUR_FACEBOOK_PAGE_TOKEN' ; const fbPageUrl = ` https://graph.facebook.com/v2.6/me/messages?access_token= ${ fbPageToken } ` ; module . exports . webhook = ( event , context , callback ) => { if ( event . method === 'GET' ) { } else if ( event . method === 'POST' && event . body . entry ) { event . body . entry . map ( ( entry ) => { entry . messaging . map ( ( messagingItem ) => { const senderId = messagingItem . sender . id ; if ( messagingItem . message && messagingItem . message . text ) { const payload = { recipient : { id : senderId } , message : { text : ` You say " ${ messagingItem . message . text } ", I say: Hi, let's chat :) ` } } ; axios . post ( fbPageUrl , payload ) . then ( ( response ) => { response = { statusCode : response . status , body : JSON . stringify ( { message : response . statusText , input : event , } ) , } ; return callback ( null , response ) ; } ) . catch ( ( error ) => { const response = { statusCode : error . response . status , body : JSON . stringify ( { message : error . response . statusText , input : event , } ) , } ; return callback ( null , response ) ; } ) ; } } ) ; } ) ; } else { } } ;

You can try to call your Lambda locally, but you won't be able to get a successful response unless you know a real sender ID.

$ sls invoke local -f webhook -d "{ \" method \" : \" POST \" , \" body \" :{ \" entry \" :[{ \" messaging \" :[{ \" sender \" :{ \" id \" : \" YOUR_SENDER_ID \" }, \" message \" :{ \" text \" : \" Hello \" }}]}]}}" { "statusCode" : 400 , "body" : "{ \" message \" : \" Bad Request \" , \" input \" :{ \" method \" : \" POST \" , \" body \" :{ \" entry \" :[{ \" messaging \" :[{ \" sender \" :{ \" id \" : \" YOUR_SENDER_ID \" }, \" message \" :{ \" text \" : \" Hello \" }}]}]}}}" }

This means it is time to deploy your project for the first time!

Deploy

As easy as:

$ sls deploy

You'll see the following logs appear:

Serverless: Packaging service .. . Serverless: Uploading CloudFormation file to S3 .. . Serverless: Uploading service .zip file to S3 ( 134.73 KB ) .. . Serverless: Updating Stack .. . Serverless: Checking Stack update progress .. . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. . Serverless: Stack update finished .. . Service Information service: my-first-chatbot stage: dev region: eu-central-1 api keys: None endpoints: GET - https://ENDPOINT_ID.execute-api.eu-central-1.amazonaws.com/dev/webook POST - https://ENDPOINT_ID.execute-api.eu-central-1.amazonaws.com/dev/webook functions: my-first-chatbot-dev-webhook: arn:aws:Lambda:eu-central-1:ID:function:my-first-chatbot-dev-webhook

Congratulations, your webhook is now available from anywhere!

What happened exactly?

Notice that you have a new folder in your project directory:

├── my-first-chatbot │ ├── .serverless │ │ ├── cloudformation-template-create-stack.json │ │ ├── cloudformation-template-update-stack.json │ │ └── my-first-chatbot.zip │ ├── node_modules │ ├── handler.js │ └── serverless.yml

Let's examine the first half of the logs to understand:

Serverless reads your serverless.yml file to create two CloudFormation files in the directory .serverless : one to create a CloudFormation on AWS through your AWS account one to create all the AWS resources you need to have your Lambda function working (here it includes the two API Gateway endpoints you need for your webhook and other credentials settings)

Serverless packaged all the files in your directory except the serverless.yml file, zip it to .serverless/my-first-chatbot.zip

file, zip it to Serverless then uploads the new files created to an S3 Bucket in the region specified in your serverless.yml and creates or update all the resources listed in the CloudFormation update file (including the Lambda function of course)

What you can do now:

Invoke your deployed Lambda function $ sls invoke -f webhook -p -d "{ \" method \" : \" GET \" , \" query \" :{ \" hub.verify_token \" : \" SECRET_TOKEN \" , \" hub.challenge \" :123456}}" $ sls invoke -f webhook -p -d "{ \" method \" : \" GET \" , \" query \" :{ \" hub.verify_token \" : \" BAD_TOKEN \" , \" hub.challenge \" :123456}}" $ sls invoke -f webhook -d "{ \" method \" : \" POST \" , \" body \" :{ \" entry \" :[{ \" messaging \" :[{ \" sender \" :{ \" id \" : \" YOUR_SENDER_ID \" }, \" message \" :{ \" text \" : \" Hello \" }}]}]}}"

Test that your Lambda function is triggered when there is a call to one of the endpoints that were just created Use curl for instance to query the endpoint https://ENDPOINT\_ID.execute-api.eu-central-1.amazonaws.com/dev/webook

Better, test your chatbot live!

Try it! Send your first message to your chatbot

Final settings for your Messenger app:

Configure Messenger parameters to "Setup Webhooks" under "Webhooks" now that your endpoint is available

under now that your endpoint is available Use the endpoint url as "Callback URL" and your SECRET_TOKEN

and your Subscribe to messages and other Messenger events you might want to handle

and other Messenger events you might want to handle "Verify and Save" : Facebook will call the GET endpoint with the token your gave him to subscribe your webhook to the app

: Facebook will call the GET endpoint with the token your gave him to subscribe your webhook to the app Once done, in the same "Webhook" section, select your Facebook page for subscription: you'll now listen to the events incoming from this page

Now for the chatbot to send you automatic messages, you need to start the conversation first (otherwise you'll get a 403)

If your page is public, find your page in Messenger and send your first message!

If not, go to the Facebook page and start a conversation from there

Your app is now in prod!

You can start iterating. Facebook allows you to grant permission to testers to use your app before it is validated and available by anyone.

Benchmarking MVP options

Pros and cons

I rated Serverless, EC2, and Heroku based on three criteria:

Serverless

EC2

Heroku

Scalability

++

+

-

Customization and services

+

++

-

Ease of use

+

+

++

On Heroku,

You need to configure manually the scale of your infrastructure

You have fewer integrations than on AWS

But it is more user-friendly than AWS EC2

You have less new concepts to understand than Serverless

On the other hand, once you get used to Serverless or EC2s, you can implement your service faster and more easily.

Pricing

I'll consider two scenarios:

The custom IFFT: low traffic and light computing memory A data processing job running every hour Requiring less than 500MB RAM Requiring more than 500MB RAM

Serverless

EC2

Heroku

1

0.30€/month

3€/month

Free for 1 app

2.1

0.67€/month

4€/month

7€/month

2.2

1.35€/month

8€/month

25€/month

Heroku is still a good plan in case 1

AWS is a better bargain if:

You need a cron job every hour

You need lots of computing memory

Serverless is cheaper than EC2

That's it for now!

I'll be happy to have your opinion or feedback if you tried using Serverless or AWS Lambda, or if you have any question or suggestion about this tutorial.

Feel free to leave a comment :)

Sources for the benchmark