Setting Functions Free
The Fn Project has been incubating for 2 years now — some may say it’s time to hatch — and in that time, we have gone back and forth on various APIs and learned some things we don’t like and some things we like even less than the things we don’t like. As is inevitable for any piece of software that manages to enslave a few programmers for long enough, we have begun cutting over to our v2 API. In this post, we’ll go over some of the things that tormented us — and possibly you!— in the previous API and what has changed, and leave you with some more breaking changes to look forward to.
If you’d like to follow along more in depth, the link to our swagger doc is http://petstore.swagger.io/?url=https://raw.githubusercontent.com/fnproject/fn/master/docs/swagger_v2.yml (because what you really needed was another Chrome tab to pollute your life).
Triggered
The v1 API for Fn was comprised of basic HTTP methods for creating, deleting, and updating apps and routes, as well as invoking a route. Briefly:
GET/PUT/DELETE /v1/apps/:app
GET/PUT/DELETE /v1/apps/:app/routes/:route
ANY /r/:app/:route
Where /r/:app/:route
will invoke a route using the HTTP request body as the payload to the function container, and return an HTTP response from the output of the function container back to the client. This glosses over a few details, such as container i/o format. We’ll get back to this later.
While the simplicity of this API was great, it was awkward to express how routes would interact with various event sources like a scheduler, or an API gateway, or a message queue, or sensors that intercept and modify brainwaves to send to long term government storage. The most obvious problem being that users want to create functions that they can use from multiple sources, even from two different routes. Under the v1 API this meant creating the exact same route twice, pointing to an image, and aligning the configuration, version, etc. Nobody needs that in their life.
What we really meant to express was a function that happens to have a default route to easily invoke for testing, short of configuring various real world URL routes on some API gateway to point to a certain Fn route. This made many conversations confusing (route/route) and meant that this intent was not clear to users either. We’ve decided to right this wrong by introducing triggers to invoke functions, and removing routes — which were effectively a function and http trigger— from the API.
Triggering
If this seems like a simple rename of our old routes resource to functions, that’s because it is (but we dropped route.path
). With the addition of triggers, though, we’ve made things more composable at a higher level. Here’s what the API looks like in v2, briefly:
/v2/apps/:app_id
/v2/fns/:fn_id
/v2/triggers/:trigger_id
Where an app is composed of various fns (functions) and triggers, and each trigger points to one fn. This makes a many-to-one relationship for triggers:functions. I made a diagram for you but my diagram skills seem about as good as our naming-things-right-the-first-time skills — words must suffice! With this new and more flexible domain model, users are able to set up various event sources to point to the same function, huzzah. For us, this means we can work towards being more friendly toward supporting things like queues or schedulers as event sources, and talking about API gateway routes instead of the old Fn routes when we use that confounding ‘route’ word.
Triggers themselves don’t appear to have much value when looking at Fn’s API alone. Right you are, Ken. Triggers just point to functions. The power here is that the code that services the events themselves, say pulling a message from a message queue and firing an event off to Fn, or an API gateway, or anything else you might consider an event source can have its own event format, attenuate permissions, authenticate or add attributes to a function event before sending it to a function. This can happen on many event sources for any given function.
For example, on a certain URL, say /whisper_hey
an API gateway can verify that calling function Wazzup
provides a JWT token that contains enough bytes to choke a 56k modem for 40 minutes. On another endpoint in the same API gateway, say /whisper_yo
the gateway can verify that calling function Wazzup
provides an OAuth 2.0 token. Et voilà! We can call the same function from 2 different endpoints. Internally, this example would be represented as 2 triggers with the trigger.type
field set to http
, trigger.fn_id
set to the id for the Wazzup
function, and one trigger with trigger.source
set to /whisper_hey
and another set to /whisper_yo
.
Pass me the sugar
If you are a previous user of Fn, we’ve made a simple path to upgrade your functions. Next time, before you $ fn deploy
run $ fn migrate
first. That’s it. If it doesn’t work, yell at us on slack and we will ring all five alarms. Running $ fn migrate
will give you a func.yaml
similar to the one you’re used to, but now with a trigger section in place of the previous path
field. func.yaml
will now look something like this:
schema_version: 20180708
name: yo
version: 0.0.1
runtime: go
entrypoint: ./func
format: json
triggers:
- name: yo
type: http
source: /yoyo
It is the same $ fn deploy
process as before for deployment, except the ways to invoke a function have now changed. If I deploy the above func.yaml
using:
$ fn deploy --app myapp
I’ll get both a function that I can invoke directly as well as an http trigger pointing to that function. The old /r/:app/:route
is now gone.
One option is to invoke the function directly:
$ fn invoke myapp yo
{"message":"Hello World"}
The other option is to invoke via the HTTP trigger. This is the replacement for the old /r/
that accepts the HTTP webhook format that you may be used to. You can use the trigger by doing:
$ curl localhost:8080/t/myapp/yoyo
{"message":"Hello World"}
Almost like you’re used to, but with some slight tweaks. For the OSS experience, in some ways you can treat it just like the r
changed to an t
. Beware, it’s possible to create functions that do not have a /t
endpoint now, by omitting the triggers:
section from the func.yaml
, so the r/t change is not a great rule of thumb. Users should now prefer fn invoke
for testing functions, with alternative trigger types coming soon.
On to the next one
This post explains the core reasons why we decided to create the v2 API with triggers and functions. This was a somewhat lateral move functionality wise in the current implementation, but with the added flexibility we are now unlocked to do future work in integrating different trigger types above just HTTP. We currently support setting up any number of http
triggers for any given function in Fn using the CLI, with more coming. In addition to the triggers and functions changes, we’re also looking forward to removing the residual format
cruft from the v1 route days. If you’ve used Fn, then you no doubt have had the pleasure of figuring out which of the 4 container formats we offer for use. We are currently working on one event and container format to rule them all, and we’ll be rolling this out in the coming weeks to the v2 API and getting rid of format
!
If it’s been a while or you’re new to Fn, you can try out the v2 API today at http://fnproject.io/tutorials/ and if you have any thoughts, good or bad, feel free to air your grievances/delights on slack.