Neuigkeiten von trion.
Immer gut informiert.

fn Project - Serverless Java mit Docker und Kubernetes

Im Markt der Serverless-Ansätze positioniert sich Oracle mit Project Fn. Dieser Beitrag demonstriert den Einsatz des Fn Project um mittels Docker oder Kubernetes Function-As-A-Service (FaaS) bzw. Serverless Architekturen umzusetzen. Als Demo wird eine Java Anwendung mit Project Fn als Docker-Container erstellt und im Fn Server betrieben.

Als Ziel des Fn Projekt ist formuliert, dass es eine Plattform fuer Serverless-Anwendungen darstellt, die herstellerunabhaenging sowohl in beliebigen Cloud Umgebungen, als auch auf eigener Hardware betrieben werden kann. Durch Verwendung von Containern als Abstraktion soll die Verwendung beliebiger Programmiersprachen moeglich sein. Bei der Positionierung von Fn steht der Entwickler stark im Fokus: Die Entwicklung mit Fn soll besonders leicht von der Hand gehen, was sich sowohl durch Code-Scaffolding als auch eine umfangreiche Dokumentation mit leicht nachvollziehbaren Tutorials zeigt. Fn bringt ein eigenes FDK (Function Development Kit) mit, was unter anderem Nebenbei ist das Versprechen, fuer Entwickler einfacher in der Verwendung zu sein, bei Serverless bereits dadurch gegeben, dass keine Applikationsserver oder anderweitige Infrastruktur eingerichtet werden muss.

Das Fn Project hat seine Wurzeln in iron.io, was ca. 2010 entstand und als Messagebroker und Eventverarbeitungssystem gestartet ist. Erst später, ca 2014, wurde von einem eigenen Abstraktionslayer auf Docker Container umgestellt, um darüber die Entkopplung von der zugrundeliegenden Infrastruktur zu realisieren.

Die Installation von Fn für die Plattformen Linux, MacOS und Windows ist auf der Webseite gut beschrieben. Am Ende dieses Beitrags wird als Alternative die Nutzung von Vagrant zum Ausprobieren aufgezeigt.

Verwendung von Fn mit Java

Nachdem die Fn Werkzeuge installiert sind, können neue Funktionen als Projekt sehr einfach erzeugt werden. Im folgenden Beispiel wird Java als Zielsprache verwendet und eine zugehöriges Projekt unter Verwendung des Fn CLI erzeugt.

Da Docker Images die zugrundeliegende Einheit für das Deployment darstellen, wird auch eine Docker Registry benötigt. Prinzipiell kann mit einem DockerHub Account und der öffentlichen Docker Registry gearbeitet werden. Entsprechend dem Namespace von DockerHub ist der Name der Anwendung dann mit dem Accountnamen zu prefixen, beispielsweise 'mustermann/myapp'. Im typischen Firmenkontext wird man eine eigene Registry verwenden und hat dort entsprechend andere Namespaces oder sogar ganz flache Namensräume für die Docker Images. Alle Beispiele werden mit der am Ende des Artikels gezeigten Vagrant VM umgesetzt und verwenden eine eigene Docker Registry ohne Namespace-Prefix.

Auch für den Namen der Anwendung selbst ergibt sich aus der Verwendung von Docker Images eine Konsequenz: Der Name der Anwendung muss den Vorgaben für Docker-Image Namen genügen.

Mit dem Kommando fn init wird ein neues Projekt erzeugt, beispielsweise für Java mit fn init --runtime java.

Erzeugung einer Fn Anwendung in Java
$ fn init --runtime java myapp
Creating function at: myapp
Runtime: java
Function boilerplate generated.
func.yaml created.

Anschließend ist eine erste voll funktionsfähige Funktion samt für das Deployment erforderlicher Metadaten vorhanden. Die Metadaten finden sich in der Datei func.yaml und geben vor, welchen Namen das zu bauende Docker Image erhalten soll, mit welchem Tooling das Image zu bauen und auszuführen ist, und welches Kommando den eigentlichen Funktionsaufruf abbildet.

