DEV Community

Peter Jausovec
Peter Jausovec

Posted on • Updated on

Playing with the Fn project

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

docker ps

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Command deploys the app (called myapp) to the local Fn server and it creates a path called /hello (our function name).

running fn deploy

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
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

When image gets downloaded and container executes, you’ll be able to access the UI on http://localhost:4000.

fn project UI

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
Enter fullscreen mode Exit fullscreen mode

These events are available in a Call listener:

BeforeCall
AfterCall
Enter fullscreen mode Exit fullscreen mode

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:

  1. Import the extension like this (line in bold):
import (
    "context"
    "github.com/fnproject/fn/api/server"
    **_ "github.com/peterj/fn-extensions/callcount"**
)
Enter fullscreen mode Exit fullscreen mode
  1. 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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

extension in action

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
...
Enter fullscreen mode Exit fullscreen mode

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).

sample response

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!

Top comments (2)

Collapse
 
mirknn profile image
Miriam Keenan

Really liked this guide!

Collapse
 
peterj profile image
Peter Jausovec

Thank you Miriam!