microservices - JAX https://jax.de/tag/microservices/ Java, Architecture & Software Innovation Fri, 18 Oct 2024 13:30:25 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Exactly Once in verteilten Systemen: Realität oder Utopie? https://jax.de/blog/exactly-once-idempotenz-java/ Mon, 10 Jun 2024 13:24:45 +0000 https://jax.de/?p=89825 Sind verteilte Systeme im Einsatz, wie es beispielsweise bei Microservices der Fall ist, ist eine verteilte Datenverarbeitung – häufig asynchron über Message Queues – an der Tagesordnung. Werden Nachrichten ausgetauscht, sollen diese häufig genau einmal verarbeitet werden – gar nicht so einfach, wie sich herausstellt.

The post Exactly Once in verteilten Systemen: Realität oder Utopie? appeared first on JAX.

]]>
Bei verteilten Systemen wird eine asynchrone Kommunikation häufig über einen Message Broker abgedeckt. Dadurch soll eine Entkopplung zwischen zwei Diensten erreicht werden, die unter Umständen separat skaliert werden können. Eine Kommunikation über einen Message Broker ist inhärent immer mindestens zweigeteilt, nämlich in Producer und Consumer.

Ein Producer erstellt dabei Nachrichten, wie in Abbildung 1 gezeigt, während ein Consumer sie verarbeitet.

Abb.1: Einfacher Nachrichtenaustausch

Der Message Broker ist häufig ein zustandsbehaftetes System – eine Art Datenbank – und vermittelt Nachrichten zwischen Producer und Consumer. Als zustandsbehaftetes System hat ein Message Broker die Aufgabe, Nachrichten vorzuhalten und abrufbar zu machen. Ein Producer schreibt also Nachrichten in den Broker, während ein Consumer sie zu einer beliebigen Zeit lesen kann. Exactly once, also einmaliges Ausliefern, bedeutet, dass der Producer genau eine Nachricht produziert und der Consumer diese genau einmal verarbeitet. Also ganz einfach, oder?

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Es kann so einfach sein

Da eine Kommunikation zwischen den Systemen über eine Netzwerkebene stattfindet, ist nicht gewährleistet, dass die Systeme über den gleichen Wissensstand verfügen. Es muss also einen Rückkanal geben, der einzelne Operationen bestätigt, um einen Zustand zu teilen. Nur so wird sichergestellt, dass Nachrichten, die erstellt wurden, auch korrekt angekommen und verarbeitet worden sind. Aktualisiert sieht der Fluss also eher so aus, wie es Abbildung 2 zeigt.

Abb. 2: Durch Bestätigungen wird der Zustand zwischen Systemen geteilt

Erstellt ein Producer eine Nachricht, die vom Consumer gelesen werden soll, benötigt dieser eine Bestätigung (Abb. 2, Schritt 2). Nur dadurch weiß der Producer, dass die Nachricht korrekt im Broker persistiert vorliegt und er sie nicht erneut übertragen muss.

Der Consumer wiederum liest die Nachricht und verarbeitet sie. Ist die Verarbeitung fehlerfrei abgeschlossen, bestätigt der Consumer das. Der Broker muss die Nachricht deshalb nicht noch einmal ausliefern.

Immer Ärger mit der Kommunikation

Bei einer verteilten Kommunikation über ein Netzwerk kann es leider immer passieren, dass diverse Kommunikationskanäle abbrechen oder Fehler entstehen – und zwar an vielen Stellen: beim Erstellen, vor dem Konsumieren und danach. Genau diese Eigenschaft macht es so schwer oder gar unmöglich, eine Exactly-once-Semantik zu erreichen.

Angenommen, ein Producer produziert eine Nachricht (Abb. 3). Um sicherzustellen, dass diese auch vom Broker gespeichert wurde, wartet der Producer auf eine Bestätigung. Bleibt sie aus, ist nicht garantiert, dass der Broker diese Nachricht wirklich ausliefern wird.

Abb. 3: Der Producer erhält keine Bestätigung und sendet die Nachricht erneut

Der Producer muss diese Nachricht folglich erneut ausliefern. Genau das ist in Schritt 2 der Abbildung 3 auch geschehen, weshalb der Producer in Schritt 3 eine weitere Nachricht mit demselben Inhalt sendet. Da jetzt zwei Nachrichten vorliegen, verarbeitet der Consumer in den Schritt 4 und 5 beide Nachrichten – wohl eher nicht „exactly once“. Die Nachricht wird durch den Retry-Mechanismus „at least once“ – mindestens einmal, nicht genau einmal – übertragen. Denn wie im Bild zu erkennen ist, überträgt der Producer dieselbe Nachricht zweimal, um sicherzustellen, dass sie mindestens einmal vom Broker bestätigt wurde. Nur so ist sichergestellt, dass die Nachricht nicht verloren geht.

ALL ABOUT MICROSERVICES

Microservices-Track entdecken

 

Natürlich kann die Bestätigung auch ignoriert werden. Schritt 2 kann also ausbleiben. Ein Retry-System würde folglich fehlen. Der Producer überträgt also eine Nachricht, ohne auf eine Bestätigung des Brokers zu warten. Kann der Broker die Nachricht selbst nicht verarbeiten oder wegspeichern, hat er keine Möglichkeit, das Fehlschlagen oder eine erfolgreiche Operation zu quittieren. Die Nachricht würde „at most once“ – maximal einmal oder eben keinmal – übertragen werden. Exactly once ist also grundsätzlich ein Problem verteilter Anwendungen, die mittels Bestätigungen funktionieren.

Leider ist das noch nicht das Ende der Fahnenstange, wenn die Nachricht Ende zu Ende, also vom Producer bis zum Consumer, betrachtet wird. Denn es existiert in einem solchen System zusätzlich ein Consumer, der die Nachrichten wiederum einmalig verarbeiten muss. Selbst wenn garantiert wird, dass der Producer eine Nachricht einmalig erzeugt, ist ein einmaliges Verarbeiten nicht garantiert.

Abb. 4: Ein Consumer verarbeitet die Nachricht und versucht diese danach zu bestätigen

Es kann passieren, dass der Consumer wie in Abbildung 4 gezeigt die Nachricht in Schritt 3 liest und in Schritt 4 korrekt verarbeitet. In Schritt 5 geht die Bestätigung verloren. Das führt dazu, dass die Nachricht mehrmals, aber mindestens einmal – at least once – verarbeitet wird.

Abb. 5: Ein Consumer bestätigt die Nachricht vor dem Verarbeiten

Es ist natürlich umgekehrt auch möglich, die Nachricht vor dem Verarbeiten zu bestätigen. Der Consumer lädt also die Nachricht und bestätigt sie direkt. Erst dann wird in Schritt 5 von Abbildung 5 die Bearbeitung der Nachricht erfolgen. Schlägt jetzt die Bearbeitung fehl, ist die Nachricht in Schritt 4 bereits bestätigt worden und wird nicht erneut eingelesen. Die Nachricht wurde wieder maximal einmal oder keinmal – at most once – verarbeitet.

