Ab in die Wolken!

Function as a Service mit AWS Lambda & Knative: Kubernetes als Multi-Cloud-Betriebssystem

Patrick Arnold

© Shutterstock / Natascha Kaukorat

Viele Unternehmen werden zukünftig versuchen, ihre IT-Infrastruktur mithilfe der Cloud wachsen zu lassen oder gar komplett in die Cloud zu verlagern – erste Vorstöße gibt es dazu schon. Häufig wird gerade bei größeren Unternehmen die „Multi Cloud“ gefordert. Nun gibt es auch im Hinblick auf Serverless einige Möglichkeiten, einen Mulit-Cloud-Betrieb zu erreichen. In diesem Artikel möchte ich einen kurzen Einblick geben, wie man mittels AWS Lambda eine Function zur Verfügung stellt und wie man das Ganze mit Knative Cloud-unabhängig machen kann.

Was war noch gleich diese „Multi Cloud“?

Unter einer Multi Cloud versteht man die Nutzung von mehreren Cloud-Providern/-Plattformen mit der Besonderheit, dass es sich für den Anwender wie eine einzige Cloud anfühlt. Meist versucht man in diese Evolutionsstufe des Cloud Computing zu kommen, um eine Unabhängigkeit von einzelnen Cloud-Providern zu erreichen.

Durch die Nutzung von mehreren Cloud-Providern wird die Ausfallsicherheit und die Verfügbarkeit gesteigert und natürlich die Nutzung von Technologien möglich, die einzelne Cloud-Provider nicht zur Verfügung stellen. Ein Beispiel hierfür wäre, dass es relativ schwer wird, seinen Alexa-Skill auf der Cloud zu deployen, wenn man sich entschlossen hat, Microsoft Azure als Provider zu nutzen. Außerdem bietet uns eine Multi-Cloud-Lösung die Möglichkeit, Anwendungen mit einer hohen Anforderung an Rechenleistung, Speicherbedarf und Netzwerkleistung bei einem Cloud-Provider zu hosten, der diese Anforderungen erfüllt. Weniger kritische Anwendungen können wiederum von einem kostengünstigeren Provider gehostet werden, um IT-Kosten zu senken.

Natürlich bringt die Multi Cloud nicht nur Vorteile mit sich. Durch die Verwendung von mehreren Cloud-Providern wird die Gestaltung der Infrastruktur deutlich komplexer und schwieriger zu managen. Die Anzahl der Fehlerquellen kann sich erhöhen und die Verwaltung für die Abrechnung der einzelnen Cloud-Provider wird aufwendiger.

Diese Vor- und Nachteile sollte man vor einer Entscheidung genau gegenüberstellen. Sollte man feststellen, dass man keine Angst davor haben muss, sich in die Abhängigkeit eines Cloud-Providers zu geben, so sollte man den Aufwand eher in die Nutzung der Cloud Services investieren.

Was ist Function as a Service?

Im Jahr 2014 ist das Function-as-a-Service-Konzept (FaaS) das erste Mal auf dem Markt erschienen. Damals wurde das Konzept von hook.io vorgestellt. In den folgenden Jahren sprangen alle großen Player der IT auf den Zug mit auf, z.B. mit AWS Lambda, Google Cloud Functions, IBM OpenWhisk oder auch Microsoft Azure Functions. Die Charakteristiken von solch einer Funktion sind:

  • Server, Netzwerk, Betriebssystem, Storage usw. sind vom Entwickler abstrahiert.
  • Die Abrechnung erfolgt nutzungsabhängig und sekundengenau.
  • FaaS ist zustandslos, d.h. für eine Haltung von Daten oder Zuständen wird eine Datenbank oder ein Dateisystem benötigt.
  • Sehr gut skalierbar.

