Powering a Facebook Messenger Service Serverless-ly

cover-image

Here at BBC News Labs we’re a curious bunch, and so we wondered what would happen if we took two trendy technology topics - Facebook’s Messenger platform and serverless architecture patterns - and mashed them together. Labber Tom Maslen was sent to investigate.

The brief was simple enough: create a quiz on Facebook Messenger for the So I Can Breathe season being run by our World Service editorial colleagues. They wanted us to help reach people who don’t typically engage with BBC News content and offer them possible solutions to pollution. Being a new member of News Labs who was already working with bots and private messaging apps, I was asked to help with the task.

With no prior experience with the Facebook Messenger service, the rest of bot the team away on holiday and a tight launch deadline, I thought the obvious thing to do would be to build the bot in a way that was completely new to me: serverless-ly.

How I did it

I had 4 weeks for the build, which was split roughly into 3 parts:

  • 1 week learning the Messenger API and building the quiz.
  • 2 weeks learning how Lambda and API Gateway work so I could deploy the quiz.
  • 1 week dealing with the Messenger review process and working on feedback from editorial.

The overall structure is very typical of the serverless pattern — a fancy new way to build web services that doesn’t require you to own any backend servers.

architecture diagram

Messenger API and quiz code

The code base for the Messenger quiz bot is split into the following modules:

index.js
data/quiz.json
lib/messengerclient.js
lib/quizengine.js
lib/user.js
  • index.js is the handler for the Lambda function. I used this like a controller: I set up the other modules and routed passed-in data to them.
  • lib/messengerclient.js deals with Messenger API interactions. Abstracting this away within messengerclient.js allows the index.js module to concentrate on pure business logic, rather than the guff you normally see with examples of handling Messenger interactions.
  • lib/quizengine.js holds the business logic for running the quiz.
  • lib/user.js holds methods, as well as properties of our user. This is to abstract away the use of S3 for storing answers to questions.
  • data/quiz.json is where we keep the information for the quiz. Towards the end of the project, my newly-returned dev team and I created a script that updates this file from a Google Spreadsheet, which my editorial colleagues used to update the contents of the quiz.

It’s important to understand that the Facebook Messenger API retains no state of the conversation you have with the user. As you respond to requests from the user, the only information passed to you is what the user just said. This information can come from either free text entries written by the user or, more usefully, actions on buttons that you have sent to the user.

To determine what to do with responses, you can append “payloads” to each button you send to the user. Payloads are essentially messages that are sent when the user presses a button. So for example, if you send the user a button called “Start quiz”, you can add a payload to it like this:

"buttons":[
  {
    "type":"postback",
    "title":"Start quiz",
    "payload":"START"
  }
]

When the user presses this button, you get a payload event back that looks like this:

{
  "sender":{
    "id":"USER_ID"
  },
  "recipient":{
    "id":"PAGE_ID"
  },
  "timestamp":1458692752478,
  "message":{
    "mid":"mid.1457764197618:41d102a3e1ae206a38",
    "text":"Start quiz",
    "quick_reply": {
      "payload": "START"
    }
  }
}

By checking the payload, we can make decisions against specific actions. I created a simple event registry in messengerclient.js for this purpose (see an example below). I add a type property to the payload, and then used that to decide how to react to the postback message:

messengerClient.on( "postback", ( userId, payload, messageEvent ) => {

	var user       = new User( userId );
	var quizEngine = new QuizEngine({
		"user": user,
		"data": data,
		"messengerClient": messengerClient,
		"stage": event.context.stage
	});

	const messageOptions = {
		"START": () =>  {
			quizEngine.start();
		},
		"ANSWER": () => {
			user.loadAnswers().then( () => {
				quizEngine.answerQuestion(
					payload.questionId,
					payload.answerId
				);
			});
		},
		"OPTIONS": () => {
			messengerClient.sendMessage({
				"userId": userId,
				"message": data.start,
				"buttons": [{
					"text": "START QUIZ",
					"payload": {
						"type": "START"
					}
				}] 
			})	
		}
	};

	messageOptions[ payload.type ]();

});

messengerclient.js has an easy-to-use method for sending messages to the user:

messengerClient.sendMessage({
	"userId": this.user.id,
	"message": "Press the start button",
	"buttons": [{
		"text": "Start quiz",
		"payload": {
			"type": "START"
		}
	}]
});

Lambda and API Gateway

In order to get our bot running without a server, we turned to AWS Lambda.Lambda executes code only when needed and scales automatically, so you’re only ever paying for servers when they’re actually being used for processing. I found Lambda incredibly easy to use once I worked out how information is passed in and out of it.

Your Lambda has to export a handler, which gives you 3 objects:

exports.handler = function ( event, context, callback ) {

	console.log( event ); // All data passed into your Lambda
	console.log( context ); // commands to send to the Lambda service, for example calling `context.done()` tells Lambda your code has finished.
	console.log( callback ); // a callback allowing you to pass data back to what called the Lambda

});

With this knowledge, it’s really simple to experiment with the Lambda locally. I created a play.js at the same directory level as index.js to help me work on the Lambda.

// play.js
const fakeMessengerMessage = {
	"context": {
		"http-method": "POST"
	},
	"params": {
		"querystring": {
			"hub.mode": "subscribe",
			"hub.verify_token": "YOUR_VERIFY_TOKEN_HERE",
			"hub.challenge": "YOUR_CHALLENGE_VALUE_HERE"
		}
	},
	"body-json": {
		"object": "page",
		"entry": [{
			"messaging": [{
				"sender": {
					"id": "123456789"
				},
				"optin": "",
				"message": "",
				"delivery": "",
				"postback": {
					"payload": "POSTBACK CONTENT GOES HERE"
				},
				"read": "",
				"account_linking": ""
			}]
		}]
	}
};