Die func.yaml beschreibt die fuer Fn relevanten Deploymentmetadaten, wie im folgenden Quellcode zu sehen.

Inhalt der func.yaml
name: myapp
version: 0.0.1
runtime: java
cmd: com.example.fn.HelloFunction::handleRequest
build_image: fnproject/fn-java-fdk-build:jdk9-1.0.62
run_image: fnproject/fn-java-fdk:jdk9-1.0.62
format: http

Neben der Deploymentbeschreibung wird auch ein Maven Projekt erzeugt. Das Maven Projekt verwendet dabei mindestens Java 9 als Zielversion - wer noch Java 8 verwendet, kann dies jedoch leicht anpassen. Es gibt aktuell keine zwingenden Abhängigkeiten auf Java 9 Features.

Eine Hello-World-Funktion
package com.example.fn;

public class HelloFunction
{
    public String handleRequest(String input)
    {
        String name = (input == null || input.isEmpty()) ? "world" : input;

        return "Hello, " + name + "!";
    }
}

Mit Maven lassen sich alle erforderlichen Abhängigkeiten auflösen und auch lokal die Anwendung bauen.

Ein besonderes Merkmal von Fn ist die automatische Erzeugung von JUnit basierten Tests. Zur Unterstützung wird von Fn auch eine JUnit Rule mitgeliefert, mit der auch komplexere Testabläufe leicht orchestriert werden können.

Testcase der Hello-World-Funktion
package com.example.fn;

import com.fnproject.fn.testing.*;
import org.junit.*;

import static org.junit.Assert.*;

public class HelloFunctionTest
{
    @Rule
    public final FnTestingRule testing = FnTestingRule.createDefault();

    @Test
    public void shouldReturnGreeting()
    {
        testing.givenEvent().enqueue();
        testing.thenRun(HelloFunction.class, "handleRequest");

        FnResult result = testing.getOnlyResult();
        assertEquals("Hello, world!", result.getBodyAsString());
    }

    @Test
    public void shouldReturnWithInput()
    {
        testing.givenEvent().withBody("Bob").enqueue();
        testing.thenRun(HelloFunction.class, "handleRequest");

        FnResult result = testing.getOnlyResult();
        assertEquals("Hello, Bob!", result.getBodyAsString());
    }
}
Lokaler Aufruf der Fn Funktion
$ export FN_REGISTRY=localhost:5000
$ cd myapp
$ fn run
Building image localhost:5000/myapp:0.0.1 ...........
Hello, world!

Die Funktion kann nun auch mit Inputwerten aufgerufen werden:

Lokaler Aufruf der Fn Funktion mit Parameter
$ echo "docker" | fn run
Building image localhost:5000/myapp:0.0.1
Hello, docker

Bemerkenswert ist, dass Fn JSON als Input transparent in Java Objekte übersetzt. Unter der Haube verrichtet dazu Jackson die Arbeit, wie man es beispielsweise auch von Spring Projekten kennt.

Build und Deployment

Um eine Funktion bei Fn zu deployen, wird stets ein Docker Image gebaut. Bereits zum Bauen des Docker Image wird, wiederum mit Docker, ein Build-Image verwendet, um die Quellen zu übersetzen. Das Tooling-Image und das für den Deployment-Container zu nutzende Basisimage wird in der bereits erwähnten func.yaml spezifiziert.

Build des Docker-Image zum Deployment
$ fn build
Building image localhost:5000/myapp:0.0.1 .....
Function localhost:5000/myapp:0.0.1 built successfully.

Details zum Build lassen sich durch fn --verbose build anzeigen, dann ist auch der Maven Aufruf und die Ausgaben zu sehen.