Doch welche Vorteile bietet einem das Ganze? Der wohl größte Vorteil ist, dass sich der Entwickler nicht mehr um die Infrastruktur kümmern, sondern nur einzelne Funktionen ansprechen muss. Die Services sind sehr gut skalierbar und ermöglichen eine exakte und nutzungsabhängige Abrechnung. Dadurch kann man eine maximale Transparenz an Produktkosten erreichen. Die Logik der Anwendung kann in einzelne Funktionen geteilt werden, dadurch ist man deutlich flexibler bei der Umsetzung von weiteren Anforderungen. Die Funktionen können dabei in unterschiedlichen Szenarien eingesetzt werden. Häufig zu finden sind:

  • Web-Requests
  • Geplante Jobs und Tasks
  • Events
  • Manuell gestartete Tasks

FaaS mit AWS Lambda

Als ersten Schritt erstellen wir uns ein neues Maven-Projekt. Um die AWS Lambda-spezifischen Funktionalitäten nutzen zu können, müssen wir die in Listing 1 zu sehende Dependency in unserem Projekt hinzufügen.

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-lambda-java-core</artifactId>
    <version>1.2.0</version>
</dependency>

Als nächsten Schritt müssen wir einen Handler implementieren, der den Request entgegennimmt und eine Response an den Aufrufer zurückmeldet. Hier gibt es in der Core Dependency zwei Stück, den RequestHandler und den RequestStreamHandler. Wir werden in unserem Beispielprojekt den RequestHandler nutzen und Inbound sowie Outbound als String deklarieren (Listing 2).

public class LambdaMethodHandler implements RequestHandler<String, String> 
{

    public String handleRequest(String input, Context context) {
        context.getLogger().log("Input: " + input);
        return "Hello World " + input;
    }
}

Führt man dann einen Maven Build aus, so wird eine .jar-Datei erzeugt, die dann zu einem späteren Zeitpunkt deployt werden kann. Bereits hier kann sehr gut erkannt werden, dass durch die LambdaDependency eine feste Verdrahtung zu AWS entsteht. Wie bereits am Anfang erwähnt, muss das nicht schlecht sein, jedoch sollte diese Entscheidung bewusst getroffen werden. Möchte man jetzt mit der Funktion eine AWS-spezifische Datenbank bedienen, so wächst diese Kopplung stärker.

Deployment der Function

Für das Deployment einer Lamba-Funktion gibt es mehrere Wege, entweder manuell über die AWS-Konsole oder aber automatisiert über einen CI-Server. Für dieses Beispielprojekt wurde der automatisierte Weg, mithilfe von Travis CI, gewählt.

Travis ist ein Cloud-basierter CI-Server, der unterschiedliche Sprachen und Zielplattformen unterstützt. Der große Vorteil von Travis ist, dass es innerhalb von Sekunden mit dem eigenen GitHub-Account verbunden werden kann. Was brauchen wir hierfür? Nun, Travis wird über eine Datei namens .travis.yaml konfigruiert. In unserem Fall brauchen wir Maven für den Build sowie die Verbindung zu unserer auf AWS gehosteten Lambda Function für das Deployment.

language: java 
jdk: 
- openjdk8 
script: mvn clean install 
deploy: 
    provider: lambda 
    function_name: SimpleFunction 
    region: eu-central-1 
    role: arn:aws:iam::001843237652:role/lambda_basic_execution 
    runtime: java8 
    andler_name: de.developerpat.handler.LambdaMethodHandler::handleRequest 
    access_key_id: 
        secure: {Dein_Access_Key} 
    secret_access_key: 
        secure: {Dein Secret Access Key} 

