Event Driven Machines

a pit stop team working on a lambo, which is generating pdfs in a way that totally makes sense
Image by Annie Ruygt

We’re Fly.io and we transmute containers into VMs, running them on our hardware around the world. We have fast booting VM’s, so why not take advantage of them?

Serverless is great because is has good ergonomics - when an event is received, a “not-server” boots quickly, code is run, and then everything is torn down. We’re billed only on usage.

It turns out that Fly.io shares many of the same ergonomics as serverless. Can we do a serverless on Fly.io? 🦆 Well, if it’s quacking like a duck, let’s call it a mallard.

Here’s a useful pattern for triggering our own not-servers with Fly Machines.

Triggering Machines

I want to make Machines do some work based on my own events. Fly.io can already stop Machines when idle based on HTTP, so let’s concentrate on non-HTTP events.

The process of running evented Machines involves:

  1. Listening for events
  2. Spinning up Fly Machines to run our code (with the events as context)
  3. Having event-aware code to run

To do this, I made a project and named it Lambdo because reasons. You can consider this project “reference architecture” in the same way you call a toddler’s scribbling “art”.

The goal is to run some of our code on a fresh not-server when an event is received. We want this done efficiently - a Machine should only exist long enough to process an event or 3.

Lambdo does just that - it receives some events, and spins up Fly Machines with those events placed inside the VMs. Once the code finishes, the Machine is destroyed.

the files are inside the computer
The files are in the computer!

Listening for Events

For our purposes, an event is just a JSON object. {"any": "object", "will": "do"}.

We want to turn events into compute, so we need some sort of event system. I decided to use a queue.

The Queue

The first thing I needed was a place to send events! I chose to use SQS, which let me continue to pretend servers don’t exist.

It’s no surprise then that the first part of this project is code that polls SQS.

When the polling returns some non-zero number of events, it collects the SQS messages’ JSON strings (and some meta data), resulting in an array of objects (a list of events).

Then we send these events to some Machines.

Spinning Up Machines

Fly Machines are fast-booting Micro-VM’s, controlled by an API.

A feature of that API is the ability to create files on a new Machine. This is how we’ll get our events into the Machine.

When Lambdo creates a Machine, it places a file at /tmp/events.json. Our code just needs to read that file and parse the JSON.

Running Our Code

Part of the ergonomics of Serverless is (usually) being limited to running just a function. Fly.io doesn’t really care what you run, which is to our advantage. We can choose to write discreet functions per event, or we can bring our whole Majestic Monolith to bear.

How do we package up our code? The real answer is “however you want!”, but here’s 2 ideas.

Use Your Existing Code Base

You can just use your existing code base. This is especially easy if you’re already deploying apps to Fly.io.

All we’d need to do is add some additional code - a command perhaps (rake, artisan, whatever) - that sucks in that JSON, iterates over the events, and does some stuff.

$events = json_decode(file_get_contents("/tmp/events.json"));

foreach ($events as $event) {
  // do a thing
}

When we create an event, we’ll tell Lambdo how to run your code - more on that later.

Use Lambdo’s Base Images

This project also provides some “runtimes” (base images). This is a bit more “traditional serverless”, were you provide a function to run.

Lambdo contains two runtimes right now - Node and PHP. There could be more, of course, but you know…lazy.

The Node runtime contains some code that will read the JSON payload file (again, just an array of JSON events), and call a user-supplied JS function once per event.

An example is here - our code just needs to export a function that does stuff to the given event:

// File /app/index.js
exports.handler = async function(event) {
    console.log("Let's process an event! The event:", event)
}

The PHP runtime is the same idea, a user-supplied handler looks like this:

// File /app/index.php
return function function(array $event) {
    // Do something with $event
}

Explore the runtime directory of the project to see how that’s put together.

Sending an Event

Since our events are sent via SQS queue, it would be helpful to see an example SQS message. Remember how I mentioned the SQS message has some meta data?

Here’s an example, with said meta data:

aws sqs send-message \
    --queue-url=https://sqs.<region>.amazonaws.com/<account>/<queue> \
    --message-body='{"foo": "bar"}' \
    --message-attributes='{
"size":{"DataType":"String","StringValue":"performance-2x"}, 
"image":{"DataType":"String","StringValue":"fideloper/lambdo-php-sample:latest"}
}'

The Body field of the SQS message is assumed to be a JSON string (it’s the event itself, and its contents are arbitrary - whatever makes sense for you).

The message Attributes contains the meta data - up to 3 important details:

  1. image: The image to run (it might be a Docker Hub image, or something you pushed to registry.fly.io). This is required.
  2. size: The CPU size and type to use† - defaults to performance-2x
  3. command: The command to run, which is the Docker CMD equivalent - defaults to whatever your CMD is set in the Dockerfile used to create the Machine image.††

†You can get valid values for the size option by running fly platform vm-sizes.

††It’s an array form, e.g. ["php", "artisan", "foo"], you may need to do some escaping of double quotes if you’re sending messages to SQS via terminal.

We did a Lambda?

Fly.io isn’t serverless, but it has all these primitives that add up to serverless. You have events, Fly.io has fast-booting VM’s. They just make sense together!

What we did here is use Lambdo to respond to events by spinning up a Machine. Our code can process those events any way we want.

What I like about this approach is how flexible it can be. We can choose the base image to use and the server type (even using GPU-enabled Machines) per event. Since we have full control over the Machine VM’s responding to the events, we can do whatever we want inside of them. Pretty neat!