Nach dem Build kann die Funktion nun theoretisch deployt werden - jedoch wird dazu nun ein Fn API-Server benötigt. Auch hier setzt das Fn Project auf Docker: Der Fn Server wird als Docker Image bereitgestellt und kann komfortabel über das Fn CLI gestartet werden.

Starten des Fn Server im Vordergrund
$ fn start
Unable to find image 'fnproject/fnserver:latest' locally
latest: Pulling from fnproject/fnserver
ff3a5c916c92: Pull complete
...
        ______
       / ____/___
      / /_  / __ \
     / __/ / / / /
    /_/   /_/ /_/
        v0.4.117

Nachdem dieser entweder im Vordergrund oder mit fn start -d im Hintergrund gestartet wurde, kann das Deployment durchgeführt werden.

Deployment der Funktion mittels Registry
$ fn deploy --app myapp
Deploying myapp to app: myapp at path: /myapp
Bumped to version 0.0.2
Building image localhost:5000/myapp:0.0.2
Pushing localhost:5000/myapp:0.0.2 to docker registry...The push refers to repository [localhost:5000/myapp]
Updating route /myapp using image localhost:5000/myapp:0.0.2...

Auch an dieser Stelle zeigt sich, dass die Entwickler von Fn mitgedacht haben: Bei jedem Deployment wird automatisch die Version des Maven Projekts hochgezählt.

Um die lokale Entwicklung zu beschleunigen, kann durch den Schalter --local auf den Umweg über eine Docker Registry verzichtet werden. Dann wird ein lokales Docker-Image beim Deployment verwendet.

Lokales Deployment der Funktion
$ fn deploy --local --app myapp
Deploying myapp to app: myapp at path: /myapp
Bumped to version 0.0.3
Building image localhost:5000/myapp:0.0.3
Updating route /myapp using image localhost:5000/myapp:0.0.3...

Sowohl die deployten Funktionen, als auch alle Routen einer Anwendung können abgefragt werden:

Deployte Fn Funktionen und Routen
$ fn list apps
myapp
$ fn list routes myapp
path	image				endpoint
/myapp	localhost:5000/myapp:0.0.3	localhost:8080/r/myapp/myapp

Die Funktion kann nun durch einen HTTP Request aufgerufen werden. Der Fn Loadbalancer mappt Routen auf deployte Funktionen und verwaltet einen entsprechenden Pool, um entsprechend der Last zu skalieren. Als Einstiegspunkt in alle Routen dient der Pfad /r. Für die eigentliche Route zur Funktion wird zum einen der Anwendungsname, zum anderen das aktuelle Verzeichnis verwendet, falls in der func.yaml nichts weiter definiert ist. Da hier beides identisch ist, ergibt sich als Route /r/myapp/myapp.

Aufruf der Funktion mit cURL
$ curl http://localhost:8080/r/myapp/myapp
Hello, world!

Fn UI

Zur besseren Übersicht kann das Fn UI verwendet werden. Der Start erfolgt auch über Docker:

$ docker run --rm -p 4000:4000 -d \
  -e FN_API_URL=http://$(docker inspect -f '{{.NetworkSettings.IPAddress}}' fnserver):8080 \
  fnproject/ui

Der Zugriff erfolgt dann mittels Webbrowser auf den Port 4000, also beispielsweise http://localhost:4000/

Abbildung 1. Fn UI im Browser

Zwischenfazit

Das Fn Project ist vielversprechend und bringt einige interessante Ansätze mit:

  • CLI als zentrales Werkzeug zur Arbeit mit der Project Fn Infrastruktur und Projekten

  • Scaffolding erlaubt für Entwickler einen sehr leichten Einstieg

  • Verwendung von Standardmechanismen der jeweiligen Sprache, wie beispielsweise Maven im Java Kontext

  • Fn Loadbalancer mit Logik zur möglichst effizienten Resourcennutzung (Hot Instance Routing)

  • Fn Flow zur Abbildung komplexerer Abläufe durch Verkettung von Funktionen

  • JUnit Integration um testgetrieben in Java Fn Funktionen entwickeln zu können

  • JSON Support für transparentes POJO-Mapping in Java

  • Unterstützung für asynchrone Funktionen, deren Ergebnis im Hintergrund erzeugt wird

