Playing with the Fn project
Peter Jausovec
Posted on May 18, 2018
I am not one of those lucky ones who can simply read a whitepaper/code/docs and can quickly figure out how things work without trying things out in practice. I need to install things, run things and play with them to get a feeling for how stuff works. If I get stuck, that’s when I am going to read the docs and code to see what I am missing.
In this edition I decided to figure out how Fn project works, how to run it, use it and even extend it. The Fn project is the container native, cloud agnostic serverless platform.
As part of this article, I’ve created three different example extensions for the Fn server — you can get them on GitHub.
The Basics — install and run
This section goes through installing the Fn server, starting it and then creating a simple function and invoking it.
You will need Docker on your machine in order to run Fn and once you have that, you can install the Fn with brew:
brew install fn
Apparently I’ve installed Fn before and the Docker image I had on my machine was > stale causing Fn fail when starting — running
fn update
will ensure you have
the latest images on your machine.
Finally, with Fn installed and images updated, you can run the Fn server with the following command:
fn start
The above command runs Fn in a single server mode with embedded database and queue. Behind the scenes, fn start command runs a Docker image calledfnproject/fnserver in a privileged mode. It also mounts the Docker socket into the container as well as the /data folder in the current working directory (this is where database and queue information is stored). Finally, it exposes port 8080 to the host, so you can invoke it on that port.
Now that you have the Fn server running, you can create a new function.
First function
The Fn CLI comes with a init command that is used for creating new functions.
At the time of writing, these were the upported function runtimes: dotnet, go,
java8, java9, java, lambda-nodejs4.3, lambda-node-4, node, php, python,
python3.6, ruby, rust, kotlin
Before we start, here’s a simple explanation of different concepts Fn uses:
Apps
Apps are a way to logically group your functions under the same name (e.g. greeter-app)
Routes
Each function lives under a route in an app (e.g. /greeter-app/hello or /greeter-app/goodbye)
Images
Docker image that packages your function; the image used depends on the language of the function (e.g.fnproject/go, fnproject/ruby, fnproject/node, …), the goal here is that the image is as small as possible to be more performant
Calls
Call holds information about a call that was made to the function. It includes information about the app, route and time call was created, started and completed include the status of the call.
With this out of the way, let’s create a new function by providing a runtime (e.g. Go, Node or other supported language) and the name of the function:
fn init --runtime go hello
Above command creates a Go function in the hello sub folder. The function structure looks like this:
hello
├── Gopkg.toml
├── func.go
├── func.yaml
└── test.json
The source of your function lives inside the func.go file and has a function handler that responds with a “Hello World” message. The func.yaml
file has information such as version runtime, name and entry point for your function.
Another interesting file is test.json
— this file holds an array of tests (input values and expected output values) and you can use it to test out your function, by running fn test
.
To run this function, you can use the fn run
command. Before you run the command, make sure you set the FN_REGISTRY
environment variable to your Docker repository.
Then when you run the command, Fn will build the Docker image with the function and runs the function like this:
$ fn run
Building image hello:0.0.1 ...........
{"message":"Hello World"}
This is all great, but we have the Fn server running locally, so let’s deploy our function to the server, instead of just running it.
To deploy the function, you can use the fn deploy command, specify the app name and add the --local
since the Fn server is running locally:
fn deploy --app myapp --local
Command deploys the app (called myapp
) to the local Fn server and it creates a path called /hello
(our function name).
This means that on the Fn server, the function will be accessible under /myapp/hello path. The app name is used to logically group functions together. To see the full list of all routes defined on the Fn server, run this command:
# List all routes for 'myapp'
$ fn routes list myapp
path image endpoint
/hello hello:0.0.3 localhost:8080/r/myapp/hello
Finally, if you access the endpoint, you will get back the “Hello World” message like this:
$ curl localhost:8080/r/myapp/hello
{"message":"Hello World"}
Grouping functions
To group the functions together, you can use the app name construct — this allows you to logically group different routes together (e.g. greeter-app
could have routes called /hello
and /goodbye
).
In this case the greeter-app
could also be the folder name where your functions live and subfolders /hello
and /goodbye
would contain the actual functions. You can also define the app.yaml
file in the app root folder, to be able to deploy all functions with one command.
Follow the steps below to create a greeter-app with hello and goodbye functions:
# Create the greeter-app folder
mkdir greeter-app && cd greeter-app
# Create app.yaml that defines the app name
echo "name: greeter-app" > app.yaml
# Create a hello function in /hello subfolder
fn init --runtime go hello
# Create a goodbye function in /goodbye subfolder
fn init --runtime go goodbye
With all this set up and app.yaml
in the root folder, you can use this command to deploy all functions to local Fn server:
fn deploy --all --local
Above command creates the following app and endpoints:
$ fn routes l greeter-app
path image endpoint
/goodbye goodbye:0.0.2 localhost:8080/r/greeter-app/goodbye
/hello hello:0.0.2 localhost:8080/r/greeter-app/hello
You can also create a function that lives in the root of your app by running fn init
command from the apps’ root folder:
fn init --runtime node
Now we have three functions under the /greeter-app
logically group:
$ fn routes l greeter-app
path image endpoint
/ greeter-app:0.0.2 localhost:8080/r/greeter-app
/goodbye goodbye:0.0.3 localhost:8080/r/greeter-app/goodbye
/hello hello:0.0.3 localhost:8080/r/greeter-app/hello
Enabling the UI
If you prefer UI to interact with the Fn — there’s that for you as well. Assuming you have the Fn server running locally, you can start the UI like this:
docker run --rm -it --link fnserver:api -p 4000:4000 -e "FN_API_URL=http://api:8080" fnproject/ui
When image gets downloaded and container executes, you’ll be able to access the UI on http://localhost:4000
.
Extending Fn
There are a couple of different options for you to extend the Fn server. All options require you to rebuild the Fn server as you will have to import your extension — you can either use the build-server CLI command and ext.yaml
file to build a new image of the Fn server with your extension(s) OR you can fork & clone the Fn repo and reference your extension in cmd/fnserver/main.go
file, then re-build the code and run it.
For development, the fastest way is to clone the Fn repo and create & register your extension there. If you are using build-server
command it might take a bit longer as that command will re-build the Fn server image each time it’s invoked. Note that you will have to build the Fn server each time in both cases, but the straight-up Go build is much faster than rebuilding a Docker image.
There are three extension points on the Fn server: listeners, middleware, custom API endpoints. Read on for a more detailed description of each extension point and look at some examples later in the article.
Listeners
You can listen to various API events and respond to them. There are 2 types of listeners at this moment: App and Call. I think Route listeners should come soon as well…
In an App listener, you can respond to the following events:
BeforeAppCreate
AfterAppCreate
BeforeAppUpdate
AfterAppUpdate
BeforeAppDelete
AfterAppDelete
These events are available in a Call listener:
BeforeCall
AfterCall
Middleware
With middleware you can add desired functionality for every API request that comes to the server. Within that middleware you can then decide if you want to cancel the request or if you want to call the next middleware in the chain. A simple example of a middleware would be an authentication middleware that checks headers for a token or a middleware that logs certain things for each request.
Custom API endpoints
Custom API endpoints allow you to add new endpoints to the Fn server. For example, you could add a custom API endpoint that handles requests to a custom route such /mycustomroute
or define an endpoint with route /v1/apps/:app_name/mycustomhandler
or /v1/apps/:app_name/routes/:route_name/mycustomhandler
.
For example, one could implement a custom endpoint on apps and routes called stats
(so, /v1/apps/:app_name/stats and /v1/app:app_name/routes/:route_name/stats
) and when those endpoints are invoked you could return some basic stats for the app or a route.
Example: Call counter extension using Call listener
I wrote a simple extension that counts the number of times an app has been called and it outputs that number to the stdout. You can get the source code for the extension here.
The extension implementation is separated into two files: callcount.go
and calllistener.go
.
In the first file (callcount.go
) I register the extension and set up the call listener like this:
In the init
function, I am creating a map called callCountMap
that I’ll use to increment the calls to specific app and then I am registering the extension by calling RegisterExtension
function and passing in my extension struct that implements Name
and Setup
functions. In the name function I am simply returning just the import name where the extension is located at and in the Setup
function I am actually adding the Call
listener, telling Fn that I’ll be listening to Call events (these events are implemented in the calllistener.go
file):
In the BeforeCall
function we check if there’s an entry with the AppID
in the map, and if it isn’t, we set the number of calls to 0. Similarly, in the AfterCall
function we increment the number of calls for the AppID
and prints out that number.
With the extension ready we can modify the Fn server to include our extension. There are two things we need to do in the cmd/fnserver/main.go
file:
- Import the extension like this (line in bold):
import (
"context"
"github.com/fnproject/fn/api/server"
**_ "github.com/peterj/fn-extensions/callcount"**
)
- Call
AddExtensionByName
in the main function:
func main() {
ctx := context.Background()
funcServer := server.NewFromEnv(ctx)
funcServer.AddExtensionByName("github.com/peterj/fn-extensions/callcount")
funcServer.Start(ctx)
}
Now we can build the fnserver and run it to try out the extension.
Try out the extension
Let’s run the command below to rebuild the fnserver:
go build -o fnserver ./cmd/fnserver
Finally, run the ./fnserver
and when it starts, try calling a function you’ve deployed earlier. You should see the “Call number: X” in the Fn server output:
Just like we implemented the Call listener, we could similarly add the App listener, middleware or custom API endpoints. Adding the App listener is similar to adding the Call listener — we’d need to create methods on our extension struct to satisfy the App listener interface, and then call AddAppListener function.
Example: Cancel call middleware
Let’s show how would one implement a middleware function that checks if a certain header is present (fn-cancel-call
) and cancels the chain of calls — that is, it doesn’t execute the function.
There are two different ways to inject custom middleware. One is using the AddAPIMiddleware
— this function injects the middleware to all API endpoints such as:
/v1/apps
/v1/apps/:app
/v1/apps/:app/routes
...
The other function — AddRootMiddleware
— injects the middleware to both API and your app calls as well.
To create a custom middleware we need implement a Handle(next http.Handler) http.Handler
function on our extension struct. Just like before, the source code for the extension is available on GitHub.
The extension registration and setup part is the same as previously, the only difference is the implementation of the middleware and the fact that we call AddRootMiddleware
function, instead of a AddCallListener
function:
The logic for the middleware is in lines 30–40. We get the header named fn-cancel-call
and if the value of that header is set to 1, we output a message and return from the function, canceling the remaining chain of middlewares. If cancel header is not set, we call the next handler in line for execution (next.ServeHTTP
) and continue the execution.
Example: Call logs using custom API endpoint
In this last example, we are going to implement a custom API app endpoint /v1/apps/:app/logs
that connect to the Fn server database an returns a list of calls that were made to the app. We are going to return a couple of fields from that array to the user.
If you went through other examples, then the above code should look familiar. There are only a couple of differences — on line 23 where we are setting up the extension, we add the Datastore
reference to our extension struct, so we can use it later in the ServeHTTP
func and get the information about the calls. We also call the AddAppEndpoint
to set up our custom API endpoint on the /logs
path and specify the GET
HTTP method.
The functionality of the extension is in the ServerHTTP
func on line 35. Here, we set up a CallFilter
first, then pass it to the GetCalls
func on the datastore to retrieve the calls made to the app.
On line 43 we are using a func that’s coming from the Fn server package to send the error response, in case we can’t retrieve the calls.
Once we get the calls, we go through each one of them and write the call ID, status, path and time call started to the response writer.
Rebuild and run the Fn server then make the call to e.g. localhost:8080/v1/apps/myapp/logs
— you will get an output similar to the one in the figure below (assuming you made some calls to that app).
Conclusion
This article should serve you as a good introduction and getting started document for the Fn. It gives you the basics you need to start playing the serverless on your local machine and gets you thinking about different ways you can extend it.
I will probably write a follow up article where I’ll talk about Fn Flow Server, Fn Loadbalancer and how to get Fn running on Kubernetes.
Thanks for Reading!
Any feedback on this article is more than welcome! You can also follow me on Twitter and GitHub. If you liked this and want to get notified when I write more stuff, you should subscribe to my newsletter!
Posted on May 18, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.