Wie also zu erkennen, ist es leicht, At-most-once- und At-least-onceSemantiken in den verschiedenen Konstellationen sowohl auf Producer- als auch auf der Consumer-Seite herzustellen. Exactly once ist aber aufgrund der verteilten Systematik ein schwieriges Problem – oder gar unmöglich?

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Lösungen müssen her

Für eine Möglichkeit, eine Exactly-once-Semantik zu erreichen, muss die Verarbeitung der Nachrichten einer Applikation eine bestimmte Eigenschaft unterstützen: Idempotenz. Idempotenz bedeutet, dass eine Operation, egal wie oft sie verarbeitet wird, immer dasselbe Ergebnis zur Folge hat. Ein Beispiel dieses Prinzips könnte das Setzen einer Variablen im Programmcode sein. Hier gibt es etwa die Möglichkeit, dies über Setter oder eben relative Mutationen zu implementieren.

Zum Beispiel setAge oder incrementAge. Die Operation person.setAge(14); kann beliebig oft nacheinander ausgeführt werden, das Ergebnis bleibt immer dasselbe, nämlich 14. Hingegen wäre person.incrementAge(1) nicht idempotent. Wird diese Methode unterschiedlich oft hintereinander ausgeführt, gibt es verschiedene Ergebnisse, nämlich nach jeder Ausführung ein Jahr mehr. Genau diese Eigenschaft der Idempotenz ist der Schlüssel, um eine Exactly-once-Semantik zu etablieren.

Angewandt auf die Systeme von zuvor bedeutet das, dass eine At-least-once-Semantik mit der Eigenschaft der Idempotenz zu einer Exactly-once-Verarbeitung führen kann. Wie eine At-least-once-Semantik umgesetzt werden kann, zeigt das zuvor beschriebene Bestätigungssystem. Was fehlt, ist also ein System von Idempotenz in der Verarbeitung. Aber wie kann eine Verarbeitung von Nachrichten idempotent gemacht werden?

Um das zu erreichen, muss der Consumer die Möglichkeit haben, einen lokalen, synchronisierten Zustand zu erhalten. Um den Zustand einer Nachricht zu erhalten, muss diese eindeutig identifizierbar sein. Nur so werden das Aufsuchen und eine Deduplizierung der Nachricht ermöglicht.

Abb. 6: Eine idempotente Verarbeitung

Anders als zuvor speichert der Consumer mit jedem Aufruf in Schritt 4 der Abbildung 6 die Nachricht zunächst in einer lokalen Zustandshaltung. An dieser Stelle kann, sofern die Nachricht bereits lokal vorhanden ist, ein erneutes Speichern vernachlässigt werden. In Schritt 5 wird die Nachricht bestätigt. Schlägt die Bestätigung fehl und wird die Nachricht folglich erneut übertragen, ist das kein Problem, da in Schritt 4 das erneute Speichern der Nachricht verhindert werden kann. An dieser Stelle lebt also die Idempotenz. Beim Bearbeiten kann der Consumer nun selbst entscheiden, ob eine Verarbeitung notwendig ist, z. B. indem zu einer Nachricht ein Status eingeführt und dieser in Schritt 6 lokal abgefragt wird. Steht dieser bereits auf Processed, muss nichts getan werden. Umgekehrt muss eine verarbeitete Nachricht den Status korrekt aktualisieren.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Fazit

Verteilte Systeme haben ein grundsätzliches Problem, eine Exactly-once-Semantik herzustellen. Es kann auf Infrastrukturebene entweder zwischen at least once oder at most once gewählt werden. Erst durch die Eigenschaft der Idempotenz kann auf dem Applikationslevel sichergestellt werden, dass Nachrichten genau einmal von Ende zu Ende verarbeitet werden.

Natürlich ist das nicht kostenlos. Es bedeutet, dass die Applikation selbst eine Verwaltung von Nachrichten übernehmen muss und deren Zustand verwaltet – wirklich exactly once ist das natürlich auch nicht, es kommt diesem durch die Eigenschaft der Idempotenz jedoch im Ergebnis sehr nahe.

The post Exactly Once in verteilten Systemen: Realität oder Utopie? appeared first on JAX.

]]>
Einfacher als gedacht https://jax.de/blog/einfacher-als-gedacht/ Tue, 24 Aug 2021 07:06:56 +0000 https://jax.de/?p=83973 In der Regel bestehen Microservices-Projekte aus mehreren einzelnen Services, die als getrennte Deployment-Einheiten separat betrieben werden und sich dabei gegenseitig aufrufen. Für die Security des Gesamtsystems ergeben sich hieraus mehrere Konsequenzen. Zum einen muss jeder einzelne Microservice für sich gewisse Securityrichtlinien beachten. Doch das allein reicht noch nicht aus, da auch die Kommunikation mit den anderen Services abgesichert werden muss.

The post Einfacher als gedacht appeared first on JAX.

]]>
Erst durch beide Maßnahmen erreicht das Geflecht an Services einen Securitystandard, der bei einem monolithischen System sehr viel einfacher zu erreichen ist. Der administrative Umgang mit den Zertifikaten für die Transport Layer Security (TLS) kann bei einem verteilten System einen recht erheblichen Aufwand verursachen.

Allein schon durch die hohe Anzahl an eigenständigen Services erhöht sich die Wahrscheinlichkeit einer Sicherheitslücke. Sobald neue sogenannte Common Vulnerabilities and Exposures (CVEs) veröffentlicht werden, müssen diese unter Umständen in mehreren Services zeitnah ausgebessert werden. Erst wenn alle Microservices anschließend neu deployt wurden, gilt dieses Sicherheitsrisiko als behoben. Ein möglicher Angreifer hat im Grunde die Qual der Wahl, welchen der vielen Services er als Erstes zu kompromittieren versuchen soll. Sollte der Angriffsversuch auf den ersten Microservice nicht zum Erfolg führen, hat er noch genügend andere Opfer, über die er in das Geflecht der Microservices eindringen kann. Werden die Services über eine ungesicherte Verbindung aufgerufen, so ergeben sich für den Hacker noch mehr Möglichkeiten, in das Gesamtsystem einzubrechen.

Zero Trust

Werden die zuvor genannten Securityprobleme bei Microservices konsequent zu Ende gedacht, kommt man im Grunde zu dem Ergebnis, dass nur ein Zero-Trust-Ansatz einen ausreichenden Sicherheitsschutz bieten kann.

