Serverless Development

#JAVAPRO #Serverless

Zu Beginn waren sie noch eine Kuriosität. Mittlerweile haben sich Serverless-Applications so stark verbreitet, dass sich zu Beginn der Software-Entwicklung häufig die Frage stellt: Kann ich meine Anwendung serverless entwickeln? Dieser Artikel gibt einen Überblick über die Vorteile dieses Architekturstils und erläutert, welche Besonderheiten und Möglichkeiten AWS-Lambda für die Java-Welt bietet.

Anwendungsentwickler sollen mit hoher Produktivität an Fachlogik arbeiten. Deswegen sucht seit jeher die Software-Industrie immer wieder nach dem richtigen Schnitt bei der Abstraktion von notwendigen aber nicht wertbringenden Teilen des Hard- und Software-Stacks. Ziel ist es, dass Entwickler nur die Artefakte zur Ausführung auf einer Zielplattform erstellen müssen. In der Java-Welt sind in der Praxis unterschiedliche Schnitte zu finden wie WARs und EARs. Ganz im Sinne des Law-of-Leaky-Abstractions kommt in fast jedem Software-Entwicklungsprojekt irgendwann der Moment, bei der eine Abstraktion Einschränkungen mit sich bringt und sich folglich als ungeeignet erweist. Meist wirkt sich das auf nichtfunktionale Anforderungen, wie Skalierbarkeit, Wartbarkeit und Interoperabilität, aus. Mit Containern
haben Entwickler ein Format gefunden, das hohe Portabilität erlaubt und zugleich viele Freiheiten bei der Installation bietet, da der gemeinsame Nenner der Betriebssystem-Kernel und ein Dateisystem sind. Ein Container lässt sich dadurch leicht durch mehrere Stages schieben — vom Entwickler-Notebook bis zur Produktion — und eignet sich für verschiedene Anwendungstypen. Sie übertragen aber immer noch viel Verantwortung an den Entwickler, z.B. für den Server innerhalb des Containers, dessen Schnittstelle, das Event- oder Request-Management, Thread-Pools, Skalierung und Patch-Management ist.

Wenn diese Verantwortung nicht notwendig und nicht gewollt ist, sind Serverless-Applications die beste Wahl. Hierbei wird die Bereitstellung und Wartung von Servern an eine Serverless-Plattform übertragen. Dies betrifft nicht nur die Hardware, sondern auch die Software. Das lässt sich am leichtesten anhand von Fachlogik veranschaulichen: Ein Entwickler stellt nur Anwendungscode und dessen spezifische Abhängigkeiten als Funktion bereit. Der restliche Stack, von der Hardware über die Virtualisierung bis hin zur Runtime, ist in der Verantwortung der Plattform. Die Basis für Anwendungen ist die Runtime. Im Fall von Java die JVM.

Sowohl die Skalierung als auch die Kosten sind nicht abhängig von der Anzahl der Server, die für den Service bereitgestellt werden. Stattdessen steht die tatsächliche Nutzung im Mittelpunkt. Sie wird gemessen in Verbrauchseinheiten wie der Anzahl und Dauer der Anfragen, belegtem Speicher, Anzahl der Nachrichten oder ähnlichem. Sofern sich die Nutzung erhöht, steht die Kapazität entweder vollständig automatisch zur Verfügung oder eine Erweiterung wird durch administrative API-Aufrufe angestoßen. Letzteres lässt sich wiederum bei einer automatisierbaren Plattform leicht umsetzen durch die Verknüpfung von Messwerten mit Aktionen. Im umgekehrten Fall ist die automatische Verringerung der Kapazität entscheidend. Damit gilt das Prinzip: Man bezahlt nur das, was man verbraucht.

Durch den Fokus auf die Kapazität wird die Verantwortung für Ausfallsicherheit und Verfügbarkeit auch an die Plattform übertragen. Während man bei traditionellen Architekturen Lastverteilung und Redundanz mit mehreren Servern einplanen muss, deckt die Plattform diese Aufgaben bei Serverless-Applications durch die Verteilung der Server auf mehrere regionale oder weltweite Standorte ab.

Die Vorteile liegen auf der Hand: Dadurch, dass Entwickler eine erprobte Plattform verwenden, auf die sie sich verlassen können, bleibt mehr Zeit für die Entwicklung von Fachlogik. Kosten basieren auf der genutzten Kapazität und auf Einheiten des Verbrauchs. Nach beendeten Experimenten, beispielsweise beim Test neuer Software-Versionen, verbleiben also keine teuren Anschaffungen und Altlasten. Für die Berechnung der Wirtschaftlichkeit kann man die Kosten fast bis auf die Transaktionsebene schätzen und sie den entsprechenden
Einnahmen gegenüberstellen. Serverless-Applications erlauben damit eine echte Cost-Aware-Architecture. Kern von Serverless-Applications ist die Nutzung von Serverless-FaaS (Functions-as-a-Service) zur Verarbeitung von Events der Plattform. Dazu wird die Fachlogik in Form
von zustandslosen Funktionen von der Serverless-Plattform ausgeführt.

Serverless-Applications in Amazon-Web-Services

Die Amazon-Web-Services-Cloud (AWS) bietet mit AWS-Lambda Serverless-FaaS. Der Entwickler schreibt dazu Funktionen in populären Programmiersprachen wie Java, JavaScript, Python, Go oder C#. Im Falle von Java liegt der gesamte Stack vom Rechenzentrum über den Server bis zur Java-8-JVM in der Verantwortung von AWS. Der Entwickler verknüpft seine Lambda-Funktion mit den Events anderer AWS-Dienste.

Die wohl bekannteste Quelle für Events ist das Amazon-APIGateway. Im Zusammenspiel mit Lambda bildet es eine Serverless-Web-Application wie in (Abb. 1) dargestellt. Das API-Gateway nimmt HTTP-Anfragen eines Browsers zu API-Ressourcen entgegen und leitet sie als Events an Lambda weiter. Eine Lambda-Funktion wiederum verarbeitet unter Nutzung weiterer Datenquellen und Dienste der Plattform das Event. Ein Beispiel dafür ist Amazon-DynamoDB als vollständig verwaltete NoSQL-Datenbank. Schließlich findet das Ergebnis der Funktion über das API-Gateway als HTTP-Antwort den Weg zurück zum Browser. Für statische Inhalte wie HTML-Seiten, client-seitigem JavaScript und Bildern wird häufig Amazon-Simple-Storage-Service (S3) verwendet. S3 speichert in Buckets beliebige Dateien und liefert
diese über HTTP aus. Um HTTP-Antworten nach Möglichkeit zwischenzuspeichern und die Verbindung zum Browser zu optimieren, wird Amazon-CloudFront als Content-Delivery-Network (CDN) genutzt, das über hundert Edge-Locations in der ganzen Welt verwendet.

Serverless-Web-Application: Amazon-API-Gateway ist die HTTP-Schnittstelle für die Lambda-Funktion. (Abb. 1)

Serverless-Web-Application: Amazon-API-Gateway ist die HTTP-Schnittstelle für die Lambda-Funktion. (Abb. 1)

Amazon-API-Gateway ist nicht der einzige Service, der Events liefert, die von Lambda verarbeitet werden können. Es ergeben sich dadurch viele Arten von Serverless-Applications neben Web-Anwendungen. Weitere Beispiele sind:

  • Bei IoT-Backends leiten Regeln im AWS-IoT-Message-Broker bestimmte Events der Endgeräte an Lambda weiter. Ein Knopfdruck auf einem Gerät führt auf diesem Weg direkt beliebigen Code in Lambda aus.
  • Bei der Verarbeitung von Daten-Streams verarbeitet Lambda in Echtzeit Events. Dazu fragt der Lambda-Dienst die in Amazon-Kinesis-Streams gespeicherten Events ab und skaliert automatisch mit der Menge der Daten.
  • Lambda bildet die Logik eines Chatbots ab, z.B. für einen Alexa-Skill des Amazon-Echo.
  • Innerhalb der Edge-Locations von CloudFront manipuliert Lambda@Edge HTTP-Anfragen und -Antworten. So wird z.B. mit wenig Latenz Logik für A/B-Tests auf dem CDN ausgeführt, ohne die dahinter liegende Anwendung anzupassen.
  • In S3 ist das Hochladen einer Datei ebenfalls ein Event. Als Reaktion darauf läuft beispielsweise Code zur Medienkonvertierung von Videos in die Formate unterschiedlicher Endgeräte.
  • Events aus API-Zugriffen, Netzwerk und Monitoring dienen zur Automatisierung von Betriebsprozessen wie Reaktionen auf Security-Incidents, Lastveränderungen und Unverfügbarkeit.
  • Der Software-Entwicklungsprozess ist mit Diensten der Serverless-Plattform vom Git-Push in ein Repository über automatisierte Builds und Tests bis hin zum Deployment in Produktion automatisiert.

Im Fall der oben gezeigten Serverless-Web Application nutzt die Lambda-Funktion DynamoDB als Backend. Die Serverless-Plattform stellt darüber hinaus viele weiterer Dienste zur Verfügung, u.a. für das Senden von Push-Nachrichten oder die Nutzung von Queues. Aber auch relationale Datenbanken können als Backend dienen.

Ein einzelner Lambda-Aufruf ist in der Ausführungsdauer derzeit auf fünf Minuten beschränkt. Oft sind lange Laufzeiten aber nur notwendig, um auf einen langlaufenden Prozess wie eine Video-Konvertierung oder einen manuellen Bearbeitungsschritt zu warten. Zu diesem Zweck wird Zustands- und Workflow-Management mit AWS-Step-Functions umgesetzt. Wie in (Abb. 2) zu sehen, orchestriert der Dienst Aufgaben mit einem Zustandsautomaten. Damit ist die Aufteilung der Logik in kleine, wiederverwendbare Blöcke möglich und der Workflow ist nicht schwer veränderlich in Code gegossen. Außerdem wird Step-Functions in Verbrauchseinheiten wie Zustandsübergängen berechnet und spart so Kosten, da keine Lambda-Funtion aktiv auf einen Zustand warten muss. Eine Aufgabe wird in Lambda entwickelt oder in einem beliebigen Prozess, der in AWS, im eigenen Rechenzentrum oder in einer anderen Cloud läuft.

Workflow zur Extraktion von Metadaten und Konvertierung eines Bilds. (Abb. 2)

Workflow zur Extraktion von Metadaten und Konvertierung eines Bilds. (Abb. 2)

Lambda-Code benötigt keine AWS-Spezifika

Der Kontrakt einer Lambda-Funktion besteht aus einem Handler, der sich nur geringfügig zwischen den Runtimes unterscheidet. Wenn Java-8 als Runtime für eine Lambda-Funktion ausgewählt ist, kann die Funktion in allen Sprachen geschrieben werden, welche die JVM unterstützt. Neben Java ist also z.B. auch ein Programm in Scala oder JRuby möglich. Der minimale Kontrakt für eine synchrone Funktion in Java ist:

outputType handler-name(inputType input) { … }

Wie in (Listing 1) zu sehen, ist nur ein Eingabeparameter und ein Rückgabewert erforderlich. Lambda erzwingt also keine AWS-Spezifika in der Methode oder Klasse. Das Format zur Paketierung ist ein herkömmliches JAR und braucht keine weiteren Metadaten.

(Listing 1)
package com.example.serverless;
public class Greeter{
  public String greet(String name) {
    return „Hello “ + name;
  }
}

Der einfachste Weg zur Erstellung einer neuen Lambda-Funktion führt über die AWS-Management-Console. Nach dem Klick auf Create-Function erlauben Blueprints einen schnellen Start mit fertigem Code für häufige Anwendungen. Für das Hochladen des eigenen Java-Pakets führt der Weg über Author-fromscratch. (Abb. 3) enthält die Maske mit den Angaben für das obige Beispiel. Hier wird die Runtime, der Name und eine Rolle in AWS-Identity and Access-Management (IAM) festgelegt. Die Rolle definiert feingranular, welche Berechtigungen die Funktion bei der Ausführung hat, um andere AWS-Dienste aufzurufen. Best-Practice ist, keine AWS-Zugangsdaten im Code abzulegen.
Stattdessen werden sie dynamisch aus der Rolle gelesen. AWS-SDKs machen das automatisch. Für dieses Beispiel ist außer Amazon-CloudWatch-Logs kein anderer Dienst beteiligt. Mit der Berechtigung für CloudWatch-Logs protokolliert der Lambda-Dienst die Systemausgaben der Funktion.

Erstellung einer neuen Lambda-Funktion in der Konsole. Hier wird auch eine neue IAM-Rolle angelegt. (Abb. 3)

Erstellung einer neuen Lambda-Funktion in der Konsole. Hier wird auch eine neue IAM-Rolle angelegt. (Abb. 3)

Nach der Erstellung der Funktion erscheint die Maske in welcher der Code hochgeladen und der Handler bestimmt wird. Der Handler enthält den Namen der Klasse und Methode, die bei einem eingehenden Event aufgerufen wird. In diesem Beispiel ist das  com.example.serverless.Greeter::greet. Nachdem der Code hochgeladen und gespeichert ist, lässt er sich direkt aus der Konsole testen über den Test Button. Es stehen dazu beispielhafte Event-Parameter von verschiedenen AWS-Diensten im JSON-Format zur Auswahl. In diesem Fall reicht ein einfaches Event mit einem Namen in Anführungszeichen aus.

Nach dem Test sind in der Lambda-Konsole wie in (Abb. 4) das Ergebnis der Funktion, die Ausführungsdauer und eventuelle Systemausgaben des Aufrufs zu sehen.

Ein Test (1) mit Ergebnis (2), Ausführungsdauer (3), abgerechneter Dauer (4) und Systemausgaben (5). (Abb. 4)

Ein Test (1) mit Ergebnis (2), Ausführungsdauer (3), abgerechneter Dauer (4) und Systemausgaben (5). (Abb. 4)

Aus der Ausführungsdauer und dem Speicher, welcher der Funktion zugewiesen wurde, errechnen sich die Kosten des Aufrufs. Mit größerer Speicherzuweisung wachsen auch die anderen bereitgestellten Ressourcen wie CPU und Netzwerkbandbreite. Damit sind selbst Lambda-Ausführungen mit mehr Speicher schneller, wenn sie nicht speichergebunden sind. Es lohnt sich also, verschiedene Konfigurationen auszuprobieren, um die geringsten Kosten zu erzielen. AWS-Lambda-Power-Tuning automatisiert Ausführungen derselben Funktion mit unterschiedlichen Konfigurationen, um leicht eine geeignete Variante zu finden.

Java-Bibliotheken und Binaries in Lambda verwenden

Lambda erlaubt die Nutzung beliebiger Java-Bibliotheken. Die wichtigsten werden im Folgenden kurz dargestellt.

Um bereits bei der Kompilierung sicherzustellen, dass die Methodensignatur kompatibel mit dem Lambda-Aufruf ist, implementiert die Handler-Klasse das Interface com.amazonaws.services.lambda.runtime.RequestHandler aus der Bibliothek aws-lambda-java-core. In dessen Methodensignatur findet sich ein zweiter Parameter Context, der zuvor optional war. Über diesen hat die Funktion Zugriff auf Metadaten wie den Funktionsnamen, die Request-ID und die Identität des Aufrufers, wenn dieser authentifiziert ist. Diese Bibliothek wird nur zur Kompilierung verwendet und muss deswegen nicht mit paketiert werden.

Eingabe-Events liegen im JSON-Format vor. Die Eigenimplementierung eines Parsers ist zwar möglich aber nicht notwendig. Lambda übernimmt die Deserialisierung, wenn der Parametertyp der JavaBeans-Konvention folgt und zum JSON-Schema passt. Das funktioniert auch für verschachtelte Strukturen. Die Bibliothek aws-lambda-java-events enthält fertige POJOs für bekannte Events anderer AWS-Dienste. Auch bei der Rückgabe wird ein POJO automatisch nach JSON konvertiert. Der traditionelle Ansatz, sich zur Fehlersuche auf einem Server einzuloggen, ist bei Lambda nicht mehr gangbar. Es ist deswegen notwendig, Log-Daten zentral zu speichern und aufzubewahren.
Die Bibliothek aws-lambda-java-log4j2 liefert einen Log4j-2-Appender, der Log-Einträge an Amazon-CloudWatch-Logs sendet. Dort laufen die Log-Streams der Lambda-Aufrufe in einer Log-Gruppe zusammen für eine spätere Fehlersuche.

Wenn die Funktion zur Ausführung Bibliotheken verwendet, gibt es zwei einfache Wege, sie im Paket mitzuliefern. Zum einen kann der Code
als self-contained JAR (auch Uber-JAR genannt) verpackt werden: Alle Klassen aller Abhängigkeiten werden darin zur Build-Zeit entpackt und in das JAR kopiert. Die zweite Option ist, den Code mit seinen Abhängigkeiten in einer ZIP-Datei zu komprimieren (Listing 2). Die Abhängigkeiten müssen dazu nicht entpackt werden, sondern werden unverändert im LIB-Ordner abgelegt. Die zweite Variante bringt zwar nur wenig Verbesserung der Leistung zur Laufzeit, der Build-Prozess ist aber schneller.
Mit Maven und dem Goal maven-assembly-plugin:single ist diese Art der Paketierung einfach umzusetzen. (Listing 3) zeigt den dazugehörigen Assembly-Descriptor. Native Bibliotheken können übrigens auch in das Paket aufgenommen und aufgerufen werden, solange diese auf Amazon-Linux lauffähig sind bzw. dafür kompiliert wurden.

(Listing 2)
lib/lib.jar
lib/another_lib.jar
[…]
com/example/serverless/Greeter.class

(Listing 3)
<assembly>
 <id>zip-with-deps</id>
 <formats>
  <format>zip</format>
 </formats>
 <includeBaseDirectory>false</includeBaseDirectory>
 <dependencySets>
  <dependencySet>
   <outputDirectory>/lib/</outputDirectory>
   <unpack>false</unpack>
   <scope>runtime</scope>
   <useProjectArtifact>false</useProjectArtifact>
  </dependencySet>
  <dependencySet>
   <outputDirectory>/</outputDirectory>
   <unpack>true</unpack>
   <scope>runtime</scope>
   <includes>
    <include>${project.groupId}:${project.artifactId}:*:${project.version}
    </include>
   </includes>
  </dependencySet>
 </dependencySets>
</assembly>

Der Lebenszyklus einer Lambda-Funktion

Der Lambda-Dienst führt die Runtime und den Anwendungscode in einem Container aus. Der Container isoliert Funktionen untereinander und stellt Resourcen wie den konfigurierten Speicher bereit.

Beim ersten Aufruf einer Funktion verwendet der Lambda-Dienst einen neuen Container. Auf Wunsch wird auch eine Netzwerk-Schnittstelle aufgesetzt für den sicheren Zugriff auf Ressourcen im eigenen privaten virtuellen Netzwerk. Der Container lädt im Anschluss den funktionsspezifischen Code und startet eine neue JVM. In diesem Prozess instanziiert Lambda die Handler-Klasse und ruft die konfigurierte Methode auf. Die Funktion wird beendet, sobald die Methode nicht mehr ausgeführt wird. Entweder durch ihr reguläres Ende oder durch das Werfen eines Throwable.

Wie oben beschrieben hängt die Zeit, die zum Start einer Funktion gebraucht wird, stark von der Speicherkonfiguration ab. Bei der Optimierung der Anwendung für Lambda gilt wie auch außerhalb von Lambda: Mit einer aufwändigen Initialisierung (wie etwa Annotation-Scanning) und vielen Klassen bzw. Bibliotheken steigt auch die Dauer bis zur ersten Antwort. Der JIT-Compiler verbraucht verhältnismäßig viel Zeit, um die richtigen Optimierungen zu ermitteln und vorzunehmen. Die Ausführung dauert deswegen zunächst länger bis eine deutliche Leistungsverbesserung spürbar ist.

Bei einem erneuten Aufruf wiederholt sich der zuvor beschriebene Ablauf. Wenn aber nicht zu viel Zeit seit dem letzten Aufruf verstrichen ist, wird Lambda wahrscheinlich den Container und die darin laufende JVM wiederverwenden. Das erlaubt viele Performance-Optimierungen durch die JVM oder den Anwendungscode. JIT-Optimierungen und sonstige Eigenschaften der JVM sind bei der Wiederverwendung noch erhalten. Zwischen zwei Funktionsaufrufen hält Lambda die Prozesse in einem Container an. Threads, die in einer vorigen Ausführung gestartet wurden, laufen weiter, sobald die Funktion erneut aufgerufen wird.

Der Entwickler sollte die Wiederverwendung des Containers nutzen, um Initialisierungen nur einmal durchzuführen. Wie zuvor beschrieben, sind Lambda-Funktionen zustandslos. Eine Funktionsinstanz kann sich aber temporär einen lokalen Zustand halten, der noch zur Verfügung steht, wenn sie wiederverwendet wird. Den Zustand speichert der Entwickler entweder im Code selbst in Instanz- oder Klassenvariablen oder im Dateisystem. Jeder Funktion stehen unter /tmp 512 MB Speicher zur Verfügung. Ein Entwickler kann beispielsweise den Aufbau einer Datenbankverbindung über mehrere Aufrufe hinweg halten oder Ergebnisse in Dateien zwischenspeichern. Aber sowohl die Wiederverwendung der JVM als auch die Dauerhaftigkeit des Dateisystems ist kein garantiertes und leicht vorhersagbares Verhalten. Aus mehreren Gründen kommt ein neuer Container zum Einsatz, z.B. wenn sich die Konfiguration der Funktion ändert oder sich die Frequenz der Aufrufe erhöht.

Um mit der Anzahl der Aufrufe zu skalieren, startet Lambda bei Bedarf neue Container. Der Anwendungsentwickler muss sich nicht um Thread-Pooling kümmern, da eine Funktion nur einen Aufruf auf einmal entgegennimmt. Die Nebenläufigkeit kann aber wie bei einem Thread-Pool kontrolliert werden über einen optionalen Parameter, der die maximale Anzahl paralleler Ausführungen einer Funktion festlegt.

Während des gesamten Lebenszyklus der JVM sind die Umgebungsvariablen aus der Funktionskonfiguration über System.getenv() nutzbar. Sie werden häufig verwendet für Werte, die sich zwischen Stages unterscheiden. Handelt es sich um sensitive Daten, sollten sie über den AWS-Key-Management-Service verschlüsselt werden. Wenn mehrere Funktionen dieselben Werte benötigen, ist die zentrale Ablage im Parameter-Store des Amazon-Systems-Manager den Umgebungsvariablen vorzuziehen.

Infrastructure as Code für Serverless-Applications

Das obige Beispiel ist recht simpel, da der Anwendungscode keine weiteren Dienste verwendet. Außerdem sind häufige Deployments der Anwendung über die AWS-Management-Console aufwändig und laden zu Flüchtigkeitsfehlern ein. Da jeder Service auch über die API angesprochen werden kann, ist die Automatisierung aller Prozeduren für Provisionierung, Deployment und Konfiguration möglich. AWS-CloudFormation ist ein Dienst, mit dem Ressourcen mit ihren Eigenschaften deklarativ in einem Template im YAML-Format beschrieben werden. AWS-CloudFormation kümmert sich darum, einen entsprechenden Stack anzulegen und diesen Stack sogar an spätere Änderungen des Templates anzugleichen.

Für ein komplexeres Beispiel sollen hier kurz die wichtigsten Bestandteile eines URL-Shortener vorgestellt werden. Dieser ist als Serverless-Web-Application entwickelt wie in (Abb. 1) dargestellt. Zunächst definiert das CloudFormation-Template in (Listing 4) die API und die Lambda-Funktionen. Mit Hilfe der SAM-Erweiterungen (AWS-Serverless-Application-Model) werden in dem Template HTTP-Methoden mit Aufrufen in Lambda verknüpft. CloudFormation nutzt das Template zur Erzeugung eines Stacks. Um Namenskollisionen mehrerer Stacks (z.B. mehrerer Testumgebungen) zu vermeiden, erhalten diese einen eigenen Namensraum durch Suffixe. In diesem Beispiel nutzt der Anwendungscode eine DynamoDB-Tabelle, deren Namen er über eine Umgebungsvariable im Zugriff hat. In (Listing 4) lassen sich auch weitere CloudFormation-Ressourcen konfigurieren, z.B. ein S3-Bucket für den HTML- und JavaScript-Code einer grafischen Oberfläche.

(Listing 4)
AWSTemplateFormatVersion: ‚2010-09-09‘
Transform: AWS::Serverless-2016-10-31
Description: URL-Shortener
Resources:
  PostUrlFunction:
   Type: AWS::Serverless::Function
   Properties:
    Handler: com.example.serverless.UrlHandler::addUrl
    Runtime: java8
    Policies: AmazonDynamoDBFullAccess
    Environment:
     Variables:
      TABLE_NAME: !Ref Table
    Events:
     PostResource:
      Type: Api
      Properties:
       Path: /urls
       Method: post
  RedirectFunction:
   Type: AWS::Serverless::Function
   Properties:
    Handler: com.example.serverless.UrlHandler::redirect
    Runtime: java8
    Policies: AmazonDynamoDBFullAccess
    Environment:
     Variables:
      TABLE_NAME: !Ref Table
    Events:
     RedirectResource:
      Type: Api
      Properties:
       Path: /url/{key}
       Method: get
  Table:
   Type: AWS::Serverless::SimpleTable

Für die Verwendung von DynamoDB muss wie in (Listing 5) ein Mapper erzeugt werden. Seine Konfiguration erhält den Tabellennamen aus der Umgebungsvariable. Das AWS-SDK nutzt für den Aufruf zu DynamoDB die Berechtigungen automatisch, die über die Policies-Angabe in (Listing 4) zugeordnet sind. Für die Beschleunigung bei zukünftigen Aufrufen sollte der Anwendungscode den Mapper nur beim ersten Aufruf erstellen und für weitere Aufrufe speichern.

Der Code für die Weiterleitung an eine bestimmte URL zeigt (Listing 6). Die Typen für den Methoden-Parameter API GatewayProxyRequestEvent und das Rückgabe-Objekt API GatewayProxyResponseEvent kommen aus der aws-lambda-java-events Bibliothek. Das Mapping zwischen den JSON-Objekten und diesen Typen übernimmt der Lambda-Dienst automatisch.

(Listing 5)
AmazonDynamoDB client = AmazonDynamoDBClientBuilder.default-Client();
String shurlTable = System.getenv(„TABLE_NAME“);
TableNameOverride tableName = TableNameOverride.withTableNameReplacement(shurlTable);
DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameOverride(tableName).build();
mapper = new DynamoDBMapper(client, config);

(Listing 6)
public APIGatewayProxyResponseEvent redirect(APIGatewayProxyRequestEvent request) {
  String key = request.getPathParameters().get(„key“);
  Shurl item = mapper.load(Shurl.class, key);
  APIGatewayProxyResponseEvent response = new APIGatewayProxy-ResponseEvent();
  response.setStatusCode(302);
  response.setHeaders(Collections.singletonMap(„location“,item.getUrl()));
  return response;
}

Der Entwicklungsprozess läuft wie gewohnt: Der Build-Prozess erzeugt das ZIP mit dem Code und seinen Abhängigkeiten, z.B. mit Maven wie oben beschrieben. Im Anschluss erstellt ein einziger Aufruf in CloudFormation aus dem Template und dem Code eine Datei, aus der später ein CloudFormation-Stack erzeugt wird. Dieser Prozess wird meist automatisiert in einem Continuous-Delivery (CD) Werkzeug. Der einfachste Weg dafür führt über AWS-CodeStar, das alle Dienste vom Git-Repository über den Build-Service bis zur CD-Pipeline automatisch erzeugt und eine Hello-World-Anwendung generiert, die den Startpunkt bildet für die ersten Code-Änderungen. Auch für diese Dienste übernimmt die Serverless-Plattform die Verwaltung von Servern, damit keine Repository- oder Build-Server verwaltet werden
müssen.

Fazit:

Eine Serverless-Plattform übernimmt viele Aufgaben, die bei traditionellen Ansätzen von der Software-Entwicklung ablenken. Den Kern von Serverless-Applications in AWS bildet AWS-Lambda, das neben JavaScript, Go, Python und C# mit der JVM eine Runtime für eine Vielzahl von Programmiersprachen bietet. Der stärkste Vorteil der zustandslosen Lambda-Funktionen ist deren leichte und transparente Skalierbarkeit. Der Anwendungscode benötigt keine AWS-Spezifika, aber viele Dienste können als Event-Quelle für die Anwendung dienen. Eine beliebte Variante wurde hier in Form einer Serverless-Web-Application dargestellt, deren gesamte Infrastruktur deklarativ im YAML-Format verwaltet wird. Dank weiterer Dienste der Plattform für den Build- und Deployment-Prozess gilt für alle Bereiche von Serverless-Applications das Prinzip, nur das zu zahlen, was wirklich verbraucht wird.

 

Steffen Grunwald ist Solutions-Architect bei AWS. Er unterstützt dort Kunden auf ihrem Weg in die Cloud. Mit seiner Erfahrung in der Software-Entwicklung taucht er gerne in Architekturen und Entwicklungsprozesse ein, um die Effizienz in der Entwicklung und im Betrieb zu steigern – und damit die Innovationsgeschwindigkeit zu erhöhen.

Twitter: @steffeng

Carolyn Molski


Leave a Reply