Nachdem mein Kollege Christoph Gerkens bereits im Blog-Beitrag „Serverless – Mit wenig Infrastruktur zu skalierbaren Systemen“ über die Grundlagen von Serverless berichtete, folgt nun ein kurzer Bericht aus der Praxis.
Fachlicher Hintergrund
Im Gesamtprojekt wird eine IoT-Plattform entwickelt. Die zu entwickelnde Plattform läuf auf AWS.
Die Vision dieser Plattform ist, dass Nutzer ihre Daten der Plattform anvertrauen. Über OAuth kann ein Nutzer Apps Berechtigungen erteilen, diese Daten abzurufen oder diese zu abonnieren. Die Plattform verwaltet also die Daten für den Nutzer. Für den Nutzer ist es stets ersichtlich, welche Anwendungen auf welche Daten zugreifen können.
Unternehmen, die IoT-Produkte entwickeln, können auf der Plattform aufsetzen und so die Produktentwicklung beschleunigen.
In einer konkreten Anwendung, die auf der Plattform aufsetzte, wurde Sensordaten erfasst und über 2G-Mobilfunk (regelmäßig kleine Datenpakete über MQTT) oder Bluetooth (große Datenbündel über S3-Buckets) der Plattform übergeben. Es handelte sich um einen Fitness-Tracker für Haustiere (Hardware) mit einer App (Software), die zurückgelegte Schritte, verbrauchte Kalorien und den Standortverlauf visualisierte. Zudem gab es eine Recommendation-Engine, die einem Gesundheitsratschläge auf Basis der vorliegenden Daten ausspielte.
Kontext
Neben der Plattform wurden weitere Nachbarsysteme benutzt.
Direkt wurde mit DynamoDB gesprochen, um Daten zu speichern. Dynamo ist eine NoSQL-Datenbank, die sowohl Dokumente als auch Key-Value-Paare speichern kann.
SNS (Amazon Simple Notification Service) wurde verwendet, um Push-Notifications zu versenden.
In S3-Buckets wurden Bilder und Content abgelegt. S3 (Amazon Simple Storage Service) ist ein Objektspeicher zum Speichern und Abrufen beliebiger Dateien.
CloudWatch wertete Log-Dateien aus, erfasste Metriken und signalisierte Probleme in der Anwendung über definierte Alarme.
Das System wurde aktiv von der o.g. Plattform angesprochen (abonnierte Sensor-Daten), ein CMS teilte geänderten Content mit dem App-Backend über Webhooks, und die Apps selbst kommunizierten über eine REST-API mit dem App-Backend.
Serverless-Framework
Das App-Backend wurde komplett mit dem Serverless Framework entwickelt. Dieses Framework verspricht, dass man damit auto-skalierende, eventgetriebene Apps entwickeln kann, die auf AWS Lambda deployt werden. Neben AWS Lambda unterstützt das Serverless Framework inzwischen auch Microsoft Azure, Google Cloud Platform und IBM OpenWhisk.
Das Serverless Framework ist nicht wirklich ein Framework zur App-Entwicklung, sondern es kümmert sich vielmehr um das Packaging und das Deployment. Es ist ein NPM-Package, das sich wie gewohnt installieren lässt. Anschließend hat man ein Command Line Interface, das es einem erlaubt, Services zu deployen.
Services wurden in Node (ES2016) implementiert und in einer serverless.yaml
-Datei beschrieben. In dieser YAML-Datei werden Services und deren Abhängigkeiten (z.B. Datenbanken) definiert. Ein Aufruf von serverless deploy
sorgt dafür, dass diese in AWS erstellt oder aktualisiert werden.
Umsetzung
AWS Lambda-Funktionen implementierten fachliche Features. Oft waren dies Funktionen, deren mathematische Grundlagen an Implementierungen in Jupyter Notebooks angelehnt waren.
Das Jupyter Notebook ist eine freie Web-Applikation, mit der Dokumente erstellt und geteilt werden können, in denen Daten, Code, mathematische Ausdrücke, grafische Auswertungen und erklärender Text enthalten sein können. Diese Notebooks wurden von einem Fachexperten (Data Scientist) exemplarisch in Python erstellt.
Jeder fachliche Service, der Daten speichern musste, speicherte diese als Dokument in einer eigenen DynamoDB. Einige Services orchestrierten darunter liegende Services und aggregierten Dokumente zu größeren, zusammengesetzten Dokumenten, um die Anzahl von HTTP-Requests zu minimieren. Diese aggregierenden Services hatten dann natürlich keine eigene DynamoDB.
AWS API Gateways exportieren diese Features als HTTP-Endpoints, die wiederum in Swagger dokumentiert waren.
Testing
Durch das Schneiden der Services in Models (Zugriffe auf DynamoDB), fachliche Services, die auf diese Models zugreifen, und schließlich Endpoints, die lediglich fachliche Services konsumieren, ließen sich Tests recht leicht schreiben. Alle Specs basierten auf der Assertion-Library Chai, dem BDD-Testing-Framework Mocha und der Mocking-Library Sinon. Durch Sinon ließen sich die einzelnen Bestandteile des Backends untereinander mocken. Die Infrastruktur AWS konnte durch aws-sdk-mock gemockt werden, so dass Specs unabhängig vom AWS schnell und Seiteneffekt-frei ausgeführt werden konnten
OAuth
Die zu verwendende Plattform implementierte OAuth. Apps starteten den OAuth-Flow im Webbrowser des Endgeräts. Dieser Flow leitete den Nutzer nach dem Login (und dem Erteilen nötiger Berechtigungen) zu einem Login-Endpoint im App-Backend weiter. Dabei übergab der Flow dem App-Backend einen Key. Dieser Key konnte dann genutzt werden, um daraus ein Token von der Plattform zu erhalten, welches Apps nutzen konnten, um sich gegen das Backend zu authentifizieren
Für die Autorisierung lassen sich eigene Lambda-Funktionen vor den Aufruf der eigentlichen Lambda-Funkion hängen. Ein OAuth-Authorizer, der einen OAuth-Token auf Gültigkeit überprüft, war dabei erstaunlich kurz und kompakt.
API-Keys
Einige Endpoints wurden nicht von angemeldeten Nutzern, sondern von anderen Backend-Systemen wie dem CMS Contentful über Webhooks aufgerufen. Diese Endpoints wurden durch API-Keys gesichert. Auch hierfür lässt sich ein Authorizer sehr leicht erstellen.
Data-Flow
Die Plattform, die verwendet wurde, befand sich noch in Entwicklung. Daher war sie teilweise nicht verfügbar, weswegen ein uni-direktionaler Datenfluss implementiert wurde. Das App-Backend versucht nicht, sich Sensordaten aus der Plattform zu holen, sondern lässt sich diese Sensordaten schicken und speichert die aufbereiteten Sensordaten „lokal“ in eigenen DynamoDBs. So war das App-Backend auch dann für Apps erreichbar, wenn die Plattform selbst gerade nicht verfügbar war.
CI/CD
Konfigurationen (Push-Applications für Push-Notifications unter iOS und Android, OAuth-URLs etc.) unterschiedlicher Umgebungen (PROD, TEST, DEV) wurden über Umgebungsvariablen in die Services injiziert. Um die Verwaltung der Umgebungsvariablen kümmerte sich der CI/CD-Server CircleCI.
Fazit
Mein erstes Serverless-Projekt war ein App-Backend, das im wesentlichen aus AWS Lambda-Funktionen bestand, die Sensordaten empfingen, diese aufbereiteten und in DynamoDBs ablegten. Einige dieser Funktionen wurden als RESTful HTTP-Services über API Gateways für Apps bereitgestellt. OAuth-Tokens schützen diese Endpoints im Frontend und API-Keys im Backend.
Nachdem die ersten Hürden genommen waren (im Wesentlichen das Mocken der Umgebung, um lokal schnell testgetrieben entwickeln zu können), ging die Entwicklung einzelner Features schnell von der Hand.
Das Serverless-Framework entwickelte sich parallel zum Projekt stets weiter, so dass meist alle benötigten Features bereits implementiert waren.
Trotzdem konnte sich gegen Ende des Projekts das Serverless-Framework nicht mehr um das komplette Deployment kümmern und wurde teilweise durch Terraform abgelöst, denn Terraform konnte zusätzlich auch noch das verwendete CDN (CloudFront), DNS-Einträge usw. definieren.