“A massive white cloud over a snow-capped mountain ridge” by Benjamin Child on Unsplash
Table of Contents
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.
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 server
will ensure you have the latest Fn server image 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.
fnserver image running locally
Now that you have the Fn server running, you can create a new 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
)
TriggersEach function has a trigger that has a source (e.g. /greeter-app/hello
or /greeter-app/goodbye
) and a target that’s the endpoint where the function lives (e.g. http://localhost:8080/greeter-app/hello
)
**Functions**This is the actual piece of code you are writing and gets executed
ImagesDocker 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 --trigger http 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 runBuilding 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).
Deploying the app to local Fn server
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 triggers defined on the Fn server, run this command:
# List all triggers for 'myapp'$ fn list triggers myappFUNCTION NAME TYPE SOURCE ENDPOINThello hello-trigger http /hello-trigger http://localhost:8080/t/myapp/hello-trigger
Finally, if you access the endpoint, you will get back the “Hello World” message like this:
$ curl http://localhost:8080/t/myapp/hello-trigger{"message":"Hello World"}
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 foldermkdir greeter-app && cd greeter-app
# Create app.yaml that defines the app nameecho "name: greeter-app" > app.yaml
# Create a hello function in /hello subfolderfn init --runtime go --trigger http hello
# Create a goodbye function in /goodbye subfolderfn init --runtime go --trigger http 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 list routes greeter-apppath 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 --trigger http
Then deploy it again:
fn deploy --all --local
Now we have three functions under the /greeter-app logically group:
$ fn list routes greeter-apppath 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
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](http://localhost:4000)
.
Fn server UI
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 incmd/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.
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:
These events are available in a Call listener:
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 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.
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
andcalllistener.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 the 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 print 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 ("context""github.com/fnproject/fn/api/server"** _ "github.com/peterj/fn-extensions/callcount"**)
2. 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)}
Finally, we can build the fnserver and run it to 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:
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.
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:
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 here.
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.
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 output from the call to the /logs custom API endpoint
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.
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!