Zero Trust besagt, dass im Grunde keinem (Micro-)Service vertraut werden darf, sogar dann nicht, wenn er sich in einer sogenannten Trusted Zone befindet. Jeder Request zwischen den Services muss authentifiziert (AuthN) und autorisiert (AuthZ) werden und mittels TLS abgesichert sein. Als Authentifikationsmerkmal wird oft ein JSON Web Token (JWT) verwendet, das bei jedem Request mitgeschickt werden muss und somit den Aufrufer identifiziert. Dieses JWT wird entweder End-to-End verwendet, d. h., dass es während der gesamten Aufrufkette nicht ausgetauscht wird, oder man generiert sich mit einem TokenExchangeService für jeden einzelnen Request ein eigenes neues Token. Zu guter Letzt soll die Absicherung nicht nur auf der HTTP- oder gRPC-Ebene stattfinden (OSI Layer 7 Application), sondern auch in den darunter liegenden Netzwerkschichten Transport und Network (OSI Layer 4 und 3). Damit wird dem OWASP-Securityprinzip [1] Defense in Depth genüge getan.

Nicht jeder sicherheitsverantwortliche Mitarbeiter will diesen finalen Schritt gehen, da klar erkennbar ist, dass die Umsetzung von Zero Trust nicht gerade trivial ist. In der Gegenüberstellung Aufwand gegen Nutzen wird dabei oft der (falsche) Schluss gezogen, dass für Zero Trust der Aufwand viel zu hoch wäre, obwohl einem die innere Securitystimme sagt, dass Zero Trust der richtige Ansatz ist. Mittlerweile gibt es jedoch Systeme, die den Aufwand für Zero Trust sehr stark minimieren, womit der Nutzen die Oberhand gewinnt.

Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

Zero Trust bei Microservices

Als Konsequenz der vorherigen Überlegungen müssen für Zero Trust bei Microservices mehrere Funktionalitäten umgesetzt werden. Jeder Request muss Informationen über den Aufrufer enthalten. Am einfachsten gelingt das mit einem JWT, das im HTTP-Header Authorization als Typ Bearer mitgeschickt wird. Der aufgerufene Service validiert dieses JWT und führt dann die notwendigen Berechtigungsprüfungen durch. Bei Verwendung eines Service-Mesh-Tools kann diese Prüfung auch von einem Sidecar übernommen oder ergänzt werden (dazu mehr in den folgenden Abschnitten). Zur Absicherung der Kommunikation mit TLS sind SSL-Zertifikate notwendig, die die Eigenschaft haben, dass sie nach einem gewissen Zeitintervall ungültig werden und somit ausgetauscht werden müssen. Das kann wegen der hohen Anzahl an Microservices nicht manuell erfolgen. Für diese administrative Aufgabe bieten Service-Mesh-Tools eine passende Automatisierung an, wodurch der Aufwand für die Ops-Kollegen gegen Null geht.

Um jetzt noch dem OWASP-Securityprinzip Defense in Depth gerecht zu werden, sollten die Kommunikationsverbindungen zwischen den Microservices mit Firewallregeln berechtigt oder unterbunden werden. Somit erfolgt eine weitere Absicherung der Kommunikation auf TCP/IP-Ebene. Kubernetes sieht dafür das Konzept der Network Policies vor. Auch hierzu mehr in den kommenden Abschnitten.

Typischer Ausgangspunkt

Die meisten Microservices-Projekte, die auf Kubernetes betrieben werden, starten mit der Ausgangssituation in Abbildung 1.

Abb. 1: Low Secure Deployment

Für die Ingress-Kommunikation stellen die Cloud-Betreiber passende Komponenten zur Verfügung oder beschreiben mit Tutorials, wie diese Komponenten einfach installiert werden können. Der Kommunikationspfad aus dem Internet geht meist über eine Firewall und wird mit einem LoadBalancer in Richtung Kubernetes-Cluster geroutet. Dafür stellen die Cloud-Provider oft fertige Lösungen zur Verfügung, die auch sehr einfach auf TLS umgestellt werden können.

Jetzt beginnt der Teil, bei dem man eigenverantwortlich weitere Maßnahmen zur Sicherheit umsetzen muss. Ein Ingress-Controller innerhalb Kubernetes kümmert sich dann um die Weiterleitung des Requests innerhalb des Clusters. Dieser wird typischerweise als SSL-Endpunkt mit einem firmeneigenen Zertifikat ausgestattet. Als Folge davon werden die Requests nach der SSL-Terminierung ohne TLS an die jeweiligen Microservices weitergeleitet, d. h., die Kommunikation innerhalb des Clusters erfolgt komplett ohne Absicherung. Manche der Microservices (oder doch schon alle?) besitzen eine Securityprogrammierung, die das empfangene JWT validiert und anschließend mit den enthaltenen Claim-Werten die Berechtigungsprüfung durchführt. Zur Validierung des JWT müssen (sporadisch) Requests zum Identity Provider (IDP), der das JWT ausgestellt hat, abgeschickt werden. Das erfolgt in der Regel mit TLS, da der IDP nur einen HTTPS-Zugang anbietet. Die interne Aufwand-Nutzen-Analyse hat ergeben, dass mit den vorhandenen Mitteln bei vertretbarem Aufwand ein gewisser Grad an Security erreicht worden ist. Das Projektteam ist sich bewusst, dass dieses Setting keinem Zero-Trust-Ansatz entspricht, glaubt aber, dass der Aufwand für Zero Trust viel zu hoch ist. Mangels besseren Wissens gibt man sich mit diesem (geringen) Level an Security zufrieden.

Service Mesh

Ein sehr viel höheres Securitylevel kann man mit Service-Mesh-Tools wie Istio oder Linkerd (u. v. m.) erreichen. Diese Tools bieten hierfür entsprechende Funktionalitäten an, die zum Beispiel das Zertifikatsmanagement automatisieren. Am Beispiel von Istio sollen diese Features genauer betrachtet werden.

Mutual TLS

Jeder Service, der Bestandteil eines Service Mesh wird, bekommt ein sogenanntes Sidecar, das die eingehende und ausgehende Kommunikation zum Service steuert und überwacht. Gesteuert wird das Verhalten des Sidecar über eine zentrale Steuerungskomponente, die dem Sidecar die passenden Informationen und Anweisungen übermittelt. Beim Start des Pods, der aus dem Service und dem Sidecar besteht, holt sich das Sidecar ein individuelles SSL-Zertifikat von der zentralen Steuereinheit ab. Damit ist das Sidecar in der Lage, eine Mutual-TLS-Verbindung (mTLS) zu etablieren. Nur der Request vom Sidecar zum Service, also die Kommunikation innerhalb des Pods, erfolgt dann ohne TLS. Nach einem vordefinierten Zeitintervall (bei Istio ist der Default 24 h), lässt sich das Sidecar automatisch ein neues Zertifikat vom Steuerungsservice ausstellen. Dieses neue Zertifikat wird dann für die nächsten 24 Stunden für die mTLS-Verbindung verwendet. Mit der in Listing 1 gezeigten Istio-Regel wird eine mTLS-Kommunikation für das gesamte Service Mesh verpflichtend.

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT

Für eine Übergangslösung, in der noch nicht alle Services in den Service Mesh integriert wurden, gibt es einen speziellen Modus: PERMISSIVE. Damit werden Services innerhalb des Service Mesh mit mTLS angesprochen und Services, die außerhalb des Service Mesh laufen, werden weiterhin ohne TLS aufgerufen. Auch das erfolgt automatisch und wird vom jeweiligen Sidecar, das den Aufruf initiiert, entsprechend ausgeführt.

Der Vorteil für die Security liegt hier auf der Hand. Die Kommunikation innerhalb des Service Mesh erfolgt über mTLS, und das Zertifikatsmanagement läuft in kurzen Zeitintervallen vollautomatisch ab. Darüber hinaus erfolgt das gesamte mTLS-Handling ohne einen Eingriff in den Code des Service, läuft also aus Sicht des Service völlig transparent ab. Diese Absicherung auf dem Application Layer des Netzwerkstacks kann dann ohne Probleme mit Absicherungen auf Layer 3 und 4 kombiniert werden (siehe kommende Abschnitte).

Ingress Gateway

Istio bietet auch ein sogenanntes Ingress Gateway an, das den Eintrittspunkt in den Kubernetes-Cluster und somit in das Service Mesh regelt. Es kann damit den Ingress Controller aus der vorherigen Systemlandschaft (Abb. 1) vollständig ersetzen.

Man könnte Istio auch ohne Ingress Gateway betreiben, würde dann aber eine Menge an Funktionalitäten verlieren. Der Vorteil des Ingress Gateway liegt darin, dass dort schon die gesamten Istio-Regeln greifen. Somit sind alle Regeln für Trafficrouting, Security, Releasing usw. anwendbar. Auch die Kommunikation vom Ingress Gateway zum ersten Service ist bereits mit mTLS abgesichert. Im Falle eines alternativen Ingress Controllers wäre dieser Request noch ohne SSL-Absicherung. Die Bezeichnung Gateway (anstatt Controller) wurde von Istio ganz bewusst gewählt, da mit dem existierenden Funktionsumfang das Ingress Gateway auch als sogenanntes API Gateway betrieben werden kann.

Für den gesicherten Eintritt in das Cluster kann das Ingress Gateway mit dem entsprechenden Firmenzertifikat konfiguriert werden, ganz analog zum Vorgehen bei einem Ingress Controller. Die Istio-Regel in Listing 2 definiert die Funktionsweise des Ingress Gateways.

apiVersion: networking.istio.io/v1alpha3
  kind: Gateway
  metadata:
    name: mygateway
  spec:
    selector:
      istio: ingressgateway
    servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: mytls-credential
      hosts:
      - myapp.mycompany.de
Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

 

Es wird ein SSL-Port (443) für alle Requests auf den Hostnamen myapp.mycompany.de geöffnet und das zugehörige SSL-Zertifikat wird aus dem Kubernetes Secret mytls-credential ausgelesen. Die Erstellung des SSL-Secrets erfolgt mit der Kubernetes-Regel in Listing 3.

apiVersion: v1
   kind: Secret
   metadata:
     name: mytls-credential
   type: kubernetes.io/tls
   data:
     tls.crt: |
           XYZ...
     tls.key: |
           ABc...

Damit ist auch hier der Eingang in den Cluster mit TLS abgesichert und die weiterführende Kommunikation erfolgt mit dem mTLS-Setting von Istio.

Network Policy

Nachdem die HTTP-Ebene mit SSL abgesichert ist, sollten nun noch weitere Netzwerkschichten (OSI Layer 3 und 4) abgesichert werden. Um in Kubernetes so etwas wie Firewalls zu etablieren, gibt es das Konzept der Network Policy. Diese Policies definieren die Netzwerkverbindungen zwischen den Pods, wobei die Einhaltung der Regeln von einem zuvor installierten Netzwerk-Plug-in durchgesetzt werden. Network Policies ohne ein solches Netzwerk-Plug-in haben keinen Effekt. Kubernetes bietet eine große Auswahl an Plug-ins [2] an, die man in einem Kubernetes-Cluster installieren kann.

Als Best Practice gilt es, eine sogenannte Deny-All-Regel zu definieren. Damit wird im gesamten Cluster die Netzwerkkommunikation zwischen den Pods unterbunden. Die Deny-All-Ingress-Regel verbietet jede eingehende Kommunikation auf Pods im zugehörigen Namespace (Listing 4).

apiVersion: networking.k8s.io/v1
  kind: NetworkPolicy
  metadata:
    name: default-deny-ingress
    namespace: my-namespace
  spec:
    podSelector: {}
    policyTypes:
    - Ingress

Nachdem diese Regel aktiviert wurde, kann man nun gezielt die einzelnen Ingress-Verbindungen freischalten. Die Regel in Listing 5 gibt beispielsweise den Ingress-Traffic auf den Pod mit dem Label app=myapp frei, aber nur wenn der Request vom Ingress Gateway kommt.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: access-myapp
  namespace: my-namespace
spec:
  podSelector:
    matchLabels:
      app: myapp
  ingress:
  - from:
    - podSelector:
        matchLabels:
          istio: ingressgateway

Für jede weitere zulässige Verbindung muss die Regel entsprechend erweitert oder es müssen zusätzliche Regeln definiert werden. Durch die Etablierung der zuvor genannten Regeln hat sich das Anfangs-Deployment (Abb. 1) verändert, wie in Abbildung 2 gezeigt wird.

Abb. 2: Medium Secure Deployment

Die SSL-Terminierung wird nun vom Istio Ingress Gateway ausgeführt. Jeder weitergeleitete Request wird mit mTLS abgesichert. Zusätzlich definiert eine Network Policy je Pod den gewollten Ingress Request. Alle ungewollten Requests werden vom Network-Plug-in unterbunden. Damit ist schon mal ein großer Schritt in Richtung Zero Trust umgesetzt. Was jetzt noch fehlt, sind Berechtigungsprüfungen, die die Zulässigkeit der Aufrufe noch weiter eingrenzen.

Authentication (AuthN) und Authorization (AuthZ)

Für AuthN und AuthZ bietet Istio eine Menge an Regeln, mit denen man sehr granular steuern kann, welche Aufrufe berechtigt sind und welche nicht. Diese Regeln werden von den Sidecars und vom Ingress Gateway beachtet, wodurch der gesamte definierte Regelsatz überall im Service Mesh angewandt wird. Auch hiervon merkt die jeweilige Applikation nichts, da dies transparent von den jeweiligen Sidecars übernommen wird.

Die Authentifizierung erfolgt auf Basis eines JSON Web Tokens, das von Istio überprüft wird. Dazu werden die notwendigen JWT-Validierungen ausgeführt, wobei auf den ausstellenden Identity Provider (IDP) zugegriffen wird. Nach erfolgreicher Prüfung gilt der Request innerhalb des gesamten Service Mesh als authentifiziert. Am besten geschieht das im Ingress Gateway, womit die Prüfung gleich beim Eintritt in das Service Mesh bzw. Cluster ausgeführt wird.

Mit der Regel in Listing 6 wird Istio angewiesen, das empfangene JWT gegen den IDP mit dem URL unter [3] zu validieren.

apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  name: ingress-idp
  namespace: istio-system
spec:
selector:
    matchLabels:
      istio: ingressgateway
  jwtRules:
  - issuer: "my-issuer"
    jwksUri: https://idp.mycompany.de/.well-known/jwks.json

Wie aus der Regel ersichtlich wird, sind die Issuer als Array zu definieren, d. h. es können auch mehrere unterschiedliche IDPs angegeben werden.

Damit gilt der Request zwar als authentifiziert, es finden aber noch keine Berechtigungsprüfungen statt. Diese müssen separat mit einem anderen Regeltyp angegeben werden. Ebenso wie bei der Network Policy gibt es auch hier eine Best Practice, mit der alle Zugriffe innerhalb des Service Mesh als nicht berechtigt deklariert werden. Dies kann mit der Regel in Listing 7 festgelegt werden.

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-nothing
  namespace: istio-system
spec:
  {}

Jetzt kann wie zuvor bei der Network Policy jeder einzelne Zugriff auf einen genau spezifizierten Pod sehr feingranular geregelt werden. Innerhalb der Regel kann in den Abschnitten from, to und when definiert werden, woher der Request kommen muss, welche HTTP-Methoden und Endpunkte aufgerufen werden sollen und welche Authentifizierungsinhalte (Claims im JWT) enthalten sein müssen. Erst wenn alle diese Kriterien zutreffen, wird der Zugriff erlaubt (action: allow), wie in Listing 8 gezeigt wird.

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: my-app
  namespace: my-namespace
spec:
  selector:
    matchLabels:
      app: my-app
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/ns-xyz/sa/my-partner-app"]
- source:
        namespaces: ["ns-abc", "ns-def"]
    to:
    - operation:
        methods: ["GET"]
        paths: ["/info*"]
    - operation:
        methods: ["POST"]
        paths: ["/data"]
    when:
    - key: request.auth.claims[iss]
      values: ["https://idp.my-company.de"]

Neben der Möglichkeit, einen Request mit der action: allow zu berechtigen, gibt es noch die Möglichkeit, gewisse Request zu verbieten (action: deny) oder mit action: custom eigene Berechtigungsprüfungen in den Service Mesh zu integrieren. Damit ist es möglich schon vorhandene Berechtigungssysteme, wie sie in vielen Unternehmen existieren, weiterhin zu nutzen.

Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

 

Finales System

Nach Anwendung der gesamten Regeln ergibt sich die Zero-Trust-Infrastruktur in Abbildung 3.

Abb. 3: Secure Deployment

Neben der mit TLS abgesicherten Kommunikation sind jetzt noch Berechtigungsprüfungen hinzugekommen, die im Grunde in jedem der Sidecars ausgewertet werden. Als Basis für die Authentifizierung dient das im HTTP-Header mitgeschickte JWT. Um diesen finalen Zustand zu erreichen, sind die in Tabelle 1 gezeigten Regeln notwendig.

Funktion Regeln
TLS Terminierung Gateway-Regel und Kubernetes Secret
mTLS Network Policy (Deny Ingress) und jeweils eine Network Policy pro Pod
Network Segmentation Request Authentication
Authentication Authorization Policy (Allow Nothing) und jeweils eine Authorization Policy pro Pod
Authorization Regeln

Tabelle 1: Übersicht Regeln

Insgesamt also nur sechs Basisregeln und pro Pod noch zwei weitere Regeln zur Zugriffssteuerung. Mit dieser geringen Anzahl an Regeln sollte nun die zuvor aufgestellte Aufwand- Nutzen-Analyse für eine Zero-Trust-Infrastruktur zu einem ganz anderen Ergebnis führen.

Fazit

Zugegeben, der Aufwand, ein Service-Mesh-Tool im Projekt zu etablieren, ist nicht gerade gering. Aber neben den ganzen Securityaspekten bieten diese Tools noch sehr viel mehr an Funktionalität, die einem beim Betrieb von Microservices einen sehr guten Dienst erweisen können. Rein aus dem Gesichtspunkt der Security wäre der Aufwand für ein Service-Mesh-Tool wohl relativ hoch, aber im Zusammenwirken mit den anderen Service-Mesh-Funktionalitäten wie Trafficrouting, Resilience und Releasing ergibt sich durchaus eine positive Bilanz in der Aufwand-Nutzen-Analyse. Außerdem wäre ein automatisiertes Zertifikatsmanagement auch nicht gerade trivial und bei einer selbst implementierten Lösung vielleicht auch nicht ganz fehlerfrei. Im Bereich der Security sollte man sich aber keine Fehler erlauben.

Die Kombination mit der Network Policy kann ohne Einflüsse auf Istio parallel etabliert werden, womit einem Defense-in-Depth-Ansatz entsprochen wird. Die Berechtigungsprüfungen können sehr fein gesteuert werden und lassen im Grunde keine Wünsche offen. Hier ist allerdings Vorsicht geboten, da durch die umfangreichen Möglichkeiten Situationen entstehen können, die sehr komplex und damit nur noch schwer verständlich sind. „Keep it simple, stupid“ (KISS) sollte hier als Handlungsoption immer wieder in Betracht gezogen werden.

Um auch bei der Berechtigungsprüfung einen Defense-in-Depth-Ansatz zu etablieren, sollte natürlich noch in den jeweiligen Applikationen eine Berechtigungsprüfung stattfinden. Im Bereich des Auditing wird von Istio derzeit nur Stackdriver unterstützt. Hier wäre eine größere Auswahl an Auditsystemen wünschenswert. Doch was noch nicht ist, kann ja noch werden.

Insgesamt lässt sich mit ein paar Kubernetes- bzw. Istio-Regeln eine Zero-Trust-Infrastruktur etablieren, die wohl bei jedem Securityaudit standhalten wird.

Links & Literatur

[1] https://github.com/OWASP/DevGuide/blob/master/02-Design/01-Principles%20of%20Security%20Engineering.md

[2] https://kubernetes.io/docs/concepts/cluster-administration/addons/

[3] https://idp.mycompany.de/.well-known/jwks.json

The post Einfacher als gedacht appeared first on JAX.

]]>
Lost in Transaction: Businesstransaktionen mit Microservices umsetzen https://jax.de/blog/microservices/lost-in-transaction-businesstransaktionen-mit-microservices-umsetzen/ Fri, 15 Feb 2019 11:20:52 +0000 https://jax.de/?p=67064 Die Auftrennung des historisch gewachsenen Monolithen in eine Unmenge von Microservices scheint heute Pflicht. Wirklich unabhängig sind die Services am Ende allerdings nur dann, wenn die Trennung konsequent auf allen Ebenen vollzogen wird. Nicht nur den Code, sondern auch die Datenbank gilt es zu trennen. Willkommen in der Wunderwelt der verteilten Transaktionen.

The post Lost in Transaction: Businesstransaktionen mit Microservices umsetzen appeared first on JAX.

]]>

von Lars Röwekamp

Die Vorteile Microservices-basierter Architekturen sind hinlänglich bekannt. Ist erst einmal ein passender Schnitt gefunden – Domain-driven Design lässt grüßen –, können die einzelnen Services mehr oder minder unabhängig voneinander entwickelt, getestet und deployt werden. Dies erhöht die Agilität der Teams und vermindert die Umsetzungsdauer neuer Features (a.k.a. Time to Market).

Eine wesentliche Grundvoraussetzung für den Erfolg einer Microservices-basierten Architektur ist dabei die konsequente Trennung der Services und ihrer Ressourcen. Getreu dem Motto „share nothing“ wird nicht nur der Sourcecode voneinander getrennt, sondern auch die zugehörige Datenhaltung. Nur wenn auch dieser Schritt konsequent gegangen wird, kann tatsächlich von einer losen Kopplung und einer damit einhergehenden Unabhängigkeit der Services gesprochen werden.

Was ist nun aber, wenn sich ein Use Case über mehrere Services und somit auch über mehrere Datenquellen aufspannt? Nehmen wir als Beispiel eine stark vereinfachte Variante eines Check-out-Prozesses innerhalb eines Webshops. Im Rahmen des Check-outs, also des Verkaufs von Produkten an einen Kunden, wird die Anzahl der Produkte innerhalb des Lagersystems um die gekaufte Menge reduziert. In einem monolithischen System würde dies transaktional erfolgen. In einem Microservices-basierten System dagegen würde es wahrscheinlich einen Check-out-Service und einen Inventory Service inkl. eigener Datenhaltung geben. Durch die Auftrennung der Datenhaltung und damit auch der Datenbank ist die Umsetzung der benötigten Transaktion nicht so ohne Weiteres realisierbar. Wir brauchen einen Plan B.

 

Versuch 1: Transaktionen vermeiden

Die aus technologischer Sicht einfachste Variante, mit dem eben beschriebenen Problem umzugehen, ist, die Services so zu schneiden, dass sich Transaktionen grundsätzlich nur innerhalb eines Service abspielen. Was sich in der Theorie denkbar einfach anhört, hat in der Praxis enorme Konsequenzen. Zwar vermeiden wir durch den Ansatz die Notwendigkeit des Aufteilens einer Transaktion auf mehrere Services, sorgen aber auf der anderen Seite für unnatürliche Service-Grenzen.

Nehmen wir noch einmal das Beispiel von oben. Um die notwendige Transaktion zu realisieren, müssten wir die beiden Services Checkout und Inventory zu einem CheckoutAndInventory Service zusammenlegen. Die erhofften Vorteile der Microservices-basierten Architektur gingen so verloren. Wir enden aus fachlicher Sicht in einem zu großen Service. Der Service hätte durch den Wegfall von Seperation of Concerns und Single Responsibility mehr als nur einen fachlichen Verantwortlichen und würde wahrscheinlich eine Größe einnehmen, die nicht mehr von nur einem agilen Team gemanagt werden kann.

Natürlich könnte man ggf. auch noch einmal die Grenzen der Services prüfen und den fachlichen Schnitt derart verschieben, dass zwar die notwendige Transaktion Teil eines der beiden Services wird, ansonsten aber weiterhin zwei Services bestehen bleiben. Dies ist dann eine Option, wenn die Services eher auf Entitäten aufbauen als auf Use Cases und somit von Anfang an ein ungünstiger Service-Schnitt gewählt wurde. In unserem Fall stellt das nicht wirklich eine Option dar. Was also tun?

Your Coffee Shop Doesn’t Use Two-Phase Commit

Als Erstes sollten wir uns einmal die Frage stellen, ob wir denn tatsächlich eine Transaktion benötigen. Was für eine dumme Frage, wird sich jetzt sicherlich der eine oder andere Leser denken. Natürlich brauchen wir eine Transaktion. Schließlich finden schreibende Zugriffe auf zwei Tabellen statt. Und wenn es eins zu vermeiden gilt, dann ist das doch wohl ein inkonsistenter Zustand in unserer Datenbank! Aus technologischer Sicht ist diese Aussage sicherlich korrekt. Aber stimmt sie auch aus fachlicher Sicht? Ist die Welt tatsächlich so transaktional, wie wir sie in unseren Systemen immer darstellen? Zu dieser Frage hat sich bereits vor fünfzehn Jahren Gregor Hohpe Gedanken gemacht und in seinem Artikel „Your Coffee Shop Doesn’t Use Two-Phase Commit“ [1] am Beispiel des Kaffeebestellvorgangs innerhalb der Starbucks-Läden beeindruckend erläutert, um wie viel besser Systeme skalieren könnten, wenn man auf die strenge Einhaltung von Transaktionen verzichtet und stattdessen lediglich garantiert, dass die Konsistenz der Daten zu einem bestimmten Zeitpunkt sichergestellt ist. Eventual Consistency – also letztendliche Datenkonsistenz – ist hier das Zauberwort der Stunde.

Okay, mag sich jetzt der deine oder andere denken, wir reden hier über Kaffee. Was ist aber mit dem obigen Beispiel des Shopsystems? Oder gar mit einem Bankensystem, bei dem Geld von Konto A nach Konto B transferiert wird?

Nehmen wir zunächst das Beispiel des Shopsystems. Aus fachlicher Sicht stellt sich weniger die Frage, ob der Produktzähler im Inventory Service zu jedem Zeitpunkt auf dem aktuellen Stand ist, sondern vielmehr, ob das bestellte Produkt an den Kunden in einer vordefinierten Zeit geliefert werden kann oder nicht. Falls das auch dann gewährleistet werden kann, wenn bei einem Lagerbestand von null z. B. auf eingehende Retouren zurückgegriffen werden kann oder Lieferanten eine kurzfristige Lieferung bei Nachbestellung garantieren, dann bringt die Sicherstellung der Konsistenz zwischen Check-out und Inventory Service nicht wirklich einen fachlichen Mehrwert. Natürlich muss letztendlich („eventual“) sichergestellt werden, dass spätestens zum Zeitpunkt einer Inventur die Produktzähler im Inventory Service mit dem tatsächlichen Lagerbestand übereinstimmen. Für unseren Bestellprozess ist das aber nicht wirklich essenziell. Tatsächlich wird sich diese Tatsache in vielen Shopsystemen zunutze gemacht, um den Check-out-Prozess zu beschleunigen und so für den Nutzer eine bessere Usability zu erreichen.

