Strukturierung von Serverless-Anwendungen in der Cloud

Keine Kommentare

Serverless ist ein Modell, bei dem Cloud-Anbieter allein verantwortlich für den Betrieb der Server-Infrastruktur sind. Compute-Ressourcen werden beim Serverless-Ansatz hauptsächlich in Functions strukturiert. Daher wird dieser Bestandteil „Functions as a Service“ (FaaS) genannt. Die Kosten dafür berechnen sich anhand der Ausführungszeit und Dimensionierung (Memory und CPU) der einzelnen Functions.

Eine Function reagiert immer auf ein Event und wird dadurch ausgeführt. Events können aus verschiedenen Quellen kommen: HTTP-Anfragen, Messages aus einem Pub-Sub-Service, Uploads, Cronjobs und Events anderer Services (z. B. eine Benutzerregistrierung). Functions können auch in State Machines (z. B. AWS Step Functions) orchestriert werden.

Sie sind aber nicht die einzigen Cloud-Ressourcen, die in Serverless-Systemen verwendet werden. In Kombination mit Managed Services für Datenbanken, Messaging, Mail- und SMS-Versand, REST API, Content Delivery Network, Queuing sowie Key- und Secrets-Management bleibt im Cloud-Universum kein Wunsch offen, um Software zu entwickeln und zu betreiben.

API Gateway, Lambda function und eine managed Datenbank wie DynamoDB lassen sich hervorragend zum Bau von Web-Apps und APIs kombinieren

Serverless erlaubt es, dass wir uns auf das Lösen von Problemen aus der Fachlichkeit konzentrieren können. Müssen wir uns nicht mehr um den Betrieb von eigenen Plattformen, Mail-Servern, Datenbanken und Runtimes kümmern, senkt das die Fertigungstiefe und Entwicklungskosten der Software. Es besteht jedoch die Herausforderung, diese Cloud-Ressourcen in langlebigen, robusten und sicheren Systemen anzuordnen. Um das meiste aus der Welt der Serverless-Anwendungen und Managed Services zu holen, soll dir dieser Artikel für dein Projekt einige Ratschläge an die Hand geben.

Bevor es losgeht: Dieser Artikel nimmt kein Bezug auf das Serverless Framework (einer von vielen Möglichkeiten, Serverless anzuwenden), sondern auf das Ausführungsmodell und den Umgang damit. In Beispielen werden Dienste für Serverless von Amazon Web Services (AWS), als aktuell reifsten Cloud-Anbieter für diese Zwecke, verwendet.

Staging-Konzept und Multi-Account-Strategie definieren

Staging ist auch in der Cloud und in Serverless-Anwendungen essenziell. Ein gängiger Ansatz sieht drei Umgebungen für unterschiedliche Zwecke vor: Development, Test und Produktion. Auf Development werden Features entwickelt und experimentiert. Auf Test werden Änderungen in einer produktionsähnlichen Umgebung getestet. Danach wird auf die Produktion ausgeliefert.

Externe Systeme, mit denen unser System kommuniziert, sollten am besten ebenfalls Staging praktizieren, damit wir uns auf jeder Umgebung mit dem passenden Gegenstück integrieren können.

Was ist an Staging in der Cloud denn anders? Wir rollen nicht auf unterschiedliche Server, Cluster oder Workspaces aus, sondern auf unterschiedliche Cloud-Accounts. Jede Umgebung sollte in einem eigenen Account leben, um aus Gründen der Sicherheit, Auditierbarkeit, Kostentransparenz, Reproduzierbarkeit und Flexibilität (Erstellung und Löschung) isoliert zu sein.

Root Account, Organisational Units und Accounts für Stages und andere Zwecke bilden in einer Cloud-Account-Struktur eine für Produkte und Organisation sinnvolle Hierarchie

Mehrere Cloud-Accounts pro Projekt sind gängige Praxis. Das sehen auch Cloud-Provider so und bieten deshalb Lösungen für die Umsetzung einer Multi-Account-Strategie an. AWS Organisations lässt uns auf einem Root-Account eine Organisationsstruktur abbilden. Diese besteht in der Regel aus fachlich oder aspektbezogenden Organizational Units (OUs), in die Teams ihre benötigten Accounts einsortieren können. Um diese Strukturen abzusichern, auditierbar und im Ernstfall forensisch untersuchbar zu machen, ergänzen Services wie AWS Control Tower, CloudTrail und SSO das Bild.

Mit Staging und einer Multi-Account-Strategie ist, auch in nicht-serverlosen Systemen in der Cloud, ein solider Anfang gemacht. Nun stehen unserem Projekt die wichtigsten Strukturen zur Ausgestaltung zur Verfügung. Es ist Zeit, diese mit Fachlichkeit und Technik zu füllen.

Fachliche Deployment-Einheiten gestalten

Eine Deployment-Einheit ist als Klammer um Source Code zu sehen, der fachlich oder technisch zusammen gehört und ausgerollt wird. In der Domäne „Lebensmittel“ liegen Definition und Implementierung von Lebensmittel-REST-Ressourcen. Die Deployment-Einheit bringt auch eigene „Infrastructure as Code“ (IaC) mit, um Datenbanktabellen und S3-Buckets zum Speichern von Lebensmitteln und Bildern zu definieren. Wenn über die Anlage eines neuen Lebensmittels auch andere Deployment-Einheiten Bescheid wissen wollen, empfiehlt sich das Publishing eines Events auf einem Messaging Topic (z. B. mit AWS SNS), das Konsumenten abonnieren können. Um die Ressourcen mit unserem Code reproduzierbar auf Cloud-Accounts auszurollen, braucht es natürlich auch eine Pipeline, die testet, baut und den Infrastruktur-Code ausführt.

Domänen werden in fachlichen Deployment-Units gruppiert, verfügen über eigene Datenhaltung und kommunizieren maßgeblich über Messaging-APIs wie SNS miteinander

Wie groß ist eigentlich eine „Deployment-Einheit“? Eine Einheit lässt sich in ihrer Größe frei gestalten. Von einem Monolithen, der alles zusammen ausrollt, über einen Microservice-Schnitt bis hin zu einzelnen Functions pro Einheit ist alles denkbar. Diesbezüglich sind die „Best practises for organizing larger serverless applications“ von AWS lesenswert.

Event-getriebene Kommunikation dominiert

Am elegantesten sind solche Schnittstellen, bei denen wir auf Event-getriebene, asynchrone Kommunikation setzen können, ohne selber die nötige Infrastruktur dafür bereitzustellen. Die Services AWS SNS und SQS bieten alles Nötige, sind kostengünstig und laden zur großzügigen Nutzung ein. Events über Messaging mit SNS sind das Mittel der Wahl, wenn Domänen untereinander kommunizieren sollen. Auch bei der Kommunikation innerhalb einer Domäne ist die Entkopplung durch Events mittels Queuing (SQS) fast immer überlegen, verglichen mit direkter, synchroner Kommunikation zwischen Functions.

Kommunizieren Domänen untereinander über Events, sieht das konkret so aus: Eine Function sendet über einen Pub-Sub-Service wie AWS SNS eine Message auf einem Topic. Die konsumierende Function der anderen Domäne kann das Topic abonnieren, dessen Messages empfangen und verarbeiten. Meistens gibt es jedoch gute Gründe dafür, zwischen SNS-Topic-Abonnements und der verarbeitenden Function eine Queue einzusetzen. Die Queue empfängt dann SNS-Messages und hält diese vor, bis die Function sie erfolgreich verarbeiten kann oder die Message ablaufen soll. Schlägt die Verarbeitung fehl (etwa weil ein benötigtes Drittsystem offline oder überlastet ist), wird die Message zurück in die Queue gestellt, um später erneut an die Reihe zu kommen.

Innerhalb einer Domäne kann auf die Übermittlung von Messages über SNS verzichtet werden. Hier reicht eine Queue völlig aus, um Produzent von Konsument zu entkoppeln.

Event-getriebene Kommunikation wird von einem Produzenten initiiert, von einem Konsumenten abonniert und mit einer Queue für fehlertolerante Verarbeitung entkoppelt

Vom Spiel zwischen fachlichen und technischen Abhängigkeiten

Für das angeführte Beispiel braucht „Lebensmittel“ ein SNS Topic „food-events“, um einer weiteren Deployment-Einheit „Produkt“ mitzuteilen, dass Lebensmittel hinzugefügt wurden. Produzent und Konsument wollen wir über Messaging lose (also asynchron) koppeln. Die beiden fachlichen Deployment-Einheiten sollen untereinander fehlertolerant und robust agieren, was auch bedeutet, dass wir sie unabhängig und fernab einer festen Reihenfolge untereinander ausrollen können. Würde das Topic „food-events“ jedoch als Teil des Rollouts von „Lebensmittel“ angelegt werden, obwohl auch „Produkt“ von der Existenz des Topic (welches eine definierte Cloud-Ressource ist) abhängig ist, wäre diese Unabhängigkeit nicht gegeben.

Gemeinsame technische Abhängigkeiten auf Cloud-Ressourcen (z.B. SNS-Topics) in vorher ausgerollte Infrastruktur-Einheiten ausgliedern

Oft gibt es mindestens eine Deployment-Einheit, die gemeinsame technische Grundlagen für die fachlichen Deployment-Einheiten schafft. Wir können geteilt genutzte Ressourcen wie Messaging Topics, DNS-Einträge und E-Mail-Versand-Konfigurationen in einer technische Deployment-Einheit „Infrastruktur“ beherbergen. Diese Einheit steht in der Deployment-Reihenfolge stets vor den fachlichen Einheiten, um für gemeinsame Vorbedingungen auf der Zielumgebung zu sorgen. Technisch ist z. B. in AWS für die Nutzung von SNS Messaging das Topic und damit die Resource Number (ARN) die Vorbedingung. Die ARN bekommen wir erst nach Erstellung des Topics und brauchen sie im Produzenten und Konsumenten, um Events zu veröffentlichen und Abonnenten darauf zu registrieren.

Unter Verwendung von Terraform würde man hier mit einem Output arbeiten. Dabei wird die ARN in den Remote State von „Infrastruktur“ geschrieben und anhand der Referenz auf das Topic von „Produkt“ und „Lebensmittel“ entsprechende IAM-Berechtigung und Abonnements erstellt.

Gesamtbild: Geteilte Infrastruktur für alle Stages, stage-spezifische Infrastruktur und fachliche Einheiten pro Stage in einer festen Deployment-Reihenfolge

Neben Ressourcen, die auf jeder Stage erneut benötigt werden, gibt es auch Ressourcen der Art „geteilte Infrastruktur“, die zentral von allen Umgebungen genutzt werden (z. B. Hauptdomain, accountübergreifende IAM-Strukturen). Geteilte Infrastruktur sollte, da sie benötigte Grundlagen schafft, in der Reihenfolge vor der Infrastruktur-Einheit ausgerollt werden.

REST APIs und User Interfaces ausrollen

Die Beispielanwendung besteht nun aus den fachlichen Einheiten „Lebensmittel“ und „Produkt“ sowie den technischen Einheiten „Infrastruktur“ und „geteilte Infrastruktur“. Um unsere Funktionalität über Schnittstellen und Oberflächen von außen nutzbar zu machen, bieten sich in der AWS die Services API Gateway und CloudFront an.

Ein API Gateway lässt sich (auch über Infrastructure as Code) mit REST-Ressourcen konfigurieren. Zur Implementierung der Ressourcen (bzw. deren HTTP-Methoden) werden Lambda-Functions integriert. Mit einer CloudFront Distribution kann eine in einem S3-Bucket hinterlegte Web-Anwendung global ausgeliefert werden.

Wie in der Softwareentwicklung üblich, müssen wir uns den Fragen stellen ob REST API und User Interface von jeder fachlichen Deployment-Einheit selbst oder zentral vom gesamten System ausgerollt werden. Fachliche Modularisierung sieht eigentlich auch vor, diese mit dem Rest ihrer fachlichen Einheit zusammen auszurollen. Das ist jedoch eine pauschale Aussage und sollte besser von Fall zu Fall unterschieden werden. Folgende Kriterien spielen für die Entscheidung eine Rolle:

  • Technische Limitierung: ein API Gateway bindet eine Subdomain (z. B. food.mycorp.com), die Definition des Pfades obliegt allein den REST-Ressourcen. Ist es für meinen Fall okay, das gesamte REST API über Subdomains zu trennen oder ist eine gemeinsame (Sub-)Domain besser?
  • Frontend-Architektur: Einzelne Frontends, die sich per URL integrieren? Einzelne Seiten, die server- oder clientseitig von einem zentralen UI integriert werden? Eine zentrale Single-Page-Application, die alle Domänen über ihr REST API integriert?
  • Kosten: Jeweils ein zentrales API Gateway, CloudFront & S3 alleine sind nicht teuer. Möchte man ein REST API und/oder UI pro Domäne, wird das mit steigender Anzahl der Domänen jedoch früher oder später auf der Rechnung sichtbar.
  • Developer Experience: Soll das REST API von externen Parteien angesprochen werden? Wird ein fachlich per Subdomains getrenntes REST API gut genug verstanden und akzeptiert?

fachliche Deployment-Einheiten (Domänen) haben eigene UIs/APIs oder es gibt zentrale UI und API für das gesamte System

Da das zentrale User Interface (sofern es eines gibt) von der REST API der fachlichen Deployment-Einheiten abhängt, bildet es eine eigene Deployment-Einheit. Entscheidet man sich auch für ein zentrales REST API, bildet dieses ebenfalls eine eigene Deployment-Einheit, welche auf Grund der Abhängigkeit vor dem User Interface ausgerollt werden sollte.

Nachdem auch REST API(s) und User Interface(s) ausgerollt wurden (initial oder zukünftig für Änderungen/Releases), ist es an der Zeit, unsere Ende-zu-Ende-Tests auf REST API(s) und User Interface(s) loszulassen.

Synchrone Kommunikation ist die Ausnahme

Manchmal benötigt eine Function (oder ein anderer Workload) auch synchron Daten von einer anderen Domäne. Anstatt der Verarbeitung von Events oder daraus entstehender Replikation von Daten zwischen Domänen funktioniert auch ein direkter Function-Aufruf. Eine Alternative stellt die Sammlung solcher internen synchronen Functions über ein eingeschränkt abrufbares API Gateway dar. Das Gateway und dessen Konsumenten könnten dafür in einem Amazon VPC liegen. Ein offensichtlicher Nachteil: Konsumenten von VPC-Ressourcen haben es bei der lokalen Entwicklung schwieriger, zuzugreifen.

Synchrone Kommunikation durch direkte AWS Lambda-Aufrufe zwischen zwei Domänen

Was all diese Arten von Schnittstellen in und um ein Serverless-System gemeinsam haben: Nach der Einführung gehen Produzenten und Konsumenten einen Vertrag ein. Der Produzent sollte Änderungen an seinen Events oder REST-Schnittstellen akribisch abwärtskompatibel halten oder API-Versionierung nutzen. Für die Dokumentation und Sicherstellung dieser Verträge empfiehlt es sich, Ende-zu-Ende-Tests gegen die APIs zu schreiben. Scripts mit HTTP-Client und AWS-SDK oder Tools wie Pact (Contract Testing) sind dafür gut geeignet.

Zusammenfassung

Wie jedes IT-System sind auch Serverless-Anwendungen auf bewusste und saubere Struktur angewiesen, damit sie langfristig weiterentwickelbar, sicher und wartbar sind.

Die Strukturierung beginnt in der Cloud beim Staging und der Account-Strategie, geht weiter bei der Gestaltung von Deployment-Einheiten (auch bei monolithischen Ansätzen) und zieht sich in die Definition unterschiedlicher Schnittstellen und „Kategorien“ von Functions durch.

Wurde die Wichtigkeit dieser Grundlagen von einem Team und der Organisation verinnerlicht und der Umgang mit den benötigten Cloud-Diensten einmal erlernt, wirkt die Implementierung hingegen eher wie ein Kinderspiel.

In der Public Cloud macht sich das Verschleppen von Anti-Pattern (Fehlnutzung von Services, Architektur und Programmiermodell) oft auch durch erhöhte Kosten bemerkbar. Tauchen auf einmal stark ansteigende Kosten für Managed Services auf, sollte man die letzten Design-Entscheidungen noch einmal Revue passieren lassen.

 

Mitglieder für unsere Gilde gesucht! Jetzt der Gilde als Cloud Native Developer und Consultant (w/d/m) beitreten

 

Jonas entwickelt als Full Stack Developer leidenschaftlich gerne digitale Produkte in agilen, crossfunktionen Teams. Dabei verwendet er am liebsten Cloud Native-Ansätze wie Serverless und Managed Services der AWS (Amazon Web Services). Ursprünglich von Java geprägt, greift er heute präferiert auf Node.js, TypeScript und moderne JavaScript-Libraries und Tools zurück.

Über 1.000 Abonnenten sind up to date!

Die neuesten Tipps, Tricks, Tools und Technologien.
Jede Woche direkt in deine Inbox.

Kostenfrei anmelden und immer auf dem neuesten Stand bleiben!
(Keine Sorge, du kannst dich jederzeit abmelden.)

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.