Problematisch zu bewerten ist dabei Oracle als Hersteller und Sponsor. Der Umgang von Oracle mit der NetBeans IDE, dem Glassfish Application Server, JavaEE und nicht zuletzt OpenSolaris als Betriebssystemplattform zeigen mehr als deutlich, welchem Risiko man sich bei Oracle Produkten aussetzt.

Es ist zu erwarten, dass eine Konsolidierung im Bereich der serverless Frameworks erfolgt. Setzt sich hier nicht Oracle durch, wird das Fn Project sicherlich noch schneller sang und klanglos eingestellt, als die oben genannten anderen Beispiele. Dass Oracle hier von Anfang an auf OpenSource setzt, erspart die "Übergabe an die Community" und entsprechende Presse.

Auch unbeachtet dem Risiko "Oracle" ist das Fn Project noch nicht für den produktiven Einsatz zu empfehlen. Ein Release als stabile 1.0 Version ist jedoch noch für 2018 geplant. Zu hoffen bleibt, dass dann auch eine Integration mit Kafka und Kubernetes, z.B. als Custom Resource Definition oder Kubernetes Operator, erfolgt.

Project Fn in Vagrant

Gerade für die Entwicklung oder Experimente eignet sich Vagrant und VirtualBox um schnell und reproduzierbar virtualisierte Umgebungen aufzusetzen. Um mit dem Fn Project zu arbeiten, kann das folgende Vagrantfile verwendet werden. Es verwendet ein Ubuntu als Betriebssystem und installiert darauf eine aktuelle Docker Version, stellt eine private Docker Registry bereit und installiert das Fn Project.

Vagrantfile für Ubuntu und fn Project
Vagrant.require_version ">= 1.8"

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/bionic64"

  config.vm.provider :virtualbox do |vb|
    vb.linked_clone = true
    vb.name = "fn demo"
    vb.customize ["modifyvm", :id, "--memory", "4096"]
    vb.customize ["modifyvm", :id, "--cpus", 2]
  end
  config.vm.synced_folder "./", "/fndemo"
  config.vm.network :forwarded_port, host: 8080, guest: 8080
  config.vm.network :forwarded_port, host: 4000, guest: 4000
  config.vm.network :forwarded_port, host: 5000, guest: 5000

  $setup = <<SCRIPT
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
 "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
 $(lsb_release -cs) \
 stable"
sudo apt-get update
sudo apt-get install -y docker-ce
sudo usermod -aG docker vagrant
sudo docker run -d -p 5000:5000 --name registry --restart=always registry:2
curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sudo sh
SCRIPT
  config.vm.provision "shell", inline: $setup

end

Das akutelle Verzeichnis wird bei dieser Konfiguration in der virtuellen Maschine unter /fndemo bereitgestellt. Damit kann lokal mit einer IDE die Projektquelltexte bearbeitet werden und werden automatisch in die virtuelle Umgebung mit Fn synchronisiert.

Nachdem mittels vagrant up die Umgebung gestartet wurde, steht sowohl eine Docker-Registry, als auch der Fn Server zur Verfügung. Mittels vagrant ssh können anschließend in der Umgebung Befehle abgesetzt werden.

$ vagrant ssh
vagrant@ubuntu-bionic:~$ cd /fndemo
vagrant@ubuntu-bionic:/fndemo$ fn init --runtime java myapp
Creating function at: /myapp
Runtime: java
Function boilerplate generated.
func.yaml created.

Da der Ordner /fndemo via Vagrant vom Host gemappt ist, kann nun mit einer beliebigen IDE an dem Projekt gearbeitet werden.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels