Entwicklung eines Microservices mit AWS Serverless

Unter Verwendung von Golang, dem Serverless Framework, AWS Lambda, Amazon API Gateway und Amazon DynamoDB

Einleitung

Im vorangegangenen Blog-Artikel „Microservices mit AWS Serverless“ hatte ich bereits erwähnt, dass mit AWS Serverless die Notwendigkeit, Infrastruktur zu warten, entfällt. Das reduziert den Overhead und es bleibt mehr Zeit für die Entwicklung, was letztlich zu einer schnelleren Time-to-Market führt. Wenn der Scope eines Microservices in den Scope einer AWS Lambda-Funktion passt, kann er zusätzlich mit Services wie Amazon API Gateway, Amazon DynamoDB und AWS CloudWatch effektiv entwickelt und überwacht werden.

Dieser Blog-Beitrag beschäftigt sich mit der Umsetzung eines konkreten Beispiels für eine einfache Microservice-Anwendung, die in Golang geschrieben wurde und AWS Lambda sowie einige andere AWS Serverless-Services verwendet.

Entwicklung eines Microservice mit Golang, Gin und dem Serverless Framework

Im Folgenden wird ein einfacher Payment-Service als Beispiel mit Golang, Gin, dem Serverless Framework und verschiedenen AWS Serverless-Services implementiert. Die grundlegende Architektur besteht aus AWS Lambda zum Ausführen des Services, dem Amazon API Gateway zum Weiterleiten und Autorisieren von Requests an den Service und Amazon DynamoDB zum Persistieren der Payment-Daten, wie in Abbildung 1 dargestellt.

User Interaction with the Payment Service that is comprised of Amazon API Gateway, AWS Lambda and Amazon DynamoDB

Abbildung 1: Benutzerinteraktion mit dem Payment-Service, der sich aus Amazon API Gateway, AWS Lambda und Amazon DynamoDB zusammensetzt

Der Service bietet den Nutzern Funktionen zum Erstellen, Lesen und Aktualisieren von Zahlungen, wie z. B. das Abrufen eines Zahlungsverlaufs, das Erstellen und Aktualisieren von Zahlungen über eine REST-API-Schnittstelle.


Golang [1] wurde als Programmiersprache für dieses Projektbeispiel gewählt, da es leicht verständlich und präzise ist. Zudem passt es gut zu den Lambda-Funktionen, da diese sehr gut mit Multithreading umgehen können. Golang überzeugt beim Multithreading mit seinen leichtgewichtigen Goroutines, die einfach zu starten und zu verwalten sind. Man hätte auch jede andere Sprache wie Java oder Python für diesen Zweck verwenden können und ist nicht auf eine Programmiersprache beschränkt, wie bereits im vorherigen Blog „Microservices mit AWS Serverless“ erwähnt.

Entwicklung eines Payment-Microservice mit Golang und Gin

Kommen wir nun zu den konkreten Funktionen des Payment-Services. Neben einem GET /version und GET /health Endpoint, um die aktuelle Version und die Verfügbarkeit des Services zu prüfen, wird es GET Endpoints geben. Über diese lassen sich alle Zahlungen für einen Nutzer sowie eine bestimmte Zahlung für einen Nutzer abrufen. Außerdem können die Nutzer über POST- und PUT-Endpoints Zahlungen erstellen und aktualisieren. Abbildung 2 gibt einen schnellen Überblick über die REST API-Schnittstelle des Services.

OpenAPI definition of all the REST API endpoints provided by the payment microservice

Abbildung 2: OpenAPI-Definition aller REST API Endpoints, die vom Payment-Microservice bereitgestellt werden 
Bildquelle

Für diesen Artikel wird das Payment-Objekt einfach und leicht verständlich gehalten (siehe Abbildung 3). Wichtig ist zunächst, dass eine Objektstruktur vorhanden ist, die zur Speicherung und Anzeige von Bezahldaten für einen Benutzer verwendet werden kann. Die ID des Payment-Objekts ist der Identifikator und wird zum Speichern und Abrufen von Daten aus der Datenbank verwendet.

Golang struct that defines the properties of a payment and how these will be named in a JSON object

Abbildung 3: Golang-Struct, welches die Eigenschaften einer Zahlung definiert und wie diese in einem JSON-Objekt benannt werden
Bildquelle

Jedes Golang-Projekt beginnt mit einer main.go-Datei in einem Main Package (siehe Abbildung 4). In der Datei main.go muss eine Handler-Funktion geschrieben werden, die sich um eingehende Events kümmert, wenn die Lambda-Funktion ausgelöst wird. Als Web-Framework wird Gin verwendet, das dafür bekannt ist, performant zu sein und die Logik der Request-Bearbeitung zu abstrahieren. Gleichzeitig wird der Boilerplate-Code reduziert, sodass sich Entwickler nur mit den wichtigen Implementierungsdetails befassen. Zu Beginn der Main Funktion wird ein Router eingerichtet (Abbildung 4, Zeile 21), der zum Mappen der Endpunkt-URLs verwendet wird (Abbildung 4, Zeile 22). Anschließend wird eine Lambda-Funktion initialisiert (Abbildung 4, Zeile 23) und gestartet (Abbildung 4, Zeile 25).

Es gibt ein dediziertes Paket, welches Gin und Lambda-Funktionen unterstützt und in diesem Projekt verwendet wird (Abbildung 4, Zeile 16, 23) [2].

Main.go file that serves as the entry point, defining the service’s HTTP routes with Gin and a lambda handler using the AWS SDK

Abbildung 4: Main.go-Datei, die als Eintrittspunkt dient und die HTTP-Routes des Services mit Gin und einem Lambda-Handler unter Verwendung des AWS SDK definiert

Der Service leitet Requests direkt an einen Payment Controller weiter (siehe Abbildung 5), der die Nutzereingaben validiert und die Daten dann an ein Repository weiterleitet (siehe Abbildung 6). Das Repository bereitet die Daten so auf, dass sie reibungslos von Amazon DynamoDB verwendet werden können (siehe Abbildung 6). Anschließend wird ein Datenbank-Client verwendet, um die Daten abzurufen und zu speichern.

Letzteres geschieht hauptsächlich mit dem AWS SDK, welches zu Boilerplate-Code führt und daher weggelassen wird, um den Artikel kurz zu halten.

Setting up the HTTP routes of the service, mapping them to the respective controller methods

Abbildung 5: Konfiguration der HTTP-Routes des Services und deren Zuordnung zu den jeweiligen Controller-Methoden

The PostPayment method takes in user data, validates the input (line 21,22) and sends the valid data to a repository (line 26), while handling possible errors received from the repository (line 28). In case of successful creation of the payment, 201 and the created object are returned

Abbildung 6: Die PostPayment-Methode nimmt die Benutzerdaten entgegen, validiert die Eingaben (Zeile 21, 22) und sendet die gültigen Daten an ein Repository (Zeile 26), während mögliche Fehler vom Repository behandelt werden (Zeile 28). Im Falle einer erfolgreichen Erstellung der Zahlung werden 201 und das neu erstellte Objekt zurückgegeben

The CreatePayment method takes in a payment object, transforms the data to make it suitable for DynamoDB (assigning a uuid to the payment object, the Payer (uuid) as PartitionKey and a string including the payment id as SortKey) and finally persists the data using the AWS SDK’s PutEntity function

Abbildung 7: Die CreatePayment-Methode akzeptiert ein Payment-Objekt und wandelt die Daten so um, dass sie für DynamoDB geeignet sind. Dafür weist sie dem Payment-Objekt eine "uuid" und dem Payer (der ebenso eine "uuid" hat) einen PartitionKey und einen SortKey zu. PartitionKey und SortKey sind Strings. Der PartitionKey enthält die Payer-ID und der SortKey eine Payment-ID. Schließlich persistiert sie die Daten mit der PutEntity-Funktion des AWS SDK.

Der vorliegende Beispielservice ist recht einfach und würde in einem Produktionsszenario nicht so verwendet werden, aber für Demonstrationszwecke ist er gut geeignet. Mit dieser Codebasis kann ein Benutzer über die REST-API-Schnittstelle des Services problemlos Zahlungen erstellen, abrufen und aktualisieren.


Bevor wir die Infrastruktur in AWS erstellen können, muss der Code als Zip-Datei erstellt und komprimiert werden und sich in dem Verzeichnis befinden, auf das später in einer serverless.yml-Datei verwiesen wird.


Wir können unseren Code mit dem Go-Build-Befehl erstellen (unter Angabe von Linux als Betriebssystem und dem korrekten Pfad zu unserer main.go-Datei):

 

              GOOS=linux go build -o bootstrap { main.go file location }

 

Sobald dies erledigt ist, können wir uns dem Infrastrukturteil widmen.

Aufsetzen der Infrastruktur mit dem Serverless Framework

Das Serverless Framework [3] ermöglicht uns eine einfache Konfiguration von AWS Lambda-Funktionen, Amazon API Gateway, Amazon DynamoDB und das Hinzufügen einiger Policies zu den Ressourcen mithilfe einer yaml-Datei. Man könnte mit dem Framework jede beliebige AWS-Infrastruktur einrichten, aber es wurde speziell für AWS Serverless-Services entwickelt und macht daher die Bereitstellung der erforderlichen AWS-Infrastruktur recht effizient und einfach. Es kann mit dem folgenden Befehl über npm installiert werden:

 

            npm i serverless -g

 

Nun kann man eine serverless.yml-Datei auf der Ebene des Root-Ordners des Projekts erstellen und einige Grundlagen wie die Serverless-Version (Abbildung 8, Zeile 1), den Namen des Service (Abbildung 8, Zeile 2) sowie den Provider (Abbildung 8, Zeile 5), das Runtime-Image (Abbildung 8, Zeile 6), die AWS-Region (Abbildung 8, Zeile 7), einen Namen für die Deployment Stage (Abbildung 8, Zeile 8) und auch eine Standard-IAM-Rolle, die unsere Lambda-Funktion verwenden soll, festlegen (Abbildung 8, Zeile 9-10).

Basic Serverless Framework setup in a yaml file, defining service name, service provider, runtime as well as AWS region and a deployment stage name

Abbildung 8: Grundlegende Serverless-Framework-Konfiguration in einer yaml-Datei, die den Servicenamen, den Service-Provider, die Runtime sowie die AWS-Region und einen Namen für die Deployment Stage definiert.

Als nächstes wird eine Lambda-Funktion definiert. Hierfür müssen wir einen Namen und einige andere Attribute wie folgt festlegen:
 

  • Package.artifact: Pfad zu dem komprimierten Code, der in die Funktion hochgeladen werden soll (Abbildung 9, Zeile 2-3)
  • Handler: Eintrittspunkt der Lambda-Funktion (muss “bootstrap” heißen in Serverless Framework Version 3, siehe Abbildung 9, Zeile 4)
  • Wichtige Umgebungsvariablen (hier: DynamoDB Tabellenname, Abbildung 9, Zeile 5-6)
  • Events (Abbildung 9, Zeile 7-22)
     

Der letzte Parameter „events“ gibt an, wie die Lambda-Funktion ausgeführt werden soll. In unserem Fall wird sie durch HTTP-Requests über das API-Gateway ausgelöst, daher der Wert „http“. Sie könnte aber auch durch eine Queue oder Notifikation über Amazon SQS oder Amazon SNS oder viele andere Services ausgelöst werden.

Definition of an AWS Lambda function in a serverless.yml file

Abbildung 9: Definition einer AWS Lambda-Funktion in einer serverless.yml Datei

Außerdem müssen die Ressourcen für DynamoDB und die Standard-IAM-Rolle definiert werden. Für DynamoDB müssen die Partition und der Sortierschlüssel angegeben werden, da sie nach der Tabellenerstellung nicht mehr geändert werden können. Es ist wichtig, für die IAM-Rolle zwei Dinge zu definieren:

  • Sie benötigt die Erlaubnis, die Lambda-Funktion zu übernehmen (Abbildung 10, Zeile 43-50)
  • Die Lambda-Funktion benötigt Zugriff auf DynamoDB-Operationen (Abbildung 11)
DynamoDB and IAM role setup with the Serverless Framework

Abbildung 10: DynamoDB und IAM Role Konfiguration mit dem Serverless Framework.

IAM Policy allowing DynamoDB actions on the table “Payments”

Abbildung 11: IAM Policy, welche DynamoDB Aktionen an der Tabelle “Payments” erlaubt.

Bereitstellung des Golang-Microservices mit AWS Lambda, Amazon API Gateway und DynamoDB

Sobald die Konfiguration abgeschlossen ist, können wir den Befehl „serverless deploy“ in einem Terminal verwenden, um die Erstellung eines sogenannten AWS CloudFormation-Stacks zu initiieren. Im Hintergrund wird ein CloudFormation-Template erstellt und an einem bestimmten AWS S3-Speicherort hinterlegt. Dieses Template wird von AWS CloudFormation als Grundlage für die Erstellung aller Ressourcen innerhalb des Stacks verwendet. Sobald die Stack-Kreierung erfolgreich war, sollte man alle Ressourcen auf der Registerkarte „Resources“ in der AWS Management Console sehen können. Dafür muss man unter „CloudFormation“ zu „Stacks“ navigieren und den neuen Stack auswählen -> der Name des Stacks lautet {service-name}-{stage-name}, wie in der Datei serverless.yml definiert. Der Status des Stacks sollte „CREATE_COMPLETE“ lauten (oder „UPDATE_COMPLETE“, wenn er erfolgreich aktualisiert wurde). Dies sollte anzeigen, dass alle im Stack definierten Ressourcen erfolgreich erstellt wurden, was auch unter dem Reiter „Resources“ überprüft werden kann. Alle Ressourcen wie z. B. das API-Gateway (das automatisch von Serverless erstellt wird, ohne dass es in der serverless.yml-Datei angegeben werden muss) sollten den Status „CREATION_COMPLETE“ haben (siehe Abbildung 12).

payment-dev stack in AWS CloudFormation after successful resource creation

Abbildung 12: payment-dev Stack in AWS CloudFormation nach erfolgreicher Erstellung der Ressourcen.

Von dort aus kann man zum API-Gateway navigieren und die Registerkarte „Stages“ aufrufen, um die Invoke-URL zu erhalten. Diese URL ist die Basis-URL für das Senden von Requests an den Payment Service (man muss beachten, dass sich die Invoke-URL nach dem Löschen und Neuanlegen des Stacks ändert). Außerdem kann man hier mehr als eine Stage haben, zum Beispiel für „dev“, „test“ und „prod“, um verschiedene Umgebungen eines Services zu verwalten. Amazon API Gateway zeigt auch alle REST-API-Endpunkte in einer hierarchischen Reihenfolge unter der Registerkarte „Resources“ an. Diese ermöglicht einen schnellen und umfassenden Überblick darüber, was der Service bietet (siehe Abbildung 13). Zudem wird die verwendete Autorisierungsmethode angezeigt. In diesem Fall sind „AWS_IAM“ für die gültige AWS-Anmeldeinformationen und die Berechtigung „execute-api“ erforderlich. Ansonsten wird bei Requests an den Service die Meldung „403 Forbidden“ zurückgegeben.

AWS Management Console for API Gateway showing the “Resource” tab for the payment service with all its available REST endpoints as well as its authorization method

Abbildung 13: AWS Management Console für API Gateway zeigt die Registerkarte „Resource“ für den Payment Service mit allen verfügbaren REST-Endpunkten und der Autorisierungsmethode.

Senden von Requests an den Payment Service

Mit dem Code und der Infrastruktur können die Benutzer nun Requests an den Microservice senden. Mit einer API-Plattform wie Postman kann man ganz einfach Requests an den Service senden, die einen gültigen Autorisierungs-Header enthalten. Dafür wählt man „AuthType“ als „AWS Signature“ in Postman und gibt die gültigen AWS-Anmeldedaten sowie „execute-api“ als „Service name“ an. Abhängig von der jeweiligen Eingabe und des Methodentyps erhält man eine Antwort. Beispielsweise sollte ein POST /payments Request mit einem gültigen JSON-Body zu „201 Created“ führen und das erstellte Objekt, einschließlich seiner ID im Response-Body, ausgeben (siehe Abbildung 14). Die ID kann dann als Pfadparameter verwendet werden, um das neu erstellte Objekt abzurufen oder zu aktualisieren.

A successful POST /payments REST call made with Postman

Abbildung 14: Ein erfolgreicher POST /payments REST Request mit Postman.

Wenn wir die Lambda-Funktion des Services in der AWS Management Console aufrufen, können wir feststellen, dass sie unter dem Abschnitt „Monitor“ getriggert wurde (siehe Abbildung 15). Von hier aus können wir auch direkt zu den in Amazon CloudWatch erstellten Logs des Services gehen, was für die Problembehandlung im Falle von Fehlern sehr nützlich ist.

Payment Lambda function showing a trigger at 3pm (bottom left graph), the calls duration (bottom mid graph), error count and success rate (bottom right graph)

Abbildung 15: Payment Lambda-Funktion mit Trigger um 15 Uhr (unteres linkes Diagramm), Dauer der Ausführung (unteres mittleres Diagramm), Fehlerzahl und Erfolgsquote (unteres rechtes Diagramm).

Man kann auch zur DynamoDB-Tabelle „Payments“ in der AWS Management Console navigieren, um die erstellte Payment Entity mit ihren Attributen anzuzeigen (siehe Abbildung 16).

DynamoDB table “Payments” with a payment object entry, showing PartitionKey (“Payer_Id”), SortKey (“Payment_Id”) and the object’s attributes stored in an additional column (“Data”)

Abbildung 16: DynamoDB-Tabelle „Payments“ mit einem Eintrag für ein Payment-Objekt, mit PartitionKey („Payer_Id“), SortKey („Payment_Id“) und den Attributen des Objekts, die in einer zusätzlichen Spalte („Data“) gespeichert sind.

Schließlich kann man den Stack bei Bedarf wieder entfernen, indem man den Befehl „serverless remove“ verwendet, der dafür sorgt, dass alle in der Datei serverless.yml angegebenen Ressourcen vernichtet werden.

Fazit

Wie in diesem Artikel gezeigt wird, können AWS Serverless-Services wie AWS Lambda einfach genutzt werden, um Anwendungen zu erstellen, ohne die zugrunde liegende Infrastruktur zu verwalten. Bei der Entwicklung von Microservices mit AWS Serverless ist es wichtig, für jeden Service einzeln zu entscheiden, ob Serverless der richtige Ansatz ist. Es gibt - wie üblich - keine Universallösung für die Entwicklung eines Microservices. Den richtigen Umfang für einen Microservice zu finden, kann eine anspruchsvolle Aufgabe sein, ebenso wie die Synchronisierung und das Testen mehrerer Microservices, die miteinander interagieren, während jeder Microservice seine eigene Datenbank verwaltet. Glücklicherweise sind dies alles Aufgaben, bei denen AWS Serverless hilfreich ist und Entwickler bei der Entwicklung von performanten, robusten und fehlertoleranten Anwendungen so weit wie nötig unterstützt.