Schauen wir uns nun das Beispiel des Bankensystems an. Wer schon einmal Geld von einem Konto auf ein anderes überwiesen hat, der weiß, dass dies definitiv kein transaktionaler Vorgang ist. In der Regel erfolgt die Abbuchung auf dem eigenen Konto sehr zeitnah. Die Wertstellung auf dem Gegenkonto kann dagegen bis zu mehreren Tagen dauern. Auch hier gilt wieder, dass lediglich sichergestellt sein muss, dass die Wertstellung letztendlich erfolgt, wir also garantieren können, dass es nach einer endlichen und fachlich vertretbaren Zeit zu einem konsistenten Zustand der Daten innerhalb des Systems kommt. Was wäre aber, wenn in der Zwischenzeit das Zielkonto gesperrt oder gar geschlossen würde? Bei einer klassischen Transaktion käme es hier zu einem Rollback. In unserem Fall muss sichergestellt sein, dass der Betrag dem Ausgangskonto wieder gutgeschrieben wird.

Für die eben gezeigten Beispiele lassen sich Transaktionen also durchaus vermeiden, indem fachliche Alternativen umgesetzt werden. Was aber, wenn es dennoch den Bedarf für Transaktionen gibt, die über Service-Grenzen hinausgehen?

 

 

Versuch 2: fachliche Transaktionen

Wir haben im obigen Beispiel des Bankensystems gesehen, dass die ursprüngliche Transaktion in mehrere Schritte aufgeteilt wurde, die zeitlich versetzt abgearbeitet werden können. Gleichzeitig wurde sichergestellt, dass am Ende entweder alle angedachten Schritte ordnungsgemäß ausgeführt wurden oder alternativ eine Kompensation – in unserem Beispiel eine Rückbuchung des Betrags auf das Ausgangskonto bei gesperrten Zielkonto – erfolgt.

Hinter diesem Pattern, das auch als SAGA-Pattern [2] bekannt ist, verbirgt sich folgende Idee: Es gilt, fachliche Transaktionen oder Invarianten, die sich über unterschiedliche Services erstrecken, auf mehrere, technisch lokale Transaktionen mit fester Ablaufreihenfolge zu verteilen.

Jede gelungene lokale Transaktion triggert den nächsten Schritt der Aufrufkette und somit die nächste lokale Transaktion an. Jede misslungene lokale Transaktion hingegen muss dafür sorgen, dass alle anderen bisher abgelaufenen lokalen Transaktionen der Aufrufkette kompensiert, also wieder rückgängig gemacht werden.

Nehmen wir uns noch einmal einen Use Case aus unserem Webshop mit folgender Invariante vor: Eine Bestellung kann nur dann erfolgreich aufgegeben werden, wenn die Summe aller offenen Bestellungen des Kunden kleiner oder gleich seines Kreditvolumens ist. Während die Bestellung selbst innerhalb des Order Service abgehandelt wird, findet die Prüfung und Aktualisierung des Kreditrahmens innerhalb des Customer Services satt. In einem ersten Schritt legt der Order-Service eine Bestellung mittels lokaler Transaktion innerhalb seiner Datenbank an, gibt diese aber noch nicht frei. Der Status der Bestellung steht entsprechend auf „pending“. Im Anschluss signalisiert der Order-Service, z. B. durch ein Domänen-Event, den erfolgreichen Abschluss dieses Schritts. Das Domänen-Event wiederum ist das Signal für den nächsten Service in der Aufrufkette – nämlich den Customer-Service, der seinen Teil der verteilten fachlichen Transaktion ausführen soll. Die Verfügbarkeit der notwendigen Bestellsumme wird geprüft und durch eine lokale Transaktion innerhalb des Customer-Service reserviert. Auch dieser Schritt wird wieder mit einer Erfolgsmeldung an die Außenwelt, also die anderen Services beendet. Dadurch weiß der Order-Service, dass er im Rahmen einer weiteren lokalen Transaktion den Status der Bestellung von „pending“ auf „approved“ setzen kann (Abb. 1).

Abbildung 1: Saga-Pattern in Aktion

 

So weit, so gut. Was aber, wenn innerhalb des beschriebenen Ablaufs nicht alles so läuft wie geplant? Was wäre, wenn zum Beispiel der Verfügungsrahmen des Kunden nicht ausreichend ist oder der Bestellstatus, aus welchen Gründen auch immer, nicht auf „approved“ gesetzt werden kann? In diesem Fall müsste die Aufrufkette Schritt für Schritt wieder zurückgegangen werden und für jede bis dato stattgefundene lokale Transaktion eine lokale Kompensation stattfinden (Abb. 2).

Abbildung 2: Transaktion und Kompensation

Was in der Theorie recht einfach klingt, kann in der Praxis beliebig komplex werden. Was passiert zum Beispiel, wenn in einem der Schritte eine E-Mail versandt wurde? Diese lässt sich nicht einfach via lokalem Rollback rückgängig machen. Stattdessen müsste eine zweite E-Mail versandt werden, die dem Adressaten verdeutlicht, dass der Inhalt der ersten E-Mail nicht mehr valide ist. In Shopsystemen wird zum Beispiel aus Gründen der User Experience (schnelle Reaktionszeit) häufig eine Bestellung bestätigt, ohne 100% zusichern zu können, dass diese am Ende auch wirklich ausgeliefert werden kann. Das ergibt durchaus Sinn, da durch Retouren und ausstehende Lieferungen weniger der tatsächliche Lagerbestand von Interesse für die Verfügbarkeit ist, als vielmehr der wahrscheinliche Lagerbestand zum Zeitpunkt des geplanten Versands. Eine angemessene Überbuchung ist somit fachlich sinnvoll.

In der Regel erhält der Shopbesucher neben der reinen Bildschirmdarstellung der Bestellbestätigung auch eine entsprechende E-Mail. Sollte nun einer der wenigen Fälle eintreten, in denen die Ware am Ende tatsächlich nicht geliefert werden kann, wird eine zweite E-Mail hinterhergeschickt. Die beinhaltet dann im günstigsten Fall neben der negativen Meldung der Nichtverfügbarkeit des Artikels gleich alternative Produktangebote evtl. sogar mit Sonderrabatten versehen, um so das negative Kauferlebnis am Ende doch noch positiv zu gestalten.

Der Microservices Track auf der JAX 2019

 

Wer soll da noch durchsteigen?

Bereits die kleinen oben aufgezeigten Beispiele lassen vermuten, dass die Verteilung einer fachlichen Transaktion auf mehrere Services und somit auf mehrere lokale Transaktionen keine triviale Herausforderung darstellt. Das gilt insbesondere dann, wenn man neben den Transaktionen selbst und ihrem Zusammenspiel auch die für sie jeweils notwendigen Kompensationen in Betracht zieht. Ganz zu schweigen von möglichen Kompensationen der Kompensationen, also Fallback-Szenarien für den Fall, dass nicht nur die Transaktion, sondern auch deren Kompensation fehlschlägt. Für die Steuerung des Ablaufs innerhalb des SAGA-Patterns kommen prinzipiell zwei Varianten infrage: Choreografie und Orchestrierung.

Bei der Choreografie sendet ein Service nach erfolgreicher Abarbeitung seiner lokalen Transaktionen eine Erfolgsmeldung via Domänen-Event. Interessierte Services, also die jeweils nächsten in der Ablaufsteuerung, registrieren sich für dieses Event und werden so zur Laufzeit durch das Auftreten des Events aktiviert. Der Gesamtablauf ergibt sich implizit durch den Fluss der Events und die Reaktion der Services auf ebendiese. Der Vorteil dieses Vorgehens liegt klar auf der Hand. Zum einen ist die Ablaufsteuerung relativ einfach zu implementieren, da die Services lediglich in der Lage sein müssen, Events zu erzeugen bzw. auf diese zu reagieren. Durch die Verwendung von Events ist zusätzlich eine lose Kopplung der Services untereinander garantiert. Die Services rufen sich niemals direkt auf und müssen sich gegenseitig nicht kennen. Nachteile ergeben sich insbesondere dann, wenn die fachliche Transaktion komplexer wird. Es findet sich nirgends im Code explizit der gewünschte Ablauf. Und auch die Komplexität des Domänenmodells erhöht sich, da für die Kommunikation zwischen den Services Domänen-Events benötigt werden. Dadurch, dass diese Events und deren Bedeutung sowohl vom Sender als auch dem Empfänger gekannt werden müssen, ergeben sich zyklische Abhängigkeiten der Services untereinander. Abbildung 3 zeigt die Choreografie eines Bestellprozesses, bei dem im Rahmen der fachlichen Transaktion zunächst die Kundendaten verifiziert werden (Customer Services). Im Anschluss erfolgt die Reservierung des zu bestellenden Produkts (Inventory Service) sowie die abschließende Zahlung (Accounting Service).

 

Abbildung 3: Implizite Ablaufsteuerung via Choreografie

 

Bei der Orchestrierung übernimmt eine zentrale Instanz die Steuerung des Ablaufs. Derjenige Service, der die Transaktion anstößt, erzeugt eine Instanz eines SAGA-Koordinators. Dieser Koordinator orchestriert den Ablauf der fachlichen Transaktion, d. h., er kümmert sich um den Aufruf der involvierten Services bzw. deren Logik sowie ggf. um notwendige Kompensationen. Der Aufruf der involvierten Services erfolgt dabei nicht über Domänen-Events, sondern über Commands. In unserem Beispiel würde der SAGA-Koordinator, der innerhalb des Order-Service beheimatet wäre, also nicht ein Event „Order mit dem Status pending wurde angelegt“ in den Raum werfen. Stattdessen würde er gezielt das Command „verify customer“ absetzen, das zur Validierung des Kunden innerhalb des Kundenservices führt (Abb. 4).

Abbildung 4: Orchestrierung

Das klingt zunächst einmal sehr ähnlich, ist es aber nicht. Da der angesprochene Service über sein Standard-API aufgefordert wird, einen seiner Dienste auszuführen, benötigt er keinerlei Wissen über den Aufrufer bzw. dessen Domäne. Bei Domänen-Events dagegen muss der Empfänger wissen, was das Event eines anderen Service fachlich bedeutet und wie er darauf regieren muss. Durch die Orchestrierung lösen wir also die zyklische Abhängigkeit auf und verlagern das Wissen über den Prozess an eine zentrale Stelle, nämlich in den SAGA-Koordinator.

 

Fazit

Soll eine Microservices-basierte Architektur zum Erfolg führen, setzt dies eine strikte Trennung der Ressourcen voraus. Das gilt auch für die Datenbank, was automatisch zu Problemen führt, wenn sich Use Cases, die zu einer verteilten Änderung von Daten führen, über mehr als einen Service erstrecken. Eine Möglichkeit, mit diesem Problem umzugehen, besteht im Hinterfragen der fachlichen Notwendigkeit der Transaktion. In der Praxis zeigt sich häufig, dass eine Transaktion nicht wirklich notwendig ist, sondern lediglich die letztendliche Sicherstellung der Konsistenz der Daten (eventual consistency).

In den Fällen, in denen tatsächlich eine Transaktion benötigt wird, lässt sich dies mit Hilfe des SAGA-Patterns realisieren, bei dem die verteilte, fachliche Transaktion in mehrere, technisch lokale Transaktionen aufgeteilt wird – inklusive Kompensation für den Fehlerfall. Die Koordination des verteilten Ablaufs der fachlichen Transaktion kann dabei entweder implizit via Choreografie, unter Verwendung von Domain Events, erfolgen oder aber explizit via Orchestrierung unter Zuhilfenahme eines SAGA-Koordinators und passender Commands. Sowohl die Choreografie als auch die Orchestrierung haben ihre Vor- und Nachteile, sodass es gilt, von Fall zu Fall bewusst abzuwägen.

Bei beiden Ansätzen geht es im Grunde genommen darum, die aktuelle fachliche Transaktion mit Hilfe diverser Status und deren Übergänge in den Griff zu bekommen. Das legt natürlich nahe, das Rad nicht neu zu erfinden, sondern für die Umsetzung auf entsprechend leichtgewichtige State- oder Workflow-Engines zu setzen. Aber das ist ein Thema für eine andere Kolumne. In diesem Sinne: Stay tuned and engage!

 

 

Cheat-Sheet: Die neuen JEPs im JDK 12


Unser Cheat-Sheet definiert für Sie, wie die neuen Features in Java 12 funktionieren. Von JEP 189 „Shenandoah“ bis JEP 346 „Promptly Return Unused Committed Memory from G1“ fassen wir für Sie zusammen, was sich genau ändern wird!

Cheat-Sheet sichern!

Links & Literatur

[1] https://www.enterpriseintegrationpatterns.com/docs/IEEE_Software_Design_2PC.pdf
[2] https://microservices.io/patterns/data/saga.html

The post Lost in Transaction: Businesstransaktionen mit Microservices umsetzen appeared first on JAX.

]]>
Spring Boot als Beispiel für DDD Strategic Design https://jax.de/blog/software-architecture-design/spring-boot-ddd-strategic-design/ Tue, 18 Jul 2017 15:41:21 +0000 https://jax.de/?p=48946 Einer der wertvollsten Bereiche von Domain-driven Design ist zweifelsohne das Strategic Design mit seinen Context-Mapping-Patterns. Finden Sie auch, dass die meisten Beschreibungen der Patterns etwas abstrakt und schwer verdaulich sind?
Michael Plöd hilft weiter!

The post Spring Boot als Beispiel für DDD Strategic Design appeared first on JAX.

]]>
Spring Boot als Beispiel für DDD Strategic Design

Im Rahmen dieses Vortrags von der JAX 2017 stellt Michael Plöd die Patterns auf Basis einer einfachen Spring-Boot-basierten Anwendungslandschaft vor. Hierbei geht er unter anderem auf folgende Patterns ein: Customer/Supplier, Open Host Language, Anti-Corruption Layer, Conformist oder Separate Ways.

Spring Boot als Beispiel für DDD Strategic Design from JAX TV on Vimeo.

 

The post Spring Boot als Beispiel für DDD Strategic Design appeared first on JAX.

]]>