require( "../index" ).handler( 
	fakeMessengerMessage,
	{
		"done": () => {}
	},
	function callback( err, data ) {}
);

As awesome as a fully-managed service like Lambda sounds, it does have some very specific limits that I was unaware of when I started the project. The number of Lambdas that can run concurrently is limited to 400. If you go over that limit the requests for your Lambda will be placed in a queue (as long as they are called asynchronously), and run eventually.

Lambdas are invoked either by an event or via polling (a scheduled, repeatable event). The smallest unit of time they can poll for is 1 minute. This makes sense if you consider that a Lambda that constantly polls is essentially a constantly running server, so you may as well spin up a EC2 instead (it’d be cheaper in the long run).

Pro tip: It’s really worth reading the documentation before you get started (I didn’t read the documentation before I started).

Setting up API Gateway, on the other hand, was really difficult. The frameworks that help you with the serverless pattern hide how complex it is to set up the public access to your Lambda. I did it the hard way and worked out the required cloud formation for an API Gateway. It almost killed me. However, I did learn a lot more about API Gateway than I would have done if I’d used a framework.

The biggest takeaway for me was the difference in how you set up workflows. With a more traditional architecture (based on running your own EC2 instances), I’d normally make separate test and production workflows, literally spinning up a different server for each test and live environment. But the AWS Lambda and API Gateway services provide you with the mechanism for running different environment versions of your architecture within the service themselves.

This is a subtle yet important part to understanding how to use them as efficiently as possible. Each time you upload a new version of your code, Lambda will version it. Lambda then gives you the ability to creates aliases that point at specific versions of your code. This means that your live and test versions of the Lambda are held within the service itself.

screenshot

The same goes for API Gateway. API Gateway uses the concept of stages. Each stage is a different version of your public routing to your code on Lambda. These have different endpoints and can set different values to the same variables, essentially giving you test and live configurations.

screenshot

While all of this is configurable via the AWS console, it’s better to be able to programatically control your infrastructure. This is where serverless frameworks excel, and again this is where I decided to go alone and build my own tooling. Building my own tooling was definitely the hardest way to go, but it also gave me real experience of the AWS services I was using.

My tooling gave me the following abilities from my terminal:

  • Create from scratch the Lambda along with the role required to run it.
  • Update the Lambda.
  • Create the API Gateway.
  • Interrogate the log files.

A great thing about using Lambda is that you get easy logging. With NodeJS, literally anything you console.log will appear in CloudWatch without any required configuration. However, CloudWatch breaks the logging up into separate files, which makes it cumbersome to read through quickly.

To get around this, I wrote a script that pulled down the logs and dumped them into the terminal for me. This was much simpler to do than you might think — I looked at how Serverless did it and just quickly rewrote their approach.

The last thing that’s worth mentioning was how I dry-ran my Lambda function locally. I always struggle with IAM roles within my architecture because they require me to think abstractly about how my code runs itself, rather than just executing it on my laptop. Instead, I find it very useful to run my applications locally with the same permissions when they are in production.

While it’s good practice to use roles rather than creating users (as you don’t need to keep a copy of the passwords in your codebase), I did create a user with the Lambda role attached to it. I added the user details to my machine:

// ~/.aws/credentials
[bottest]
aws_access_key_id = XXX
aws_secret_access_key = XXX

And then ran the code locally like this:

AWS_PROFILE=bottest node play.js

This way, I was setting the credentials for the code to execute in rather than having to hard-code sensitive values into my code. I’m sure there are better ways of doing this, but this is the way that I found worked best for me.

What I learned

There are some specific facts I learned about Lambda and Messenger that would have been good to have known before I started.

Don’t pack your code into the Lambda

Think of the Lambda as routing for your actual code rather than the code itself. Lambdas can be made up of multiple files and file types. You can even pack binaries into your Lambda, for example. Your Lambda project will easily zip up and be deployed.

Lambdas have full access to the underlying computer resource

Lambdas run on Amazon Linux, so you can deploy and run anything that is compatible with it. The serverless framework Apex.run allows you to deploy Go applications to Lambda by using a NodeJS module that calls the Go binary via command line. Don’t constrain your ideas to the module.

Lambdas can be slow

Lambda is a fully managed service, and your code has to share it with other customers’ Lambda functions. Your code doesn’t have immediate priority, so the latency can be quite high. Once your Lambda has been called, it will stay “hot” for 5 minutes and will respond within milliseconds, but after that period it will go cold again. You need to take this into consideration. During our user testing we didn’t find a problem with it, but your experience may vary.

No record of conversation history

Though the user gets the context of the interactions with your bot via the conversation in Messenger, the Facebook API does not give your bot any conversation history. If you want a stateful conversation, then you need to create that state yourself. We did this by storing user answers in S3. We used the user id you get with each message (a persistent UID per user, but anonymized so you can’t determine who that person is) as the key for storing the content.

TL;DR

In the end, I found the whole process much simpler than I originally thought. There are a ton of examples online showing you how to setup Messenger and how to use the serverless pattern. I definitely chose the hardest option of not using a serverless framework, but now I’ve had first-hand experience of how to use Lambda and API Gateway. Reading through the documentation of these frameworks is now much easier as I have more context about what they are going on about.

Useful links:


Categories:

Tags: