An Introduction to the Fn Project
How to easily write and maintain functions in a cloud-agnostic way
Johan Vos

Although the term serverless computing will not win the award for the clearest terminology, it is one of the recent buzzwords in IT. It is more than a buzzword, though, and it is a very relevant concept for developers writing software that is intended for use in production in cloud environments.

From On-Premises Monoliths to Serverless Functions

One of the main reasons why companies are moving their operations from on-premises servers in their own data centers to centrally managed cloud systems is the cost reduction. Buying and maintaining servers can be very expensive and really makes sense only from a cost perspective if the servers are used at their maximum capacity for most of the time.

In most real-world situations, though, companies experience peak loads during which the available servers cannot handle all requests fast enough. During quiet moments, there are too many servers doing nothing.

Figure 1. Monitoring real-time server load typically shows overused and underused servers
Figure 1. Monitoring real-time server load typically shows overused and underused servers.

The initial cloud offerings allowed you to add and remove servers when required. That implies a cost reduction for many companies, but servers are still kept running (and they have to be paid for). In many cases, the servers are capable of executing more requests, but there are fewer incoming requests, so the servers are mainly just waiting for work. Due to the granularity (the server is the unit norm), this is not really "pay per usage."

Figure 2. Depending on expected load, users can add or remove servers
Figure 2. Depending on expected load, users can add or remove servers.

Using container technologies such as Docker, it became easier for developers to write software in their own environment and deploy the same software in the same environment on cloud servers. Using container management software, it is even possible to scale servers when needed.

But still, the unit at which "pay per usage" applies is still the server. It would be more cost efficient for cloud users if the billing unit were based on real server usage. Changing the billing unit to the amount of time a function is running is a big step closer to "pay per usage."

The success of serverless computing is not only due to a cost factor. The evolution from monolithic applications towards a set of microservices is leading to the same outcome. Using microservices, a large application can be decomposed into smaller units that can scale independently.

Many microservices can run on several servers, and different instances of a specific microservice can run on several servers.

In many cases, microservices can be decomposed into a number of smaller stateless functions. The difference between microservices and functions is not simply the size. Functions are stateless, and they require no knowledge about or configuration of the underlying server—hence, the term serverless.

Figure 3. Monolithic applications can be broken into microservices, and microservices can call a number of smaller functions
Figure 3. Monolithic applications can be broken into microservices, and microservices can call a number of smaller functions.

Therefore, the serverless computing revolution is very important to developers. In order to benefit from it, developers have to make sure their applications can leverage functions. Because a "serverless" environment depends on the notion of running functions only when they are needed, the term functions as a service (FaaS) is used as well.

This series of articles will introduce you to the Fn project, which is an open source platform that provides the foundation for an FaaS component.

The Fn Project

At the JavaOne 2017 conference, Oracle announced the Fn project. The project is hosted at https://fnproject.io and the source code is available at https://github.com/fnproject/fn.

Figure 4. The Fn project's logo
Figure 4. The Fn project's logo.

One of the key characteristics of Fn is that, although it is intended to run in cloud environments, it is not tied to a specific cloud vendor. The platform itself can be hosted on any cloud environment that supports Docker. That means you can run it on Oracle Cloud, but you can also run it on your own infrastructure or on other cloud systems, for example, Amazon Web Services (AWS), Google Cloud Platform, Microsoft Azure, and so on.

But it goes even further. Because the only requirement to run Fn is a Docker engine, you can also run the platform on your local development system, provided that you have Docker installed on that system.

This is a major benefit for developers. You can develop your functions completely cloud-agnostic. You test them on your local system, and if they run there, they will run on any system. This avoids a vendor-lock from a specific cloud vendor. As long as your function is not using any cloud-specific APIs, you can move it from one cloud to another—or you can still run it in an on-premises environment if that is required.

The Fn platform contains a number of components. The whole platform can be installed in a number of ways, as explained on the GitHub page.

One platform-independent way to install the platform is by downloading the installer via the command-line tool curl:

curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh

After a successful installation, you should see the following ASCII-text confirmation:

