Software in Containern auszuliefern, ist seit ein vielen Jahren ein zentraler Trend. Kubernetes hat sich als Mittel der Wahl etabliert, die Container auf vielen Rechnern zu betreiben und zu orchestrieren. Auf welcher konkreten Infrastruktur es läuft, ist dabei zweitrangig. Es kann das Angebot eines der großen Cloud-Providers sein, ein lokales Rechenzentrum oder sogar eigene Hardware vor Ort: Kubernetes empfiehlt sich als universelle Plattform. Für viele ist das ein wichtiges Argument für den Einsatz. Entwicklern verlangt das System jedoch einiges an Wissen ab, wie sich eine Anwendung in einer solchen Umgebung betreiben lässt.

Unter dem Begriff Serverless vereinen sich wiederum Ansätze, bei denen die Infrastruktur in den Hintergrund rückt. Ein Kernbestandteil dieser Architektur ist Function as a Service (FaaS). Es werden nur noch Funktionen definiert, die auf bestimmte Events reagieren. Eine Anwendung entsteht durch die Kombination vieler solcher Funktionen. Bei Serverless- Angeboten bindet man sich jedoch an den jeweiligen Anbieter der Infrastruktur und kann nicht mehr selbst bestimmen, wo die eigene Software läuft. Verträge, Regulierungsbehörden und nicht zuletzt der Datenschutz erlauben es vielen Anwendern häufig nicht, auf Serverless umzusteigen.

Es ist daher naheliegend, eine Anwendung zu suchen, die auf vielen verschiedenen Plattformen funktioniert und gleichzeitig die Details der Infrastruktur für Entwickler so transparent wie möglich gestaltet. Entsprechend vielfältig sind bereits die Angebote, die FaaS auf die Kubernetes-Plattform übertragen. Als Open-Source-Plattformen existieren unter anderem OpenFaaS, Fission, OpenWhisk, Knative und Kubeless. Letzteres wird im Folgenden exemplarisch näher betrachtet.

Funktionen und Trigger

Die zentralen Elemente einer FaaS-Anwendung sind Funktionen und Trigger. Funktionen stellen die eigentliche Logik dar. Sie lassen sich durch unterschiedliche Ereignisse (Trigger) aufrufen, wobei dieselbe Funktion mit verschiedenen Arten von Ereignissen zurechtkommt.

Funktionen lassen sich in Kubeless in unterschiedlichsten Programmiersprachen implementieren. Um dies zu ermöglichen, führt Kubeless ein weiteres Element ein: die Runtime. Das ist ein Container, der sich an bestimmte Konventionen hält, mit denen er den Quellcode der Funktion übersetzen und ausführen kann. Er kümmert sich auch um die Installation etwaiger externer Bibliotheken.

Kubeless liefert in der aktuellen Version (v1.0.5) Runtimes für die folgenden Sprachen und Umgebungen mit:

Das Erstellen eigener Runtimes ist gut beschrieben. Sie können dann in der Konfiguration hinterlegt werden und stehen für neue Funktionen zur Verfügung. Dadurch eröffnen sich interessante Optionen für eigene Runtimes, die neben weiteren Programmiersprachen auch zusätzliche Funktionen mitbringen können.

Alle Funktionen verfügen über ein einheitliches, relativ einfach aufgebautes Interface. Beim Aufruf erhält eine Funktion zwei Parameter: Event und Context.

Der Parameter Event enthält eine Reihe von Metadaten wie den Aufrufzeitpunkt, die Art des Events und eine ID. Vor allem sind darin die eigentlichen Daten des Events enthalten. Sie sind üblicherweise sehr individuell, weshalb hier als Typ einfach ein String übergeben wird.

Der Parameter Context liefert eine Reihe weiterer Metadaten der Laufzeitumgebung. Dazu zählen der Name, unter dem die Funktion registriert ist, ihre Runtime und Angaben über Timeouts und Speicherlimits. Der Rückgabewert der Funktion ist wiederum ein String, den die Runtime auswerten kann.

Über Trigger lassen sich die in einer Runtime containerisierten Funktionen aufrufen.

Als Trigger für die Funktionen bietet Kubeless im derzeitigen Release an: HttpTrigger, CronjobTrigger und MessageTrigger. Die HttpTrigger hören auf HTTP-Aufrufe und leiten sie an die entsprechende Funktion weiter. Dabei nutzen sie die Infrastruktur von Kubernetes und richten für den Trigger die entsprechenden HTTP-Routing-Regeln in Kubernetes in Form einer Ingress-Definition ein. Es lassen sich sowohl virtuelle Hostnamen, Pfade und die HTTP-Methoden als auch eine REST API konfigurieren.