Wie man der Konfiguration in Listing 3 entnehmen kann, braucht man für ein erfolgreiches Deployment folgende Konfigurationsparameter:

  • Provider  Dieser ist im Deploy-Schritt bei Travis der Zielprovider, auf dem deployt werden soll.
  • Runtime  Die Runtime, die benötigt wird, um das Deployment ausführen zu können.
  • Handler-Name  Hier müssen wir unseren Request Handler angeben, mit Package-, Klassen- und Methodennamen.
  • Amazon Credentials  Zu guter Letzt müssen wir noch die Credentials einpflegen, damit der Build die Funktion deployen kann. Dies kann mit folgenden Befehlen erledigt werden:AccessKey: travis encrypt "Dein AccessKey" –add deploy.access_key
    Secret_AccessKey: travis encrypt "Dein SecretAccessKey" –add deploy.secret_access_key

    Sollten diese Credentials nicht über die Konfiguration übermittelt werden, so sucht Travis nach den Environment-Variablen AWS_ACCESS_KEY und AWS_SECRET_ACCESS_KEY.

Zwischenfazit

Der Aufwand, um eine Lambda-Funktion zur Verfügung zu stellen, ist relativ gering. Die Schnittstellen zur Lambda-Implementierung sind klar und einfach zu verstehen. Da AWS die Lambda Runtime á la SaaS zur Verfügung stellt, müssen wir uns nicht um Installation und Konfigurationen kümmern und bekommen einige Dienste für den Betrieb (z.B. Logging und Monitoring) out of the Box mit dazu. Natürlich geht man damit eine doch sehr starre Bindung zu AWS ein. Möchte man zum Beispiel aus irgendwelchen Gründen zu Microsoft Azure oder auf die Google Cloud wechseln, so muss die Funktion migriert und entsprechend auch im Code angepasst werden.

FaaS mit Knative

Das Open-Source-Projekt Knative wurde 2018 ins Leben gerufen. Die Gründungsväter waren Google und Pivotal, aber mittlerweile spielt auch dort die ganze IT-Prominenz wie z.B. IBM und Red Hat mit. Das Knative Framework basiert auf Kubernetes und Istio, welche die Anwendungsumgebung (auf Containern basierend) und ein erweitertes Netzwerkrouting zur Verfügung stellen. Knative erweitert Kubernetes um eine Reihe von Middleware-Komponenten, die für den Aufbau moderner und containerbasierten Anwendungen unerlässlich sind. Da Knative auf Kubernetes aufsetzt, ist es möglich, die Anwendungen lokal, in der Cloud oder in einem Rechenzentrum eines Drittanbieters zu hosten.

Pre-Steps

Egal ob AWS, Microsoft, IBM oder Google: mittlerweile bietet jeder große Cloud-Provider ein „managed Kubernetes“ an. Für Testzwecke kann man auch einfach ein lokales Minikube oder auch Minishift verwenden. Für meinen Use Case verwende ich das bei Docker for Windows mitgelieferte Minikube, dieses lässt sich einfach über das Docker UI aktivieren (siehe Abbildung 1).

Abb. 1: Aktivierung von Kubernetes via Docker UI

Das Standard Minikube allein bringt uns jedoch leider noch nichts. Wie installieren wir also nun Knative? Bevor wir Knative installieren können, benötigen wir erst einmal Istio. Die Installation von Istio und Knative ist auf zwei Wegen möglich, einen automatisierten und einen manuellen.

In der Dokumentation von Knative gibt es einzelne Installationsschritte für unterschiedliche Cloud-Provider. Hierbei ist zu sagen, dass sich der spezifisch auf den jeweiligen Cloud-Provider beziehende Teil auf die Provisionierung eines Kubernetes Clusters begrenzt. Nach der Installation von Istio ist die Prozedur für alle Cloud-Provider gleich. Die manuellen Schritte die hierfür nötig sind, findet man in der Knative-Dokumentation auf GitHub.

Für den automatisierten Weg hat Pivotal ein Framework namens riff veröffentlicht. Dieses Framework wird uns später auch beim Entwickeln über den Weg laufen. riff soll die Entwicklung von Knative-Anwendungen vereinfachen und alle Kernkomponenten unterstützen. Aktuell ist es in Version 0.2.0 verfügbar, für die Nutzung wird ein für den richtigen Cluster konfiguriertes kubectl vorausgesetzt. Hat man dieses vorliegen, so kann man mit dem Befehl aus Listing 4 Istio und Knative über das CLI installieren.

>>riff system install

Achtung! Wenn ihr Knative bei euch lokal installiert, so muss der Parameter --node-port hinzugefügt werden. Nach dem Ausführen dieses Befehls sollte innerhalb von ein paar Minuten die Meldung riff system install completed successfully erscheinen.

Nun haben wir unsere Anwendungsumgebung mit Kubernetes, Istio und Knative aufgesetzt. Aus meiner Sicht dank riff relativ schnell und einfach.

Entwicklung einer Funktion

Für die Entwicklung einer Funktion, die dann auf Knative gehostet wird, gibt es nun sehr viele unterstützte Programmiersprachen. Ein paar Beispielprojekte finden sich auf der GitHub-Präsenz von Knative. Ich habe mich für meinen Use Case dafür entschieden, das Project Spring Cloud Function zu nutzen. Dieses unterstützt speziell, Business Value als Functions auszuliefern, bringt aber dennoch alle Vorteile des Spring-Universums mit sich (Autokonfiguration, Dependency Injection, Metrics etc.).

Als ersten Schritt fügt man die in Listing 4 beschriebene Dependency in seiner pom.xml ein.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-function-web</artifactId>
</dependency>

Haben wir das erledigt, können wir nun unsere kleine Funktion schreiben. Um die Lambda Function und die Spring Function vergleichbar zu machen, werden wir dieselbe Logik implementieren. Da unsere Logik sehr minimal ist, werde ich sie in SpringBootApplication selbst implementieren. Natürlich kann man, wie man es mit Spring gewohnt ist, die Dependency Injection auch ganz normal verwenden. Für die Implementierung der Funktion nutzen wir die Klasse java.util.function.Function</code. (Listing 5). Diese wird als Objekt unserer hello-Methode zurückgegeben. Wichtig ist, dass die Methode mit der @Bean-Annotation versehen wird, da sonst keine Veröffentlichung des Endpoints stattfindet.

package de.developerpat.springKnative;

import java.util.function.Function; 
import org.springframework.boot.SpringApplication; 
import org.springframework.boot.autoconfigure.SpringBootApplication; 
import org.springframework.context.annotation.Bean; 

@SpringBootApplication 
public class SpringKnativeApplication { 

    @Bean 
    public Function<String, String> hello(){ 
        return value -> new StringBuilder("Input: " + value).toString(); 
    } 

    public static void main(String[] args) { 
        SpringApplication.run(SpringKnativeApplication.class, args); 
    } 
} 

Knative Deplyoment

Damit die Bereitstellung unserer Funktion erfolgreich ist, müssen wir zuvor unseren Namespace mit riff initialisieren. Hierbei wird der Benutzername und das Secret unser Docker Registry hinterlegt, damit beim Erzeugen der Funktion das Image gepusht werden kann. Denn wir müssen nicht selbst das Image schreiben, das übernimmt praktischerweise riff für uns. Hierfür werden einige CloudFoundry Buildpacks genutzt. In meinem Fall verwende ich als Docker Registry den Dockerhub. Die Initialisierung kann mit folgendem Befehl vorgenommen werden:

>>riff namespace init default --dockerhub $DOCKER_ID

Sollte man einen anderen Namespace als default initialisieren wollen, so tauscht man einfach die Bezeichnung aus. Nun wollen wir unsere Funktion aber deployen. Natürlich haben wir alles in unser GitHub Repository eingecheckt und wollen nun diesen Stand auch bauen und anschließend dann deployen. Nachdem riff unser Maven-Projekt gebaut hat, soll ein Docker Image entstehen und in unser DockerHub Repository gepusht werden. Folgender Befehl erledigt das für uns:

>>riff function create springknative --git-repo 
https://github.com/developerpat/spring-cloud-function.git --image developerpat/springknative:v1 –-verbose 