Figure 5. Indication that the Fn platform installation succeeded
Figure 5. Indication that the Fn platform installation succeeded.

As part of the installation, the Fn command-line interface (CLI) is now available. There are a number of ways to integrate with the Fn platform, and the Fn CLI is currently the easiest one.

If you want a list of functions that are supported by the CLI, simply enter the command fn:

Figure 6. A list of the commands supported by the fn CLI
Figure 6. A list of the commands supported by the fn CLI

Some of the command-line tools require an Fn server to be running, but some do not depend on that. As explained later, the Fn server is required for typical operations, for example, to keep track of the functions, routes, applications, and logs. If you simply want to compose, run, and test a function, you don't need the server to be up and running.

Getting Started with Functions

The Fn platform allows you to execute functions in any language. This article focuses on Java functions. Apart from the Java code for your function, some minor metainformation is needed in a configuration file named func.yaml. The Fn CLI tool allows you to easily generate a default function and a default configuration file, as follows:

fn init --runtime java

The fn init command causes the creation of a new function. This basically means a template is applied in the current directory. If you specify the --runtime java parameter, a Java template is used and a Maven project file (pom.xml) file is created, together with a very simple Java file. Also, a func.yaml file is created that contains the metainformation.

The generated func.yaml file looks as follows:

version: 0.0.1
runtime: java
cmd: com.example.fn.HelloFunction::handleRequest
build_image: fnproject/fn-java-fdk-build:jdk9-1.0.55
run_image: fnproject/fn-java-fdk:jdk9-1.0.55
format: http

The information contained in this file is needed for the Fn platform to create a Docker image that holds your function.

At this point, the most important entry in that file is the following:

cmd: com.example.fn.HelloFunction::handleRequest

This entry states that when the function is called, the method handleRequest on the com.example.fn.HelloFunction class will be invoked. The fn init command created this class already, as part of the default template. It looks as follows:

package com.example.fn;

public class HelloFunction {

    public String handleRequest(String input) {
        String name = (input == null || input.isEmpty()) ? "world"  : input;
        return "Hello, " + name + "!";
    }

}

This is a very simple Java class and the behavior of the handleRequest function is self-explanatory. You can, of course, change this code and make it more suitable for your real-world applications. Chances are that you will need more than just a class with a one-line function. You can add more classes and move them to different packages.

The fn init command also generated a Maven pom.xml file that contains the build and packaging instructions, and you can modify that file as well, for example, to introduce dependencies.

If you don't want to use Maven, you can use Gradle or any other build system, and I will show later how you can do this with slightly modified build instructions.

Running a Function

At this point, let's execute the function that was auto-generated. There are a number of options:

  • Use fn run to build a Docker image and run the function in a standalone environment.
  • Use fn deploy and other commands to deploy your function in the Fn server.

Clearly, if you really want to take advantage of an FaaS platform, you need the second option. You want to package your function and deploy it somewhere to a service that will then manage it for you.

But for testing purposes, it is often handy to simply run the functions locally. The fact that you don't have to modify code if you want to switch between local deployment and deployment in a cloud environment is a huge advantage.

Running the function locally can be done using the fn run command.

The output of the command is the following:

Building image javatwo:0.0.1 .........................................................................
Hello, world!

The first time this command is executed, it might take some time. If you run the command a second time, it will be much faster.

Although as a developer you don't need to worry about the implementation details, it might be interesting to understand how the Fn project is doing this.

This command performs two operations: it builds the function and it runs the function. How this is done is abstracted away from the developer.

The Fn CLI tool manages building and locally running functions for you by using a multistage Docker build process. Building the function and running the function each happen in a separate Docker stage.

In our simple case, building the function means executing the right Maven command in the right environment. Because Fn knows that you want to use a Java environment, it will execute the build commands in a Docker image that has all the required tools (for example, the Java 9 SDK).

The second step runs the function. This starts a new Docker container with the correct environment, and it launches the function specified by the cmd argument in the func.yaml configuration file.

Figure 7. The fn run command builds and runs the function
Figure 7. The fn run command builds and runs the function.

The key point for developers is that you should focus on your function, not on how to run that function on a specific configuration or infrastructure.

The fn run command executes your function inside a specific Docker container, but it doesn't integrate it into an FaaS environment yet. The Fn platform, which provides an FaaS environment, adds functionality such as routing, asynchronous execution, versioning, logging, and so on.

If we want to deploy our function to the Fn server, we first need to start the Fn server. This can be done using the following command, which is part of the CLI tool:

fn start

Starting the server gives the following output:

Figure 8. Output from starting the Fn server
Figure 8. Output from starting the Fn server.

The Fn server, which is a Docker image itself, will take care of the bookkeeping required for dealing with functions and applications.

In order to deploy our function to the Fn server, we use the CLI command fn deploy:

fn deploy --app demo --local

We use the --local argument to indicate that we want to send the generated Docker image to our local environment as opposed to registering it on Docker Hub. I will talk about Docker Hub later, but for now, we want to do all development in our local development environment.

The --app demo argument indicates that we want to deploy our function as part of the "demo" application. If that application doesn't exist yet, it will be created.

The function can now be identified by the combination of the application name (demo) and the function name (by default, the name of the directory).

The result of the fn deploy command is the following:

Deploying javatwo to app: demo at path: /javatwo
Bumped to version 0.0.3
Building image javatwo:0.0.3 ..
Updating route /javatwo using image javatwo:0.0.3...

Note that each time a function is deployed to the Fn server, the version of that function is incremented by one. You can verify that by inspecting the func.yaml file, which now has this entry:

version: 0.0.3

The function is now deployed on the Fn server. In order to run it via the Fn server (as opposed to directly running the Docker image using fn run, as explained before), we have a few options:

  • Use the Fn CLI.
  • Use an HTTP trigger.

Invoking a function via the Fn CLI tool can be done using this command:

fn call <app> <function>

In our case, this boils down to the following:

fn call demo javatwo

The result of this execution is simply the following:

Hello, world!

The second approach for invoking a function is using an HTTP trigger. When a function is registered to the Fn server, a route is created that can be accessed over HTTP. The following syntax should be followed:

curl http://<host>:8080/r/<app>/<function>

In our simple demo, this leads to the following REST call, which gives the same output as above:

curl http://localhost:8080/r/demo/javatwo

In both these options, the function wasn't directly invoked by the CLI tool. Rather, the CLI tool or the REST call triggered the Fn server to invoke the specific function that is part of a specific application.

In a real production environment, the Fn server will also be the entity that triggers the function invocations. But as you could see, the result of invoking a function directly (via fn run) or via the Fn server produces the same output, and that makes it very convenient to develop these functions.

Summary

In this first article, I introduced the Fn project and demonstrated how developers can easily write and maintain Java functions and test them in a standalone environment, or deploy them in an Fn server. The important thing to realize is that when functions are packaged in an Fn server, they can be executed anywhere, on any system that supports Docker.

In a next article, I will show how you can leverage the Fn project on cloud services. Also, I will demonstrate how using the Java Function Development Kit (FDK) allows you to create Java functions that stay vendor-neutral yet benefit from FaaS-specific APIs.

About the Author
Johan Vos started working with Java in 1995. He was part of the Blackdown team, porting Java to Linux. His main focus is on end-to-end Java, combining back-end systems and mobile/embedded devices. He received a Duke's Choice Award in 2014 for his work on JavaFX on mobile devices.
In 2015, he cofounded Gluon, which allows enterprises to create mobile Java client applications leveraging their existing back-end infrastructure. Gluon received a Duke's Choice Award in 2015. Vos is a Java Champion, a member of the BeJUG steering group and the Devoxx steering group, and he is a JCP member. He is the lead author of Pro JavaFX 8 (Apress, 2014), and he has been a speaker at numerous conferences on Java.
Join the Java Community Conversation
DEVO_ATTACH_BOTTOM
Experience Oracle Cloud—Get US$300 in free cloud credits.