CronjobTrigger benutzen das in Kubernetes bereits vorhandene Objekt des Cronjob, um regelmäßige Aufrufe einer Funktion zu realisieren. Die Definition der Zeitpunkte folgt dabei dem zwar kryptischen, aber auch schon seit Jahrzehnten etablierten Muster der Cron- Expression. Schließlich lassen sich Funktionen auch über einen Message-Bus auslösen. Dazu existieren aktuell zwei Implementierungen. Die derzeit bevorzugt eingesetzte nutzt Apache Kafka und die andere Implementierung baut auf NATS, das als Projekt der Cloud Native Computing Foundation (CNCF) im Kubernetes-Umfeld zunehmend populär wird.

Im Vergleich zu vielen Cloud-basierten FaaS-Ansätzen ist die Anzahl der Trigger recht überschaubar. Das Erstellen eigener Trigger ist allerdings ebenfalls recht gut dokumentiert, sodass sich spezifische Anforderungen in der eigenen Umgebung umsetzen lassen.

Komponenten von Kubeless

Kubeless besteht aus einer Reihe von serverseitigen Komponenten und einem Kommandozeilenwerkzeug namens kubeless. Dabei handelt es um ein statisches Binary, das sich über die Release-Seite des Github-Projekts beziehen lässt. Alternativ wird auf dem Mac auch die Installation mit dem Paketmanager Homebrew unterstützt.

Die Installation der Serverkomponenten setzt einen beliebigen Kubernetes-Cluster voraus. Abgesehen von ausreichenden Berechtigungen zur Anlage der entsprechenden API-Objekte gibt es keine Vorbedingungen. Daher lässt sich Kubeless auch problemlos in einem lokal laufenden minikube ausprobieren.

Kubeless stellt die notwendigen Kubernetes-YAML-Dateien in seinem Repository zur Verfügung. Die Installation erfolgt in einen eigenen Namespace, der normalerweise “kubeless” heißt. Dabei werden die folgenden Objekte installiert:

Die Custom Resource Definitions erstellen eigene Kubernetes-API-Objekte für Funktionen und Trigger. Damit lassen sich diese wie jedes andere Kubernetes-Objekt über die API und die Kommandozeile von Kubernetes abfragen und anlegen.

Der Kubeless-Controller-Manager fasst die Controller zusammen, die für die Auswertung der Custom Resources zuständig sind. Damit lässt sich gewährleisten, dass Funktionen und Trigger nach ihrer Definition in Pods gestartet werden.

In Clustern mit aktivem Role Based Access Control (RBAC) lassen sich vordefinierte Serviceaccounts und Rollen einrichten. Für Cluster, die RBAC nicht nutzen, gibt es spezifische YAML-Dateien.

Schließlich ist noch eine ConfigMap zu installieren, die eine Reihe von Parametern für die Laufzeit von Kubeless beinhaltet. In der ConfigMap findet sich unter anderem auch die Liste aller verfügbaren Runtimes, die sich durch eigene Runtimes ergänzen lässt.

Die Trigger für Kafka- beziehungsweise NATS-Messages sind in der ursprünglichen Installation noch nicht enthalten. Entwickler müssen sie separat installieren. Auch hierfür existieren allerdings vorgefertigte Kubernetes-YAML-Dateien. In beiden Fällen ist auch eine Installation des jeweiligen Message-Brokers notwendig. Kubeless bietet in seinen Repositories für die Installation von Kafka einfache Kubernetes-Konfigurationen an. Im Fall von NATS ist auf den existierenden Operator zu verweisen.

Hello World

Das “Hello World”-Beispiel von Kubeless ist eine einfache Python-Funktion, die ihre Parameter loggt und die übergebenen Daten zurückgibt:

def hello(event, context):
    print event
    return event['data']

Vorausgesetzt, dass diese Funktion in einer Datei namens helloworld.py abgelegt ist, lässt sie sich mit dem folgenden Kommando auf der Kubeless-Kommandozeile als Funktion installieren:

$ kubeless function deploy hello --runtime python2.7 \
--from-file helloworld.py \		
--handler helloworld.hello

Kubeless legt damit ein Objekt für die Funktion an und lädt die Datei helloworld.py als ConfigMap in Kubernetes hoch. Daraufhin wird der Kubeless Function Controller aktiv und erzeugt einen Pod, mit dem unter dem Parameter runtime genannten Container. Dieser wiederum liest die Daten aus der ConfigMap und initialisiert sich entsprechend. Anschließend steht die Funktion zum Aufruf bereit. Noch fehlt ein entsprechender Trigger, aber zu Testzwecken lässt sich die Funktion manuell mit folgendem Kommando aufrufen:

$ kubeless function call hello --data "Hello World"

Als Ergebnis gibt sie wiederum “Hello World” aus. Der Aufruf findet sich ebenfalls im Log der Funktion, das sich mittels

$ kubectl logs -l function=hello

aufrufen lässt.

Im nächsten Schritt folgt ein passender Trigger. Der lässt sich beispielsweise für HTTP einrichten. Dazu dient folgendes Kubeless-Kommando:

$ kubeless trigger http create hello-trigger --function-name hello

Basierend auf diesem Trigger wird ein Kubernetes Ingress angelegt. Auf einer minikube- Installation sieht das so aus:

$ kubectl get ing
NAME HOSTS ADDRESS PORTS AGE hello-trigger hello.192.168.99.101.nip.io 10.0.2.15 80 8m

Kubeless nutzt per default URLs der Form ..nip.io. Nip.io ist ein Dienst, der bei DNS-Abfragen die zuvor notierte IP-Adresse zurückgibt. Was in diesem Fall für einen Kubernetes-Ingress, der einen Hostnamen voraussetzt, recht praktisch ist. Selbstverständlich lassen sich mit der Option —hostname bei der Erstellung des Triggers auch andere Namen angeben. Die Funktion lässt sich anschließend per HTTP beispielsweise mit curl aufrufen:

$ curl hello.192.168.99.101.nip.io -d "Hello World" Hello World

Die Definition der Funktionen unterscheidet sich je nach Programmiersprache deutlich. Ein guter Ausgangspunkt für die Details der jeweiligen Runtime zu einer Programmiersprache sind die Beispiele auf GitHub.

Komplexere Funktionen benötigen neben dem eigentlichen Quellcode oft noch externe Abhängigkeiten. Wie die einzubinden sind, ist ebenfalls von der jeweiligen Runtime abhängig. Im Falle von Python reicht eine requirements.txt. Sie enthält die Liste aller Python-Pakete, die eingebunden werden sollen. Für Node.js ist es eine package.json, für die Java-Runtime ist es eine pom.xml im Maven-Format, die von einer bestimmten Parent-Pom ableitet. Auch dieses Vorgehen lässt sich gut anhand der genannten Beispiele nachvollziehen.

Skalierung

Ein großer Vorteil von Serverless-Umgebungen ist die Tatsache, dass sich Entwickler nicht um die Skalierung ihrer Funktion kümmern müssen. Die Aufgabe übernimmt die Infrastruktur automatisch. Bei Angeboten eines Cloud-Providers ist es vollkommen transparent, wie die Skalierung umgesetzt wird. Bei einem Ansatz basierend auf Kubernetes und Kubeless kommt klassisches Autoscaling zum Einsatz. Hierbei ist zu beachten, dass diese Skalierung auf zwei Ebenen stattfindet. Die eine ist die Skalierung der Anzahl der Instanzen einer Funktion, um durch Trigger ausgelöste Events abzuarbeiten. Sie ist begrenzt durch die Kapazität des Kubernetes-Clusters selbst. Auch hundert Instanzen einer Funktion können nicht weiter skalieren, wenn dem Kubernetes-Cluster nur wenige Nodes zur Verfügung stehen. Die Skalierung der Nodes ist die zweite Skalierungsebene, die stark von der konkreten Installation des Clusters abhängt.

Für die erste Ebene bietet Kubeless eine Unterstützung, die nur eine dünne Abstraktionsschicht über den entsprechenden Features von Kubernetes darstellt. Um Instanzen automatisch zu skalieren, ist zuerst ein Wert festzulegen, an dem sich messen lässt, ob eine Skalierung notwendig ist oder nicht. Das folgende Beispiel betrachtet die CPU- Auslastung. Die Definition der Funktion ist dazu um einen Parameter —cpu zu erweitern. Ein Update der weiter oben bereits definierten Funktion sieht dann so aus:

$ kubeless function update hello --cpu 100m

Die übliche Maßeinheit für die CPU-Nutzung in der Kubernetes-Welt sind millicores, also ein Tausendstel der Auslastung eines CPU-Kernes. Im Beispiel werden 100 millicores angefordert. Diese Einstellung vorausgesetzt, lässt sich eine automatische Skalierung für die Funktion einrichten. Das geschieht durch folgendes Kommando:

$ kubeless autoscale create hello --min 2 --max 100 --value 80

Damit richten Entwickler für die Funktion hello ein Autoscaling ein, bei dem mindestens 2 und maximal 100 Instanzen der Funktion existieren. Neue Instanzen werden dann erzeugt, wenn die durchschnittliche Auslastung der existierenden Instanzen 80 Prozent übersteigt.

Ein weiterer Aspekt bei der Skalierung ist die Initialisierung des Containers selbst. Im Default- Fall wird beim Start jeder Instanz der Basiscontainer der Runtime verwendet. Falls notwendig sind außerdem alle Abhängigkeiten zu laden und der Quellcode zu kompilieren. Das kann je nach Komplexität eine Weile dauern. Kubeless lässt sich aber auch so konfigurieren, dass für jede Funktion ein Docker-Image erzeugt wird. Dafür ist eine entsprechende Docker-Registry mit eventuellen Zugangsdaten zu registrieren. Dieses Vorgehen empfiehlt sich insbesondere bei größeren Funktionen, die öfter zu skalieren sind.

Ganz ohne Gedanken zur Skalierung, wie es die Nutzung der FaaS-Angebote bei Cloud- Providern verspricht, funktioniert es bei Kubeless also nicht. Dafür lässt sich die Skalierung aber recht feingranular beeinflussen, was abhängig vom individuellen Szenario auch ein Vorteil sein kann.

Monitoring

Alle Runtime-Container von Kubeless sind so konfiguriert, dass sie Monitoring-Daten im Format des populären Monitoring-Tools Prometheus zur Verfügung stellen. Ist Prometheus im Cluster installiert und entsprechend konfiguriert, werden die Daten der Funktionen direkt ausgelesen. Dazu gehören unter anderem Messwerte zu Aufrufhäufigkeit, der Anzahl von Fehlern, der Dauer der Request-Verarbeitung und der CPU-Belastung.

Beim Monitoring baut Kubeless auf das verbreitete Prometheus-Format.

Häufig nutzen Anwender Grafana als Dashboard, um die Monitoring-Daten aus Prometheus sichtbar zu machen. Es existiert eine Beispielkonfiguration, wie sich auf diese Art ein Dashboard für Kubeless erstellen lässt.

Kubeless UI

Alle bisherigen Ausführungen bezogen sich auf Kubeless aus Sicht der Kommandozeile. Um einen zu Cloud-FaaS-Angeboten vergleichbaren Komfort umzusetzen, existiert eine auf React basierende Benutzeroberfläche für Kubeless. Sie lässt sich separat im Cluster installieren und bietet die rudimentäre Möglichkeit, Funktionen in verschiedenen Sprachen anzulegen und zu verwalten. Die Funktionalität ist dabei noch recht einfach gehalten und die Oberfläche wird auch nicht sehr intensiv weiterentwickelt. Eine zu Cloud-Diensten vergleichbar einfache Bedienung darf man hier nicht erwarten.

Fazit

Kubeless als FaaS auf Kubernetes ist ein interessantes Konzept. Es nutzt die Aspekte von Kubernetes optimal aus und bleibt dadurch flexibel. In der Kubernetes-Welt gibt es allerdings noch weitere Ansätze, die in eine Betrachtung aufgenommen werden sollten. Unter diesen ist sicherlich Knative der Kandidat mit den höchsten Ansprüchen. Es soll eine standardisierte Basis für Serverless Frameworks aber auch noch viele weitere Funktionalitäten schaffen. Einige Angebote gehen bereits diesen Weg, Kubeless bisher jedoch nicht. Welches FaaS- Framework schließlich auf Kubernetes zum Einsatz kommen soll, will anhand der individuellen Anforderungen genau überlegt sein. Es hängt bei der derzeitigen Entwicklungsgeschwindigkeit der verschiedenen Ansätze außerdem stark vom Zeitpunkt der Entscheidung ab.

Es stellt sich grundsätzlich die Frage, ob es eine Lösung auf Basis von Kubernetes sein soll oder doch lieber das FaaS-Angebot eines Cloud-Anbieters. Ist der Betrieb in der Cloud aus “politischen” oder rechtlichen Erwägungen heraus kein Problem, so punktet dieser Ansatz mit einer transparenten Skalierung, einem lukrativen Abrechnungsmodell, einer umfassenden Integration in weitere Cloud-Dienste und ausgereiften Benutzeroberflächen.

Wenn der Betrieb auf Basis von Kubernetes bereits etabliert ist, liefert Kubeless gegenüber der Cloud jedoch einige interessante Eigenschaften. Der eigene Einfluss auf die Fähigkeiten des FaaS ist höher. So lassen sich eigene Runtimes gestalten, die exotische Sprachen unterstützen, oder auch weitere Erleichterungen für Entwickler der Funktionen mitbringen. Die Skalierung der Funktionen lässt sich granularer beeinflussen als bei einem Cloud-Anbieter. Schließlich kann auch die Möglichkeit der Einbindung eigener Trigger ein ausschlaggebendes Argument für den Einsatz von Kubeless sein.