Wot no FDK?

Writing Serverless Functions in a New Language

Background

Fn Project is an open - source, Docker based, cloud native serverless platform.  You can run it in any environment that supports Docker.  Oracle Functions is based on Fn Project.

If you want to have a play with Fn before reading the rest of this post, I recommend the "Quick Start" guide.

One of the great strengths of Fn Project is that it is open.

When other serverless platforms (that shall remain nameless) restricted you to JavaScript / Node plus maybe a couple of others, Fn offered an FDK for Ruby (for those who prefer elegance and beauty in their code).

This was a major factor in me getting involved with the project, and becoming a contributor to the Ruby FDK.

In addition to Ruby, Fn offers a number of other FDKs: Go, Java, Python, Node and .NET Core.

But beyond that, Fn has always been open to additional language runtimes.  If your code can run in a Docker container, then (in principle) it can run as an Fn function.

Now this theory is all well and good, but what if you have or want to write code in a language for which there is no FDK.  How do you actually go about building a function out of it?

Practice

Hotwrap

The first, easiest way is if your code can be run from a Linux command line.  Then you can use hotwrap, which will act as a wrapper for your code.  Your code takes it's input from STDIN and writes it's output on STDOUT and hotwrap takes care of the translation to Fn's http-stream format.

Helper

But what if your code doesn't run from the command line, or you don't want to add a CLI to each function you write?

Then you're going to want to set up some machinery (or a "helper") to interact with Fn directly.

So I'm going to show you how to create a reusable helper that will allow you to write functions in a language for which there isn't an FDK.

For this example I've chosen Crystal, a relatively new language that offers the elegance of Ruby, with type safety (since it's statically typed) and speed (since it's compiled).

However the important thing to focus on is the process and the machinery, since these will be the same across all languages.

Protocol

The Fn server communicates with Functions using HTTP over UNIX sockets ("http - stream").

Message Format

For this example, both the function's input and output are JSON formatted strings.

Process

  1. Fn starts the Docker container for the function
  2. As per the Dockerfile, the file containing the function code is executed
  3. The "helper" code opens a UNIX socket in the container.  The path for the socket is read from an environment variable that Fn sets when it runs the container.
  4. The "helper" code begins listening on the socket.
  5. Fn sends the function payload over HTTP via the socket.
  6. The "helper" code reads the input data from the socket and calls the actual (business) function code, passing it the input data.
  7. The function returns the output.
  8. The "helper" code sends the output back over HTTP via the socket.
  9. The "helper" code continues to listen on the socket for repeat invocations until Fn terminates the function container. 

In each case, the "helper" code is performing the steps that would be carried out by the FDK if there was one available.

The helper that we're going to build is not a full blown FDK, but it provides a "minimum viable proxy" for Fn to communicate with the function. 

In walking through the example code we're going to take a kind of "reverse - big - bang" approach.  We'll start with an "exploded view" which is admittedly verbose but makes it easy to see all the parts of the machinery separately.  Then we'll collapse it down to 15 lines of code (if not quite to a singularity).

Walk Through

The first thing to do (assuming you already have a working Fn installation) is to start Fn in debug mode, since you will want to be able to see what is going on as you build the machinery:
$ fn start --log-level DEBUG
Then in a second window, create a new directory.

In this case, the directory is called crystal-hello, which will also be the name of our function.

Inside the new directory, create your first iteration of your function file, which just writes something to STDERR:


And a Dockerfile to build it:

The reason for doing this is just to check that you can build the container and get debug output to the window where you have Fn running!

In fact lines 2 and 3 of the Dockerfile were added after I discovered that Crystal needs a writeable cache directory pointed to by CRYSTAL_CACHE_DIR.  For an Fn function container, this means it has to go under /tmp.

Once you have these two files run the following:
$ fn create app no-fdk
$ fn init
You can now attempt to build and deploy your "function":
$ fn deploy --app no-fdk
The "function" should now be deployed to Fn and you can invoke it:
$ fn invoke no-fdk crystal-hello
It should fail ;-)

But you should see your distinctive error message in the window where Fn is running.

Now that we know that we can get error messages out, we can start building our function so that it carries out the steps of the process described above and logs what it has done to standard error:

At this stage, it's reading the environment variable and creating the socket.  The part about the private_socket_path is defensive - during development of some of the FDKs there was a race condition where Fn started writing to the socket before the container was ready (more (or less) about this later).

Now we have the socket, we need to start listening on it.  Something that listens for HTTP requests and sends responses back sounds a bit like an HTTP server to me, so let's use Crystal's default HTTP::Server class to handle that part:


So if you deploy that, you should have a function that you can invoke with a JSON message and it sends you the message back.

So now we need to add the business functionality we wanted in the first place (arse, swamp and alligators may come to mind at this point!).

In this case I'm using a Proc for the function...
my_proc = ->(input : JSON::Any) do
name = input["name"]? || "world"
%({"message": "Hello #{name}"})
end
...but you could also use a block or whatever mechanism your language provides for passing a function.

Once we have this we wind up with our "exploded view" of the function and the helper...


So now we should have a function that will say "hello" to either "world" or then <name> you passed in.

It is however a bit on the verbose side, so let's look at shrinking it down:

That is less verbose :-)

The private_socket_path piece is a bit ugly though.  Depending on how fast your helper starts listening you may be able to skip it.  YMMV / "here be dragons", but I have found with Crystal I can drop it, giving me just 15 lines of helper code:



Summary

As I said before, this is not a fully fledged FDK, but it is a helper that I can use (and reuse) to write functions in Crystal, and if you follow this pattern, you'll be able to write your functions in your preferred language and run them on the Fn platform.

Comments

Popular posts from this blog

The Case of the Vanishing Dockerfile

Serverless Functions using Rust and WebAssembly