Möchten wir das Ganze von einem lokalen Pfad aus machen, dann tauschen wir --git-repo durch --local-path und den entsprechenden Pfad. Guckt man sich jetzt den Verlauf des Befehls an, so kann man schön erkennen, dass das Projekt analysiert, mit dem richtigen Buildpack erstellt und am Ende das fertige Docker Image gepusht und deployt wird.

Aufruf der Funktion

Nun wollen wir auch, wie es auch bei AWS über die Konsole relativ einfach möglich ist, testen, ob der Aufruf gegen unsere Funktion funktioniert. Das können wir dank unseres riff CLIs ganz einfach wie folgt:

>>riff service invoke springknative --text -- -w '\n' -d Patrick 
curl http://localhost:32380/ -H 'Host: springknative.default.example.com' -H 
'Content-Type: text/plain' -w '\n' -d Patrick 
Hello Patrick 

Mit dem invoke-Befehl kann man sich einen curl nach Wunsch generieren und ausführen lassen. Wie man nun sehen kann, läuft unsere Funktion einwandfrei.

Kann Knative mit einem integrierten Amazon Lambda mithalten?

Aufgrund der Tatsache, dass Knative auf Kubernetes und Istio basiert, stehen einige Funktionalitäten bereits nativ zur Verfügung:

  • Kubernetes
    • Skalierung
    • Redundanzen
    • Rolling out & back
    • Health Checks
    • Service Discovery
    • Config & Secrets
    • Resilienz
  • Istio
    • Logging
    • Tracing
    • Metrics
    • Failover
    • Circuit Breaker
    • Traffic Flow
    • Fault Injection

Diese Bandbreite an Funktionalität kommt dem Funktionumfang einer Function-as-a-Service-Lösung wie AWS Lambda schon sehr nahe. Ein Manko gibt es jedoch: Es gibt keine UIs, um die Funktionalität zu nutzen. Möchte man die Monitoring-Informationen in einem Dashboard visualisiert haben, ist es notwendig, sich selbst eine Lösung dafür aufzusetzen (z.B. Prometheus). Hierfür gibt es einige Tutorials und Hilfestellungen in der Knative-Doku (/*TODO*/). Bei AWS Lambda bekommt man diese UIs von Haus aus mit dazu und muss sich um nichts mehr kümmern.

Multi-Cloud-Fähigkeit

Alle großen Cloud-Provider bieten mittlerweile ein „managed Kubernetes“ an. Da Knative als Basis-Umgebung Kubernetes verwendet, ist es vollkommen unabhängig vom Kubernetes-Anbieter. So ist es kein Problem, seine Applikationen in einer enorm geringen Zeit von einer Umgebung in eine andere zu migrieren. Der größte Aufwand dabei ist, die Zielumgebung beim Deployment in der Konfiguration zu ändern. Aufgrund dieser Tatsachen können einfache Migrations- und Exit-Strategien entwickelt werden.

Fazit

Knative entspricht nicht allen Punkten aus dem FaaS-Manifest. Ob es denn dann überhaupt FaaS ist? Aus meiner persönlichen Sicht, Ja. Für mich ist der wichtigste Punkt aus dem FaaS-Manifest, dass keine Maschinen, Server, VMs oder Container im Programmiermodell sichtbar sein dürfen. Dieser Punkt wird von Knative in Verbindung mit dem riff CLI erfüllt.

Geschrieben von
Patrick Arnold
Patrick Arnold
Patrick Arnold ist IT-Consultant bei der Pentasys AG. Für ihn ist Technologie wie Spielzeug für kleine Kinder, er ist süchtig nach Neuem. Nach einer Oldschool-Ausbildung im Mainframebereich wechselte er in die dezentrale Welt. Sein technologischer Schwerpunkt sind moderne Architektur- und Entwicklungsansätze wie Cloud, API-Management, Continuous Delivery, DevOps und Microservices.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: