Java Magazin   1.2020 - GraalVM

Preis: 9,80 €

Erhältlich ab:  Dezember 2019

Umfang:  100

Autoren / Autorinnen: 
Dominik Mohilo ,  
Tam Hanna ,  
Wolfgang Weigend ,  
Lars Hupel ,  
Chris Thalinger ,  
Stephan Kaps ,  
Golo Roden ,  
Elena BochkorDr. Veikko Krypczyk ,  
Konstantin Diener ,  
Christian Schulz ,  
Michael Simons ,  
Sascha MölleringDennis Kieselhorst ,  
Manuel Mauky ,  
Albrecht Weinert ,  
Tam Hanna ,  
Markus Winand ,  

Abenteuer, oder Âventiure, wie es im Mittelhochdeutschen noch hieß, sind die Grundlage für viele Geschichten und Sagen: Von höfischen Dichtungen, die von Artus und den Rittern der Tafelrunde erzählen, über die Helden aus Jules Vernes Romanen bis hin zu modernen Abenteurern auf der Leinwand und in Videospielen wie Lara Croft oder Indiana Jones. Ikonisch dabei sind Gefahren und Schätze, die es zu überstehen bzw. zu bergen gilt. Der Grundtenor ist also die Spannung.

Betrachtet man das Java-Ökosystem, so sind Begriffe wie „Abenteuer“ und „Spannung“ vermutlich nicht das Erste, das einem in den Sinn kommt, und wirken fast etwas deplatziert. Oracle hatte nach der Übernahme von Sun gerade das „Unspannende“, das Beständige von Java auf den Fahnen stehen. Schließlich ging es – und geht es – auch ums Geschäft. Und auch der Wahlspruch „Write once, run everywhere“ zeugt nicht gerade von Abenteuerlust, wenn man ehrlich ist.

Das bedeutet allerdings nicht, dass es keine spannenden Entwicklungen im Java-Universum geben kann, auch der Duke bzw. seine Jünger können von Zeit zu Zeit ein wenig Durst nach Aufregung verspüren. Der Ort, solche Anwandlungen auszuleben, sind heutzutage die Oracle Labs, deren Ziel die Erforschung und Förderung neuer Technologien ist. Dort ist auch vor einigen Jahren die Idee gereift, die etwas staubige – wenn auch höchst zuverlässige und exzessiv optimierte – Java Virtual Machine (JVM) neu zu erfinden. Mit dem ersten produktionsreifen Release der GraalVM könnte dieser große Schritt nun gelungen sein.

Die neue Virtual Machine wurde selbstverständlich mit der Performance im Kopf entwickelt und soll in erster Linie schneller starten, sich besser integrieren lassen sowie insgesamt performanter sein. Aktuelle Tests zeigen, dass sie diesem Anspruch durchaus standhält. Grund genug, uns diese technologische Weiterentwicklung einmal genauer anzusehen. Eine theoretische Einführung zum Thema gibt Wolfgang Weigend in seinem Artikel „GraalVM startet durch“, der sich insbesondere mit den grundlegenden Eigenschaften der GraalVM und deren Performance sowie möglichen Einsatzbereichen beschäftigt. Eine gute Grundlage für die wagnisreiche Jagd nach der Performance.

Dass auf der JVM nicht nur Java-Anwendungen ihr Zuhause haben, sondern auch Sprachen wie Scala, Kotlin, Clojure oder Groovy laufen, machte die virtuelle Maschine bereits recht polyglott. Doch auch ganz fremde Vertreter wie etwa JavaScript wurden durch Projekte wie Nashorn auf der JVM heimisch. Die GraalVM hingegen wird mit dem Truffle API ausgeliefert, das theoretisch jede Programmiersprache zur „JVM-Sprache“ werden lässt. Lars Hupel nimmt Java-Entwickler in seinem Artikel „Das eierlegende Truffle-Schwein“ mit auf eine Abenteuerreise über die Grenzen des Java-Ökosystems hinaus.

Nun heißt es also: Raus aus dem miefigen Alltagstrott und hinaus in die Wildnis. Ich verspreche Ihnen, es lohnt sich.

mohilo_dominik_sw.tif_fmt1.jpgDominik Mohilo | Redakteur

Fällt das Wort Digitalisierung, denken viele an Projekte im Umfeld des Internet of Things oder an spektakuläre Blockchains. Immer häufiger wird Digitalisierung in einem Atemzug mit künstlicher Intelligenz genannt, oder es werden neue Megaprojekte wie eine europäische Cloud ausgerufen. Dass es bei Digitalisierung jedoch zu großen Teilen darum geht, Prozesse digital zu unterstützen, die aktuell entweder mit manuellem Aufwand wie beispielsweise persönlichem Erscheinen verbunden sind oder lediglich schriftlich erledigt werden können, wird stark vernachlässigt. Nicht so in diesem Artikel.

Es gibt noch hunderte von Verfahren, vor allem im Behördenumfeld, die für den Bürger einfacher werden, wenn sie digitalisiert sind. Ein solches Antragsverfahren sehen wir uns näher an. Mit Hilfe eines Überblicks über die Architektur, die Infrastruktur und die organisatorischen Aspekte soll verdeutlicht werden, dass moderne Technologien und Methoden für jede Art von Organisation Mehrwerte bringen und Aussagen wie „Wir sind doch nicht Netflix“ keine tragfähigen Argumente sind.

Um was es geht – die Domäne

Das hier vorgestellte behördliche Antragsverfahren wird von mehr als 20 000 Bürgern pro Jahr genutzt. Die Fachlichkeit ist relativ komplex und würde daher den Rahmen dieses Artikels sprengen – sie ist im Detail aber auch nicht entscheidend. Zur Veranschaulichung wird in Abbildung 1 ein mit Hilfe von Domain Storytelling (Kasten: „Domain Storytelling“) modellierter Prozess dargestellt. In diesem Fall handelt es sich um eine stark vereinfachte Abbildung des gesamten Antragsverfahrens.

kaps_architekturen_1.tif_fmt1.jpgAbb. 1: Domain Story des Onlineantrags

In diesem Modell ist zu sehen, dass ein Antragsteller seine Antragsdaten in einem Onlineformular erfasst, woraufhin diese Daten nach dem Abschicken in eine Fachanwendung übertragen werden. Die Fach anwendung wiederum erzeugt in einem Dokumentenmanagementsystem (DMS) eine Akte und fordert, wenn notwendig, automatisiert weitere Informationen beim Antragsteller nach. Der Antragsteller übermittelt diese Informationen in Form von Anlagen per Upload über eine Art Onlineportal. Diese Dokumente werden in das DMS importiert und der entsprechenden Akte hinzugefügt. Die Fachanwendung verteilt den Fall an einen Sachbearbeiter und erzeugt im besten Fall bereits einen Bescheidentwurf. Details zu Berechnungen, Sonderfällen sowie den verschiedenen Arten von möglichen Bescheiden werden nicht näher betrachtet.

Der soeben erzeugte Bescheidentwurf wird vom ermittelten Sachbearbeiter geprüft und bestätigt, wodurch er dem jeweiligen Vorgesetzten zur Mitzeichnung (Freigabe) vorgelegt wird. Ist diese erfolgt, löst die Fachanwendung im positiven Fall eine Auszahlung über das Zahlungssystem aus und der Fall ist beendet.

Folgende Ziele werden mit dem hier betrachteten Projekt verfolgt:

  • Bereitstellung eines einfachen (Online-)Antragsverfahrens für den Bürger, mit der Möglichkeit, Anlagen hochzuladen und sich über den aktuellen Bearbeitungsstatus online zu informieren

  • Schnelle Durchlaufzeiten; Standardfälle werden nach der Erfassung vom System berechnet und beschieden oder es wird, im Fall einer Unvollständigkeit, automatisch ein Nachforderungsschreiben versandt und auf den oder die Eingänge gewartet

  • Entlastung der Sachbearbeiter durch den Wegfall manueller Tätigkeiten, beispielsweise der Erstellung von Eingangsbestätigungen, Nachforderungsschreiben, Erinnerungsschreiben, Bescheiden u. v. m.

  • Automatisierte (einheitliche) Erstellung von Schreiben mit Hilfe von konfigurierbaren Textbausteinen und Ablage sämtlicher Dokumente in der E-Akte

  • Automatisierte Meldung der ausgezahlten Leistungen an die zuständigen Finanzämter

  • Automatisierte Überweisungen an Dritte und automatisierte Überwachung der Überweisungen auf Ausgleich

  • Automatisierte Berechnung und Berücksichtigung von Widerspruchs- und Klagefristen sowie die prozessuale Umsetzung der Verfahren

Neben den gerade aufgezählten (eher fachlichen) Zielen werden des Weiteren noch die folgenden (eher technischen) Ziele verfolgt:

  • Integration der Drittsysteme mit loser Kopplung und standardisierter Kommunikation über definierte Schnittstellen

  • Minimierung des Customizings des E-Akten-Systems, um zukünftige Versionsupdates ohne Aufwand und Risiko durchführen zu können

  • Teilautomatisiertes Fachverfahren mit BPMN-gesteuerten Prozessen und somit einheitlicher und effizienter Bearbeitung der Anträge

  • Einführung regelbasierter Entscheidungstabellen, um Ablehnungsgründe, Sonderfälle und Inkonsistenzen schnell und zweifelsfrei identifizieren zu können

  • Automatisierte Übertragung und Import der Onlineanträge in die Fachanwendung sowie automatisierte Erstellung von Akten und Vorgängen im E-Akten-System

Durch die BPMN-gestützte Prozesssteuerung wird der Sachbearbeiter durch die Bearbeitung von Anträgen geführt. Durch die Zuweisung konkreter Aufgaben (Tasks) im Rahmen des Prozessverlaufs ist zu jeder Zeit klar, welcher Schritt mit welchen Tätigkeiten erfolgen muss. Schauen wir uns im Folgenden die daraus entstandene Architektur an.

Die Architektur

Abbildung 2 zeigt die zum Zeitpunkt der Erstellung dieses Artikels aktuelle Architektur des Gesamtsystems. Um dem Bürger eine Onlineantragsmöglichkeit bereitzustellen, läuft in der DMZ (demilitarisierten Zone) eine Webanwendung, die XML-Dateien erzeugt, die von der internen Fachanwendung in regelmäßigen Abständen importiert werden. Des Weiteren hat der Antragsteller die Möglichkeit, den aktuellen Bearbeitungsstatus online abzufragen. Das ist eine eigene Webanwendung.

Die zentrale Rolle spielt die Fachanwendung. Dabei handelt es sich ebenso um eine Java-Webanwendung, die wie alle anderen Webanwendungen als klassisches WAR-Archiv auf einem WildFly Application Server betrieben wird. Integriert in diese Fachanwendung ist die leichtgewichtige Workflow-Engine Camunda (embedded). Des Weiteren sind die Drittsysteme Dokumentenmanagementsystem und Zahlungssystem zu sehen, bei denen es sich um proprietäre Produkte handelt, die bereits existieren und über SOAP-Schnittstellen verfügen.

Um die Anbindung der Drittsysteme einfach und konsistent zu anderen Schnittstellen umzusetzen, wurden REST Services als Anti-Corruption Layer für die SOAP-Schnittstellen implementiert (DMS REST Service und ZS REST Service). Neben diesen REST Services existieren weitere (PLZ-, BLZ-, Textbaustein-, Report- und Status-Exporter-Service), die alle als Spring Boot Microservices in Docker-Containern betrieben werden.

Der PLZ-Service ist für die Validierung von Postleitzahl-Ort-Kombinationen zuständig und verwaltet alle möglichen Kombinationen in einer eigenen Datenbank. Die Daten stammen von einem Produkt der Post und werden quartalsweise aktualisiert. Der Textbaustein-Service liefert die für die Bescheiderstellung oder Nachforderungsschreiben notwendigen Textbausteine. Diese verwaltet er ebenso in einer eigenen Datenbank. Die Pflege der Daten erfolgt über die Fachanwendung. Beim Status-Exporter-Service handelt es sich quasi um einen Scheduler, der in regelmäßigen Abständen den Bearbeitungsstatus sämtlicher Fälle der letzten Monate aus der Fachanwendungsdatenbank liest und eine Datei erzeugt, die in der DMZ bereitgestellt wird. Diese wiederum kann von der App zur Onlinestatusabfrage ausgelesen werden. Der Report-Service kümmert sich um den Sonderfall der Erzeugung von Bescheiden im Word-Format, die zwar Daten aus dem Fall enthalten, aber um individuelle Texte durch einen Sachbearbeiter ergänzt werden müssen, z. B. im Widerspruchsverfahren. Der BLZ-Service kann mit Hilfe einer durch die Bundesbank bereitgestellten Datei zu einer IBAN die entsprechende Bankleitzahl feststellen.

Beim FinanzdatenService und der ElsterLohnApp handelt es sich technisch gesehen ebenfalls um Spring Boot Services. Ersterer kümmert sich fachlich um die Datenextraktion aus der Datenbank der Fachanwendung sowie die notwendige Transformation in das vorgegebene XML-Format, während die ElsterLohnApp den Transferprozess dieser XML-Daten an die Finanzämter steuert.

Bei den Komponenten, die am linken und rechten Rand vertikal aufgeführt sind, handelt es sich um Infrastrukturkomponenten, die im zweiten Teil dieser Serie genauer betrachtet werden.

kaps_architekturen_2.tif_fmt1.jpgAbb. 2: Architektur des Gesamtsystems

Damit die Fachanwendung nicht die Endpunkte der Services kennen muss, wird eine Service Registry, hier HashiCorp Consul, verwendet. In dieser Registry sind alle Services registriert, meist mit mehreren Instanzen mit sich eventuell dynamisch ändernden Lokationen. Die Fachanwendung muss nur einen lokalen Agenten nach dem Namen eines Service fragen, und die Registry liefert einen Endpunkt zurück, der aktuell auch Anfragen entgegennehmen kann. Das wird mit Hilfe automatischer Health Checks sichergestellt.

Warum Microservices?

Das hier beschriebene System wird von einem lediglich zehnköpfigen Entwicklerteam umgesetzt, was bedeutet, dass organisatorische Gründe wie die Reduzierung von Kommunikationsbeziehungen und Abstimmungsaufwand bei der Entscheidung, Microservices zu nutzen, nicht entscheidend waren. Microservices bieten jedoch generell eine elegante Art der Modularisierung. Es ist möglich, die für die eigentliche Domäne primär nicht relevanten fachlichen Teile auszulagern, was Vorteile wie lose Kopplung, bessere Wart- und Testbarkeit sowie unabhängige Deployments ermöglicht.

Bei den in Abbildung 2 zu sehenden Microservices PLZ-Service, BLZ-Service sowie Textbaustein-Service handelt es sich um solche Komponenten, die im Domain-driven Design (DDD) als Generic Subdomains bezeichnet werden. Diese Services repräsentieren bereits gelöste Probleme, die für diverse Systeme oder sogar unterschiedliche Unternehmen immer gleich umgesetzt werden. Sie gehören in DDD nicht zum Kerngeschäft (den sogenannten Core Subdomains) und können im einfachsten Fall hinzugekauft werden – oder es existieren entsprechende Open-Source-Projekte.

In der Fachanwendung selbst existieren aktuell mehrere unterstützende Funktionalitäten, die zwar mit der Businesslogik verknüpft sind, jedoch leicht in separate Microservices ausgelagert werden könnten, um die Domäne von nicht zum Kern gehörendem Code zu befreien. Bei DDD spricht man dabei von Supporting Subdomains. Beispiele dafür sind Referenz- oder Stammdatenverwaltungen, Protokollierungs- und Kommentarfunktionen.

Die Services DMS-Rest-Service und ZS-Rest-Service dienen der Abstraktion der SOAP-Schnittstellen, was Information Hiding fördert und die Benutzbarkeit der Schnittstellen verbessert, da sie auf die fachlichen Bedürfnisse zugeschnitten sind. Diese Designtechnik wird auch als Anti-Corruption Layer bezeichnet [2]. Es handelt sich dabei um eine architekturelle Schicht, die das Domänenmodell von anderen Modellen trennt und die es ermöglicht, dass auf das fremde Modell so zugegriffen werden kann, wie es das eigene Domänenmodell erfordert.

Microservices bieten generell die Möglichkeit, unabhängige Technologiestacks zu verwenden. Bezüglich des Report-Service war es ursprünglich nicht vorgesehen, einen eigenen Microservice zu entwickeln. Die transitiven Abhängigkeiten der dafür eingesetzten Bibliothek waren jedoch mit denen der Fachanwendung nicht vereinbar. Durch die Auslagerung in einen separaten Service, der wiederum seine eigenen Abhängigkeiten mitbringen kann, konnte dieses technische Problem elegant gelöst werden.

Ist das nicht SOA?

Die soeben vorgestellten Services können in drei Kategorien unterteilt werden. In die erste Kategorie gehören der Postleitzahlen- und der Bankleitzahlen-Service. Diese Services kommen ohne jeglichen fachlichen Kontext aus und können daher von mehreren Consumern ohne Anpassungsbedarf wiederverwendet werden.

Zur zweiten Kategorie zählen der Status-Exporter-Service, der Report-Service und der Textbaustein-Service. Diese Services haben einen starken fachlichen Bezug zu der konkreten Fachanwendung. Es wäre vorstellbar, den Textbaustein-Service als zentralen Service anzubieten und in anderen Fachanwendungen wiederzuverwenden. Das würde jedoch dazu führen, dass entweder zu viel Aufwand in eine Generalisierung gesteckt wird oder fachlicher Kontext sich im Code oder sogar im API widerspiegelt. Ein Beispiel: In der in diesem Artikel betrachteten Domäne werden Textbausteine für diverse Kategorien gepflegt und verwendet: für Nachforderungsgründe, Ablehnungsgründe usw. Textbausteine einer Kategorie können direkt über das API abgefragt werden. Sollte ein weiteres System Textbausteine benötigen, jedoch keine Kategorien, wäre es gezwungen, eine fiktive Dummykategorie zu verwalten, um diesen Service so nutzen zu können. Das mag für dieses Beispiel akzeptabel klingen, aber ich denke, das Prinzip wird klar. Im Allgemeinen bedeutet es: Bei fachlichem Kontext werden separate Services entwickelt.

Der DMS-Service und der Zahlungssystem-Service bilden Kategorie 3. Es handelt sich um fachlich sehr stabile Services, die für mehrere Fachanwendungen einsetzbar sind. Daher wird ein Zwischenweg gewählt, indem die Services je Kontext mit unterschiedlichen Konfigurationen (Spring Boot Profiles) separat deployt werden. Sollte der Bedarf entstehen, für eine konkrete Fachanwendung spezielle Änderungen oder Funktionen in diesen Services bereitzustellen, kann noch auf die Variante eines Forks (wie Kategorie 2) umgestellt werden.

Das ist ein (Frontend-)Monolith, oder?

Bei der Fachanwendung handelt es sich immer noch eher um einen Monolithen, trotz einiger Microservices. Das ist generell auch kein Problem, vor allem unter Berücksichtigung der Teamgröße.

Ob in Zukunft die Fachanwendung in mehrere Bounded Contexts zerschnitten wird und somit weitere Microservices für die Core Subdomains entstehen, ist fraglich. Sowohl technisch als auch fachlich besteht aktuell kein Bedarf. Es gibt nur einen fachlich Verantwortlichen für das Verfahren, und somit existieren keine fachlichen Schnittstellen oder getrennte Kontexte. Wären mehrere Abteilungen am Prozess beteiligt und hätten interne Abläufe, für die sie selbst verantwortlich sind, würde alles dafürsprechen, sowohl die Fachanwendung als auch die Prozesse in unabhängige Einheiten zu separieren. Dadurch wären lokale Anpassungen leichter umsetzbar, ohne notwendige Abstimmung, wann welche Funktionalität in welcher Version releast werden kann.

Ein natürlicher Schritt in naher Zukunft wird sein, das UI des Textbaustein-Service (also das Pflegen von Textbausteinen) in den Service selbst zu überführen.

Eine einfache Möglichkeit der Integration in das Frontend der Fachanwendung wäre über Links möglich. Auch die Integration über iframes ist nicht auszuschließen, da diese das Aussehen und die Funktionalität sauber kapseln würden. Alternativen wären Transklusion, SSI (Server-side Includes) oder ESI (Edge-side Includes). Durch die Verlagerung des UI in den Service würde der Service zu einem Self-contained System (Kasten: „Self-contained Systems“).

Wieso Workflow-Engine?

In diesem System verwenden wir wie in Abbildung 2 gezeigt eine Workflow-Engine. Generell wird durch den Einsatz von Geschäftsprozessmodellierung die Zusammenarbeit von Fachbereich und IT deutlich verbessert. Sie ermöglicht es, Abläufe in einfacher Notation darzustellen und ein gemeinsames Verständnis sowie eine gemeinsame Sprache zu entwickeln. Durch den Einsatz einer Workflow-Engine wird es möglich, den zwischen Fachbereich und IT abgestimmten Prozess in einen technischen Workflow zu überführen – im besten Fall ohne Anpassung der modellierten Elemente. Diese Prozesstransparenz verbessert das Spezifizieren von Anforderungen.

Eine Workflow-Engine kann als State Machine bezeichnet werden, was bedeutet, dass zu jedem Zeitpunkt ersichtlich ist, in welchem Status sich Prozessinstanzen befinden. Durch die Möglichkeit, Regeln und Entscheidungen leicht zu modellieren und zu automatisieren, werden die Verständlichkeit und vor allem die Durchlaufzeit von Vorgängen verbessert.

Existieren Prozesse, in denen es zu langlaufenden Aktionen kommen kann oder in denen menschliche Interaktion notwendig ist, sind das weitere Indikatoren für den Einsatz einer Workflow-Engine.

Ein weiterer Einsatzbereich ist die Microservices-Orchestration, was die Steuerung der Aufrufe an die diversen Microservices mit Hilfe des Prozesses bedeutet. Da es sich um verteilte Systeme handelt, bedingt das jedoch, sich mit Idempotenz und Kompensation [4] sowie Fehlerbehandlung und Resilienz innerhalb des modellierten Prozesses zu beschäftigen.

In dem hier beschriebenen Projekt koordiniert der Workflow, welche Aktion als Nächstes durchzuführen ist, und delegiert die Ausführung an die Fachanwendung, die wiederum weiß, welche Services aufzurufen sind und was zu tun ist, wenn einzelne Schritte fehlschlagen.

In Listing 1 ist ein kleines Beispiel zu sehen. Die Klasse implementiert das von Camunda bereitgestellte Delegate, ein Java-API, das erfordert, dass eine Methode execute überschrieben wird. An die Methode wird eine DelegateExecution übergeben, die es ermöglicht, an den Prozess und dessen Ausführung und somit an sämtliche Prozessvariablen zu gelangen. Die ausgelesene Fall-ID der Prozessinstanz wird im Anschluss an eine Servicemethode zum Anlegen oder Aktualisieren der zugehörigen Akte übergeben. Diese wiederum ruft den DMS REST Microservice auf und verwendet dabei Resilienzpattern wie Circuit Breaker und Fallbacks. Über ein execution.setVariable() wird das im Service generierte Geschäftszeichen an eine Prozessvariable übergeben. Der von der Klasse erweiterte CamundaEndpoint stellt ein selbst implementiertes Verhalten für Fehlerzustände bereit, das von allen Delegates im catch-Bereich einfach per createIncident() verwendet werden kann. Dabei handelt es sich zum Beispiel um speziell aufbereitete Logmeldungen oder eine Benachrichtigung per E-Mail. Die Prozessinstanz selbst erzeugt in diesem Beispiel im Fehlerfall einen Incident und muss manuell behandelt werden, da dann meist von einer technischen Störung auszugehen ist, die sich auch durch bereits drei standardmäßig durchgeführte Retries nicht selbst heilen konnte.

Listing 1

@Named
public class DmsFileCreateOrUpdateDelegate extends CamundaEndpoint implements JavaDelegate {
 
  ...
 
  @Override
  @Transactional(Transactional.TxType.REQUIRES_NEW)
  @Interceptors(CamundaLoggerIntercept.class)
  public void execute(DelegateExecution execution) throws Exception {
    try {
      Long fallId = (Long) execution.getVariable(Vars.ATTRIB_FALL_ID);
      Akte akte = antragService.createOrUpdateAkte(fallId);
 
      execution.setVariable(ATTRIB_AKTENZEICHEN, akte.getGeschaeftszeichen());
    } catch (Exception e) {
      createIncident(e, execution);
    }
  }
}

Weshalb Camunda?

Bei der Frage einer Prozesssteuerungssoftware fiel die Wahl sehr schnell auf Camunda. Zum einen handelt es sich dabei um ein Open-Source-Produkt, das bereits weit verbreitet und gut dokumentiert ist, zum anderen bietet es neben der Möglichkeit, REST APIs zu verwenden, auch ein leichtgewichtiges Java API. Nachdem die Dependencies in der pom.xml eingetragen sind, ist es sehr einfach möglich, laufende Prozesse anzufragen, Aufgabenlisten abzurufen oder entsprechende Schritte abzuschließen. Die Nutzung des Java API ist äußerst entwicklerfreundlich, und es ist einfacher, Unit-Tests zu implementieren und Transaktionen zu steuern sowie technische Tasks aus dem fachlichen Prozess weitestgehend herauszuhalten.

Neben der BPMN-Notation unterstützt Camunda auch Decision Model and Notation (DMN) für Entscheidungstabellen und Case Management Model and Notation (CMMN) für spezielle Fallbearbeitung.

Camunda wird bereits in der Open-Source-Variante mit zusätzlichen Applikationen ausgeliefert. Dazu zählt eine Administrationsoberfläche für Berechtigungen und Deployments, eine Taskliste, in der die durch die aktuell laufenden Prozesse erzeugten Human Tasks angezeigt werden, sowie ein Cockpit wie in Abbildung 3, in dem alle laufenden Prozesse überwacht und im Fehlerfall wieder gestartet werden können.

kaps_architekturen_3.tif_fmt1.jpgAbb. 3: Camunda Cockpit

In dem in diesem Artikel beschriebenen System sind inzwischen acht Prozesse mit insgesamt 53 Tasks, begleitet von neun Decision Tables und acht Timern entstanden.

Aber als BPMN-Monolith, oder?

Ist durch die umfangreiche Auslagerung von Geschäfts- und Ablauflogik in den modellierten Geschäftsprozess ein BPMN-Monolith entstanden, der wiederum schwer zu warten und zu ändern ist? Wäre es nicht besser, zumindest die Subprozesse herauszulösen?

Dieser Annahme widerspricht wie bereits erwähnt die Tatsache, dass es für den gesamten Geschäftsprozess nur einen Owner gibt. Das heißt, eine Person ist Ende-zu-Ende verantwortlich, sodass die Notwendigkeit, hier Bounded Contexts zu ziehen, um Abstimmungsaufwände zu reduzieren, nicht gegeben ist.

Des Weiteren ist der Gesamtprozess bereits in mehrere Subprozesse aufgeteilt. Bei Bedarf wäre es denkbar, diese in eigene Spring-Boot-Applikationen mit jeweils eigener integrierter Workflow-Engine zu überführen. Die in der Fachanwendung betriebene Workflow-Engine wird nicht von weiteren Applikationen als eine Art zentrale Workflow-Engine verwendet.

Aufgrund der Integration der Workflows in die Fachanwendung müssen keine Überlegungen über separate und zu synchronisierende Deployment-Pfade stattfinden. Häufig wird eine neue Version eines Workflows mit einer neuen Version der Fachanwendung zusammenhängen.

Sehr positiv ist darüber hinaus die Möglichkeit, eine automatische Migration von laufenden Prozessen beim Deployment von einer alten auf eine neue Version durchzuführen, gegebenenfalls mit Hilfe programmierter Migrationslogik.

Fazit

Das in diesem Artikel vorgestellte Verfahren ist produktiv im Einsatz und hat alle gesteckten Ziele erreicht. Leider ist der letzte Schritt, das Onlinebereitstellen der Bescheide, noch nicht umgesetzt. Es fehlt hier an geeigneten technischen Möglichkeiten mit weiter Verbreitung, den Antragsteller eindeutig identifizieren zu können (Stichwort: Zwei-Faktor-Authentifizierung). Wer einen Vorschlag für uns hat, kann sich gerne bei mir melden.

In einem Folgeartikel gehe ich auf die Infrastruktur dieses modernen On-Premises-Verfahrens ein und stelle euch die organisatorischen Maßnahmen vor, die wir gewählt haben, um Agilität in einem nicht agilen Umfeld leben zu können.

kaps_stephan_sw.tif_fmt1.jpgStephan Kaps leitet die Softwareentwicklung im Bundesversicherungsamt und ist Gründer der Java User Group Bonn. Als Softwarearchitekt und Entwickler hat er seit 2002 mit Java zu tun.

Web

Weder Domain-driven Design (DDD) noch Microservices sind konzeptionell neu. Trotzdem erfahren beide Themen seit einigen Jahren zunehmend mehr Zuspruch und werden immer häufiger auch gemeinsam genannt. Woran liegt das?

Domain-driven Design (DDD) ist eine Entwicklungsmethode, deren Ziel eine gemeinsame fachliche Sprache innerhalb eines interdisziplinären Teams ist. Gelingt das, können sich alle Beteiligten – vom Fachexperten über den Designer bis hin zum Entwickler – über die gleiche Thematik unterhalten, ohne dass es ständig zu Missverständnissen und impliziten Annahmen kommt.

Die Idee dahinter ist, dass ein Team durch das Überbrücken der ansonsten gegebenen (fach-)sprachlichen Hürden schneller und besser in der Lage ist, die gewünschte Software zu entwickeln. Diese Annahme ist durchaus berechtigt, da es keine Seltenheit ist, dass man zwar miteinander redet, aber dennoch aneinander vorbei. Verwenden jedoch alle die gleichen Begriffe und haben zudem alle auch das gleiche Verständnis dieser Begriffe, verbringt man weniger Zeit mit der Frage nach dem eigentlichen Sinn.

Das ist allerdings zugleich auch der Grund, warum DDD so vielen Entwicklern so schwerfällt: Es hat wenig bis gar nichts mit Technologie zu tun, sondern erfordert, dass man seine persönliche Komfortzone verlässt und sich mit einer bislang fremden Fachthematik auseinandersetzt. Das ist für einen Technologen viel schwieriger, als sich mit den neuen (technischen) Features des jeweils favorisierten Frameworks zu befassen.

Software ist kein Selbstzweck

Software ist aber kein Selbstzweck. Software wird nicht geschrieben, weil es so viel Spaß macht, Software zu schreiben. Letztlich wird Software geschrieben, um fachliche Probleme zu lösen und das Leben von Menschen und/oder Tieren einfacher, sicherer oder komfortabler zu machen. Software ist, auch wenn Entwickler das nicht gerne hören, in den meisten Fällen nur Mittel zum Zweck – und genau das rückt DDD in den Vordergrund.

Fängt man an, sich auf DDD einzulassen, wird man zunächst von einer Reihe wenig verständlicher Begriffe erschlagen. Dinge wie Commands oder Domain Events mögen noch greifbar erscheinen, aber Aggregates oder Bounded Contexts sind da schon deutlich abstrakter. Leider ist auch die gängige Literatur zu DDD dabei keine allzu große Hilfe, da sie sehr abstrakt und akademisch geschrieben ist. Es ist eine gewisse Ironie des Schicksals, dass ausgerechnet eine Methode, die ein besseres Verständnis anstrebt, zu oft daran scheitert, wie sie erklärt wird.

Ohne auf die Details von DDD eingehen zu wollen, kann man jedoch für alle genannten Begriffe einfache, greifbare Definitionen finden:

  • Ein Command ist der Wunsch eines Anwenders, ein bestimmtes Ziel zu erreichen. Commands drücken daher stets eine Intention aus und werden häufig im Imperativ formuliert. Daher stehen die Formulierungen von Schaltflächen und Menüpunkten in taskorientierten Benutzeroberflächen auch häufig in ebendieser Form: Open Game, Move Figure oder Give up sind typische Beispiele, in diesem Fall aus der Fachlichkeit des Schachs.

  • Ein Domain Event ist das Resultat auf ein Command: Es ist ein Fakt, der durch das System als Reaktion auf ein Command entstanden ist. Da Fakten tatsächlich geschehen sind und ohne Zeitmaschine nicht mehr rückgängig gemacht werden können, stehen sie in der Vergangenheitsform: Game opened wäre das Pendant zu einem der zuvor genannten Commands. Während Commands also quasi Requests beschreiben, stehen Domain Events für die Responses.

  • Ein Aggregate ist schließlich das Ding, das den Zustand verwaltet, auf dem Commands und Domain Events gemeinsam operieren: Im Falle einer Partie Schach ist ein laufendes Spiel zum Beispiel ein Aggregate, da die genannten Commands Informationen über das laufende Spiel und den aktuellen Spielstand benötigen, um Entscheidungen zu treffen. Der Bezug zu Objekten in der objektorientierten Programmierung liegt nahe, Aggregates müssen aber nicht zwingend als Objekte implementiert werden.

  • Ein Bounded Context schließlich ist eine Sprachgrenze: Er löst das Problem, dass Sprache zwar eindeutig sein soll, aber keine universelle Sprache gewünscht ist. Stattdessen definiert man Bereiche, innerhalb derer die Sprache eindeutig sein muss. Der gleiche Begriff darf aber durchaus in verschiedenen Bounded Contexts in unterschiedlicher Bedeutung auftreten, nur eben nicht innerhalb eines solchen.

Von DDD zu Microservices

Microservices haben hingegen so gar nichts mit DDD zu tun – sie stehen nicht einmal im Zusammenhang mit Fachlichkeit. Stattdessen sind sie höchst technische Artefakte: Ein Microservice ist ein Prozess (im Betriebssystemsinn), der für eine in sich geschlossene Thematik zuständig ist, diese kapselt, und den Zugriff darauf nur über wohldefinierte Schnittstellen von außen zulässt. Microservices sind technisch und inhaltlich autonom und autark.

Schaut man sich diese Beschreibung an, erinnert das stark an die Beschreibung von Web-Services, wie sie in den 1990er Jahren im Rahmen von SOAP und SOA (Service-oriented Architecture) geläufig war. Tatsächlich stellen Microservices letztlich genau die gleichen Ideen dar wie vor 25 Jahren – nur dieses Mal unter einem anderen Namen: auf Basis von REST, gRPC oder GraphQL als Protokoll und JSON als Datenformat. Der Kerngedanke ist aber nach wie vor der gleiche.

Das wirft die Frage auf, wie Microservices zu schneiden sind: Es liegt auf der Hand, dass nicht die gesamte Logik der Anwendung in einem einzigen Microservice enthalten sein sollte, denn ansonsten hat man wieder einen Monolithen. Wie verteilt man also die Logik der Gesamtanwendung so auf mehrere Dienste, dass sie jeweils sinnvolle Aufgaben erfüllen und losgelöst voneinander agieren können – eben autonom und autark?

Technische Schnitte, fachliche Schnitte

Eine gute Faustregel ist dabei, sich zu überlegen, welchen Teil einer Anwendung man als eigenständiges Geschäftsmodell extrahieren könnte. Verwendet die Anwendung beispielsweise Authentifizierung? Dann spräche (theoretisch) nichts dagegen, dies in einen eigenen Service auszulagern – Anbieter, die Authentication as a Service (oder auch Identity as a Service) offerieren, machen es vor.

Verwendet die Anwendung einen umfangreichen Logger? Wie wäre es mit Logging as a Service? Wie man an diesen beiden Beispielen sieht, fallen einem als Erstes technische Komponenten ein, die orthogonal zur eigentlichen Fachlichkeit der Anwendung stehen, die man eigenständig entwickeln, betreiben und potenziell auch vermarkten könnte.

Das Gleiche gilt jedoch auch für fachliche Aspekte. Auch hier lassen sich Bereiche finden, die losgelöst voneinander entwickelt, betrieben und potenziell vermarktet werden können. Als Beispiel hierfür sei eine Software zur Verwaltung von Urlaubsanträgen genannt. Der Kernbegriff an dieser Stelle ist sicherlich das Wort Urlaub, weshalb man sich die Frage stellen sollte, welche Bedeutung dieses Wort eigentlich hat.

Viele denken bei dem Wort Urlaub an Themen wie Reisen, Entspannung, Strand, Meer, Berge, Wandern, Tanzen, Ausgehen, gutes Essen, ... Kaum jemand denkt dabei an Themen wie Urlaubsvertretung oder Sonderurlaub. Das liegt daran, dass man zuerst an den eigenen Urlaub denkt, und nicht daran, welche Auswirkungen der eigene Urlaub auf die Teamkollegen hat oder wie die HR-Abteilung Urlaub verwaltet.

Was ist Urlaub?

Modelliert man eine solche Anwendung nun aber mit DDD, stellt man rasch fest, dass in einem interdisziplinären Team mindestens drei verschiedene Sichtweisen auf das Wort Urlaub bestehen: Es gibt die Rolle desjenigen, der Urlaub nehmen möchte. Es gibt die Rolle derjenigen, die die Urlaubsvertretung gewährleisten müssen. Und es gibt die Rolle desjenigen, der den Urlaub genehmigt. Für diese drei Parteien ist das Wort Urlaub jeweils mit einer anderen Bedeutung behaftet. Es ist sogar so, dass jede dieser Parteien je nach persönlicher Situation eine andere der drei Rollen einnehmen kann (auch ein Vorgesetzter oder jemand aus der HR-Abteilung möchte schließlich irgendwann einmal Urlaub nehmen).

Gemäß DDD ist es also wenig sinnvoll, zu versuchen, die Unschärfe des Begriffs dadurch zu lösen, dass man sich auf eine universelle Sprache verständigt und krampfhaft versucht, für zwei der Begriffe einen mehr oder minder passenden Ersatz zu finden. Stattdessen teilt man die Anwendung schlichtweg in unterschiedliche Kontexte ein:

  • Es gibt zum einen die persönliche Sicht als Bounded Context, in dem der Antrag auf Urlaub gestellt wird. Hier ist mit dem Wort Urlaub tatsächlich das gemeint, woran die meisten Menschen spontan denken.

  • Es gibt zweitens die Teamsicht als Bounded Context, in dem das Wort Urlaub gleichbedeutend ist mit Fehlzeit, die anderweitig ausgeglichen werden muss. Ob das nun erfolgt, weil jemand im Urlaub ist, oder weil derjenige krank ist, spielt für das Team eine untergeordnete Rolle.

  • Drittens gibt es die Verwaltungssicht als Bounded Context, in dem das Wort Urlaub als rein kalkulatorische Einheit gesehen wird. In diesen Zusammenhang fallen dann Begriffe wie Urlaubsanspruch, Resturlaub, Sonderurlaub usw.

Bounded Contexts als Service-Grenzen

Nach dieser Überlegung liegt es nahe, zu sagen, dass man für die verschiedenen Bounded Contexts jeweils einen eigenen Microservice entwickeln könnte:

  • Der erste Microservice fokussiert auf das tatsächliche Stellen und Genehmigen des Urlaubsantrags.

  • Der zweite Microservice kümmert sich um die Planung innerhalb des Teams.

  • Der dritte Microservice übernimmt die organisatorische Verwaltung, welchem Mitarbeiter wie viel Urlaub zusteht.

Es ist leicht denkbar, aus jedem dieser Dienste eine eigene Anwendung zu entwickeln, die man auch gesondert von den übrigen betreiben und – das war die Ausgangsbasis des Ganzen – auch gesondert vermarkten könnte. Wie man sieht, haben hier rein fachliche Überlegungen zu dieser Grenzziehung geführt, keine technischen.

Tatsächlich stellt man nach einer Weile fest, dass Bounded Contexts, die sich durch die Modellierung einer Domäne mit DDD ergeben, auch eine gute Abgrenzung für Microservices sind. Der Grund dafür liegt auf der Hand: Da Bounded Contexts eine in sich geschlossene Sprache beschreiben, die sich mit einer Thematik befasst, lässt sich auch leicht ein entsprechender Microservice entwickeln, der genau diese Thematik und die dazugehörige Sprache aufgreift.

Von DDD zu Microservices, zum Zweiten

Praktischerweise lassen sich auch die anderen genannten Artefakte aus DDD hervorragend auf Microservices mappen: Ein Command ist nämlich letztlich nichts anderes als ein Request an einen Service, ein Event dessen Reaktion darauf – sprich, die Response. Das gilt für alle HTTP-basierten Wege, um Microservices mit einer Schnittstelle nach außen zu versehen. Die Aggregates, die sich logisch innerhalb der Bounded Contexts befinden, gruppieren schließlich innerhalb des Service die Logik und verhindern, dass der Service selbst wieder zum Monolithen wird.

Hat man diesen Zusammenhang zwischen den Bounded Contexts von DDD und Microservices einmal hergestellt, ist es schwer, ihn wieder aus dem Kopf zu bekommen. Letztlich sind beide Konzepte nur zwei Seiten derselben Medaille – nur eben einmal fachlich und einmal technisch formuliert.

CQRS und Event Sourcing lassen grüßen

Im Zusammenhang mit DDD werden häufig auch das Architekturmuster CQRS (Command Query Responsibility Segregation) und der Speichermechanismus Event Sourcing genannt. Mit ihnen verhält es sich im Grunde wie mit DDD: Die gängigen Erklärungen sind durchaus korrekt, aber häufig zu theoretisch und abstrakt. Doch auch sie lassen sich einfach und anschaulich erklären:

  • Die Idee von CQRS ist, das Lesen aus einer Anwendung vom Schreiben in ebendiese zu trennen. Das Ziel davon ist, beide Seiten unabhängig voneinander warten und skalieren zu können. CQRS funktioniert dann besonders gut, wenn Schreibzugriffe als fachliche Aufträge und weniger als Befehle zur Datenmanipulation aufgefasst werden – das ist der Grund, warum CQRS und DDD so gut zusammenpassen: Die Commands aus DDD finden sich in CQRS wieder.

  • Der Ansatz von Event Sourcing ist nicht – wie in relationalen Datenbank üblich –, den Status quo der Daten zu speichern und ihn gegebenenfalls zu überschreiben oder gar zu löschen. Vielmehr gilt es ähnlich wie Git, die Blockchain oder eine Bank bei der Kontoführung, die Deltas stets nur zu speichern, die im Lauf der Zeit zum aktuellen Stand der Dinge geführt haben. Man erhält also eine zeitliche Abfolge von Ereignissen, die sich auch im Nachhinein noch einmal abspielen und neu interpretieren lässt. Dass diese Ereignisse hervorragend mit den Domain Events aus DDD korrelieren, liegt auf der Hand.

Fasst man all das nun zusammen, ergibt sich daraus ein Ansatz, wie sich Schnittstellen von Microservices standardisiert und unabhängig von der zur Implementierung verwendeten Technologie gestalten lassen. Im Grunde gilt es dabei lediglich, nach der gewünschten Technologie für die Kommunikation zu unterscheiden, also REST, gRPC, GraphQL oder etwas anderes. Im Folgenden soll der Fokus auf REST und GraphQL liegen.

Microservices mit REST …

Um es kurz zu machen, passt REST im eigentlichen Sinne nicht gut zu CQRS. Der Grund ist simpel: REST stellt Datenressourcen in den Mittelpunkt, nicht fachliche Prozesse. REST macht genau das, was man, wie zuvor angesprochen, nicht tun sollte, nämlich Commands als Auftrag zur Datenmanipulation ansehen. Das zeigt sich bereits daran, dass jegliche Interaktion mit dem Service auf die immer gleichen vier Verben GET, POST, PUT und DELETE heruntergebrochen wird.

Das passt historisch gesehen wunderbar zu der Denkweise relationaler Datenbanken, da sich die vier Verben auf die passenden SQL-Pendants SELECT, INSERT, UPDATE und DELETE abbilden lassen. Für CQRS ist es jedoch wenig geeignet, und für DDD als Methodik, die Wert auf sprachlichen Ausdruck legt, erst recht nicht. Ein Urlaubsantrag wird nicht created, inserted oder gepostet, er wird abgegeben. Ein Schachspiel wird nicht created, inserted oder gepostet, es wird eröffnet.

Nutzt man REST so, wie es konzeptionell gedacht ist, gibt man all diese Mannigfaltigkeit nicht nur der natürlichen, sondern vor allem auch der Fachsprache auf, und ersetzt sie durch vier Verben, auf die dann als logischer Schluss alle Aktionen heruntergebrochen werden müssen. Wie ausdrucksstark das funktioniert, mag sich jeder selbst ausmalen.

Unabhängig davon ist REST in der Praxis aber ohnehin häufig nicht das, was mit REST in der Theorie gemeint ist: Viele Entwickler verwenden den Begriff REST synonym zu HTTP mit JSON – quasi als Gegenentwurf zu SOAP, was in gewissem Sinne für HTTP mit XML steht.

Wendet man also das Konzept eines einfachen HTTP API an, passt das zu CQRS schon ganz gut: Commands bewirken eine Veränderung und werden deshalb per POST an den Service übermittelt. Domain Events hingegen informieren über Änderungen und müssen daher gelesen werden, werden also über GET abgerufen. Mehr gibt es nicht. Um einen Urlaubsantrag abzugeben, genügt ein Aufruf der Route POST /submit-vacation-request-form, um ein Schachspiel zu eröffnen, POST / open-game.

... und mit GraphQL

Noch passender wird es allerdings, wenn man den Service mit einem GraphQL-Endpunkt ausstattet. GraphQL passt konzeptionell hervorragend zu CQRS, und kennt neben den flexiblen Queries (für die GraphQL im Wesentlichen bekannt ist), auch sogenannte Mutations und Subscriptions.

Eine Mutation ist dabei ein Auftrag an den Server, etwas zu verändern. GraphQL stellt dabei nicht zwingend ein Datenschema in den Mittelpunkt, sondern kann auch Funktionsaufrufe mitsamt Parametern abbilden. Eine Mutation entspricht in GraphQL also eher einem Remote Procedure Call (RPC) – und nichts anderes ist ein Command letztlich. Eine Funktion bildet die Intention eines Commands nämlich viel besser ab, als ein reines Datenobjekt, denn schließlich geht es bei einem Command darum, einen fachlichen Vorgang anzustoßen.

Eine Subscription hingegen ist eine Art Abonnement, mit dem sich der Client vom Server über Dinge, die passiert sind, informieren lassen kann. GraphQL kümmert sich dabei um den technischen Aspekt der langlaufenden Verbindung, aber inhaltlich klingt das genau nach dem, was man für das Zustellen von Domain Events benötigt.

Verlässt man also den allseits bekannten Pfad von GraphQL und schaut, welche Werkzeuge abseits davon mit Mutations und Subscriptions zur Verfügung stehen, stellt man rasch fest, dass GraphQL und CQRS perfekt zusammenpassen. Das Schöne ist, dass eine Entscheidung für GraphQL keine Entscheidung gegen das zuvor beschriebene HTTP API sein muss – beides lässt sich problemlos parallel betreiben.

Event Sourcing in Microservices

All das ist unabhängig von der Frage, ob man Event Sourcing in einem Microservice einsetzt oder nicht. Tatsächlich lässt sich das sogar, je nach Bedarf, von Service zu Service individuell entscheiden.

Tatsache ist aber, dass der Einsatz von Event Sourcing ebenso wie die vorherigen Überlegungen zu HTTP- und GraphQL-basierten APIs nur möglich ist, weil man statt der Technologie die Fachlichkeit und deren Begriffe in den Vordergrund rückt. Und genau das ist das Verdienst von Domain-driven Design.

Es ist also sehr wohl möglich, DDD ohne CQRS und Event Sourcing einzusetzen, und selbstverständlich kann man auch CQRS oder Event Sourcing ohne die jeweils beiden anderen Konzepte nutzen. Doch fügt man diese Bausteine zusammen, ergibt sich als Gesamtes mehr, als es die Summe der Einzelteile vermuten lassen würde.

Fazit

Man kann also beginnen, Microservices weniger als technisches Artefakt zu sehen. Selbstverständlich sind Microservices auch zu einem gewissen Grad technisch motiviert, doch der Kern ist das Kapseln von Fachlichkeit, und das ist der Bereich, in dem sich Microservices und Domain-driven Design überschneiden. Leider ist das, wie erwähnt, auch der Bereich, wo man die eigene Komfortzone verlassen und sich auf Neues einlassen muss. Wenn es aber gelingt, über den eigenen Schatten zu springen und dieses Neue nicht als lästiges Problem, sondern als Chance anzusehen, neue Welten kennenzulernen, öffnen sich ganz neue Türen.

Ob man sich diesem Schritt nun eher von der einen oder von der anderen Seite her nähert, spielt dabei letztlich keine so große Rolle: Am Ende gehören DDD und Microservices untrennbar zusammen, wenn man es denn zulässt. Und wem das gelingt, der hält die Bausteine für ein nachhaltiges Geflecht aus einander ergänzenden fachlichen Diensten in der Hand. Wir müssen uns nur trauen.

roden_golo_sw.tif_fmt1.jpgGolo Roden ist Gründer und CTO der the native web GmbH (www.thenativeweb.io), eines auf native Webtechnologien spezialisierten Unternehmens. Für die Entwicklung moderner Webanwendungen bevorzugt er JavaScript und Node.js und hat mit „Node.js & Co.“ das erste deutschsprachige Buch zu diesem Thema geschrieben. Darüber hinaus ist er journalistisch für Fachmagazine und als Referent und Content Manager für Konferenzen im In- und Ausland tätig. Für sein qualitativ hochwertiges Engagement in der Community wurde Golo von Microsoft vierfach als Microsoft MVP ausgezeichnet.

Agile und DevOps sind Konzepte und Trends gleichermaßen. Sie stehen für eine maximale Flexibilisierung der Projektarbeit und das Zusammenwachsen von Development und Operations. Gibt es eine Schnittmenge zwischen diesen beiden Ansätzen?

Die agile Vorgehensweise – mit dem Ziel einer geplanten Flexibilität des Entwicklungszyklus – und DevOps – als die Zusammenarbeit von Development und Operations – sind umfassende Bausteine einer Transformation der Informationstechnologie im Allgemeinen und der Softwareentwicklung als wichtige Teildisziplin im Speziellen. In vielen Beiträgen findet man agile Methoden ausführlich beschrieben. Es gibt kaum einen Entwickler, der damit noch nicht in Berührung gekommen ist. Ausgangspunkt war die Notwendigkeit, den Entwicklungsprozess deutlich flexibler zu gestalten. Statt mit ungeplanten Überraschungen durch sich ändernde Anforderungen konfrontiert zu sein, wollte man sich auf diese Ereignisse bestmöglich vorbereiten und damit die Änderung zur Norm erklären. Eine ähnlich simple Erklärung gibt es für DevOps. Konnte man traditionell die Entwicklung einer Software (Development) von ihrem Betrieb (Operations) trennen, ist das schon lange nicht mehr zeitgemäß. Sich immer weiter verkürzende Releasezyklen erfordern ein Zusammenarbeiten über die Grenzen der eigenen Abteilung hinaus. DevOps können wir also verkürzend als eine Kulturrevolution der gesamten IT-Landschaft begreifen. Und an welcher Stelle ist die Schnittmenge zwischen beiden Konzepten? Das wollen wir hier untersuchen. In einer zweiteiligen Artikelserie wollen wir die Zusammenhänge zwischen agilem Vorgehen und DevOps-Ansatz herausarbeiten.

Nach einem kompakten Überblick über die agilen Methoden skizzieren wir den Grundgedanken der DevOps-Bewegung, die mehr mit Organisation als mit Technik zu tun hat. Anschließend geht es darum, die Schnittmengen beider Konzepte herauszuarbeiten und zu erkennen, an welcher Stelle die Chancen, aber auch mögliche Stolpersteine der Zusammenarbeit liegen. Beginnen wir mit den agilen Methoden der Softwareentwicklung.

Flexibilität durch agile Methoden

Bekanntermaßen ist die strikte Abarbeitung eines Entwicklungsvorhabens streng nach Phasen gestaffelt eher schwierig. Das Hauptproblem besteht darin, dass eine vollständige und korrekte Erhebung der Anforderungen zu Projektbeginn nahezu unmöglich ist. Das hat mehrere Ursachen. Die wichtigsten sind:

  • Unvollständigkeit der Anforderungen zu Beginn des Projekts

  • dynamische Änderungen in den Geschäftsmodellen während der Projektlaufzeit

  • lange Projektlaufzeiten, teilweise über mehrere Jahre

Es ist eine Tatsache, dass auch die Kunden und späteren Nutzer einer Software oft nur eine ungefähre Vorstellung davon haben, in welcher Form die Software das bestehende Problem lösen soll. Darüber hinaus ist es schwierig, den Entwicklern der Software diese ungefähren Anforderungen zu vermitteln, sie beispielsweise für diesen Zweck auch vollständig und nachvollziehbar zu dokumentieren. Ein weiteres Problem ergibt sich aus der Dynamik der Geschäftsmodelle: Anforderungen und Rahmenbedingungen, die zum Zeitpunkt der erstmaligen Analyse gelten, sind nicht über die gesamte Projektlaufzeit konstant. Dabei ist zu beobachten, dass das Ausmaß an Dynamik im Laufe der Zeit zunimmt. Da die zu erstellenden Anwendungssysteme immer komplexer und umfangreicher werden, nehmen auch die Laufzeiten der Projekte zu. Das wirkt sich ebenso negativ auf die Möglichkeiten einer umfassenden Planung aus. Je länger die Zeitspanne zwischen dem Projektstart und der endgültigen Bereitstellung der Software ist, desto schwieriger wird es, die dann geltenden Kundenanforderungen zu erfüllen.

Die agilen Methoden sind nicht vom Himmel gefallen, sondern die Folge einer längeren zeitlichen Entwicklung (Abb. 1).

krypczyk_bochkor_agil_devops_1.tif_fmt1.jpgAbb. 1: Entwicklungsmethoden in zeitlicher Folge

In den 1980er und frühen 90er Jahren wurde großen Wert auf sorgfältige Projektplanung und formalisierte Qualitätssicherung gelegt. Die Entwicklung erfolgte oft in großen Teams. Im Ergebnis wurde oft mehr Zeit für die Planung aufgewendet als für die tatsächliche Programmentwicklung und die nachfolgenden Tests. Trotz dieses sehr planvollen und stark phasengetriebenen Vorgehens waren die Ergebnisse nicht immer zufriedenstellend. Zu viele Projekte scheiterten oder konnten die Anforderungen und Wünsche der Kunden nicht erfüllen. Als Reaktion wurde ab Mitte der 1990er Jahre erstmals von agilen Methoden gesprochen. Das Ziel war schnell formuliert: Die Entwickler sollten sich verstärkt um ihren eigentlichen Job, die Programmentwicklung, kümmern und nicht mehr so viel Zeit für Entwurf und Dokumentation aufwenden. „Agilität“ steht also für die Fähigkeit, mit Änderungen umzugehen, d. h. schnell und flexibel auf Kundenanforderungen zu reagieren. Änderungen werden nicht mehr als störend aufgefasst, sondern tragen dazu bei, dass das Anwendungssystem frühzeitig in die richtige Richtung gesteuert wird. Ein wesentliches Ziel dieses Vorgehens ist es, in möglichst kurzer Zeit eine erste Version des späteren Endprodukts an den Kunden auszuliefern und schon in sehr frühen Phasen des Entwicklungsprozesses mit Prototypen zu arbeiten. Frühe produktionsfähige Produktversionen sind zwar im Funktionsumfang eingeschränkt, geben jedoch den Anwendern die Möglichkeit, rechtzeitig Feedback zu geben. Dieses Feedback ist wiederum die Ausgangsbasis, um mögliche Modifikationen zeitnah in den weiteren Projektverlauf einzuarbeiten.

Es gibt unterschiedliche Ansätze agiler Entwicklungsmethoden. Scrum gilt heute als sehr weit verbreitet, hat sich vielfach bewährt und ist weithin akzeptiert und stellt zusammen mit Extreme Programming die bekannteste agile Methode dar.

Extreme Programming – oder kurz XP – wurde in den Jahren 1995 bis 2000 von Kent Beck, Ward Cunningham und Ron Jeffries entwickelt. Der Name wurde dadurch geprägt, dass die gut bekannten Praktiken, beispielsweise iterative Entwicklung, „ins Extreme“ gesteigert wurden. Mit XP ist es möglich, mehrere neue Versionen eines Programms an einem Tag zu entwickeln, zu integrieren und zu testen. Ziel ist es, die Entwicklung der Software den gegebenen Bedingungen anzupassen und diese fortlaufend zu verbessern. Die Anforderungen werden in Form von User Stories zusammengefasst und anschließend als eine Folge von Aufgaben implementiert. Welche User Story als nächste in ein Release aufgenommen wird, wird auf Basis der zur Verfügung stehenden Zeit und der zugeordneten Priorität entschieden. Großen Wert wird auf paarweise Zusammenarbeit, d. h. Pair Programming, gelegt. Ergebnisse werden auf diese Weise sofort überprüft. Mit Ihrem Programmierpartner unterstützen Sie sich jederzeit gegenseitig. Statt erst Programmcode zu schreiben und dann umfangreich nach möglichen Fehlern zu suchen, dreht man den Spieß einfach um. Bevor man zum Code schreiben übergeht, werden Tests für die einzelnen Aufgaben entwickelt. Die sollen mit Erfolg abgeschlossen werden, bevor neuer Code in das System integriert wird. Nach Fertigstellung einer Aufgabe ist sie auch sofort in das System zu übernehmen. Diese Art des Vorgehens wird als Test-driven Development bezeichnet. Die Prinzipien von XP sind daher: inkrementelle Planung, einfacher Entwurf und kleine Releases, kollektives Arbeiten, Pair Programming, Test-driven Development und Kunden vor Ort. Mit dem letzten Aspekt ist gemeint, dass ein Kundenbevollmächtigter am Entwicklungsprozess kontinuierlich beteiligt ist und dem XP-Team ständig zur Verfügung steht. Er ist dafür verantwortlich, dem Team die Systemanforderungen mitzuteilen und Akzeptanztests für das System durchzuführen. Der Kunde arbeitet gewissermaßen aktiv am Projekt mit.

Scrum ist die am häufigsten angewandte agile Methode, die als Rahmenwerk für agile Prozesse bekannt ist. Wir stellen sie etwas ausführlicher vor. Dabei handelt es sich nicht um ein Modell, das das agile Vorgehen konkret beschreibt bzw. konkrete Techniken vorgibt. Scrum gibt nur den Rahmen vor. Der Begriff „Scrum“ stammt aus der Sportart Rugby. Dabei stehen sich zwei gegnerische Mannschaften gedrängt gegenüber („Scrum“ = „Gedränge“) und versuchen, den Ball zu erkämpfen. Ein selbstorganisiertes Entwicklerteam steht im Mittelpunkt von Scrum. Der Scrum Master bildet eine Schnittstelle zwischen dem Team und den Produktverantwortlichen. Er sorgt dafür, dass der Entwicklungsprozess reibungslos abläuft. Die Idee ist, dass bei umfangreichen Projekten der Erfolg nur durch regelmäßiges Feedback zu erreichen ist. Dadurch werden die Abweichungen vom Soll schnell erkannt und notwendige Anpassungen ermöglicht. Die gesamte Entwicklung wird in Iterationen mit einer Dauer zwischen einer und vier Wochen strukturiert und durchgeführt. Diese Iterationen werden als Sprints bezeichnet. Die Sprints haben eine feste Dauer und enden unabhängig davon, ob die Aufgabe abgeschlossen werden konnte oder nicht. Die Produktanforderungen des Kunden werden vor dem Sprint in einem Product Backlog gesammelt, das fortlaufend vervollständigt wird. Das Team entscheidet über die Priorisierung der Anforderungen bzw. Aufgaben.

Jeder Sprint endet mit der Überprüfung der Ergebnisse (Sprint Review) und Reflexion der Prozesse (Sprint Retrospective). Am Ende jedes Sprints steht eine lauffähige, getestete und inkrementell verbesserte Software zur Verfügung. Scrum unterscheidet zwischen drei Rollen: Der Product Owner repräsentiert in Scrum die Kundenbedürfnisse. Im besten Fall handelt es sich um den Kunden selbst oder einen vom Kunden bevollmächtigten Vertreter. Er trifft eigenständig und zügig qualifizierte Entscheidungen über das Produkt. Er steuert die Softwareentwicklung und arbeitet mit dem Team während des ganzen Projekts eng zusammen. Der Product Owner koordiniert die Kundenanforderungen und priorisiert sie im Product Backlog. Er definiert Anforderungen und deren Akzeptanzkriterien und nimmt die Arbeitsergebnisse des Teams ab. Der Scrum Master ist der Coach des Teams. Er hilft den Teammitgliedern, Scrum richtig einzusetzen. Dabei ist seine Rolle umso wichtiger, je weniger Erfahrung das Team mit Scrum hat. Der Scrum Master hat im Projekt keine spezifischen Verantwortlichkeiten und Aufgaben, die unmittelbar mit der Umsetzung desselben zu tun haben. Wenn ein Scrum Master seine Arbeit gut macht und das Team richtig zusammenarbeitet, wird er kaum noch benötigt. Das Team hilft sich in allen Situationen selbst. Das Entwicklerteam hat die zentrale Rolle, da es alle Arbeiten ausführt, die zur Umsetzung der Anforderungen notwendig sind. Es ist von Vorteil, wenn die Mitglieder in einem Scrum-Team über ein breites Spektrum an Wissen verfügen. Dieses Wissen darf sich nicht in den Köpfen von Spezialisten konzentrieren, sondern es muss gemeinsames Wissen des gesamten Teams sein oder werden. Die Teammitglieder bezeichnet man auch gerne als „generalistische Spezialisten“. Das Team organisiert sich selbst (Empowered Team), d. h. es entscheidet über eine Vielzahl von Dingen selbstverantwortlich. Hierdurch entsteht eine höhere Identifikation mit dem Projekt und das Verantwortungsgefühl steigt. Insbesondere schätzt das Team selbst die Aufwände für die einzelnen User Stories, und es übernimmt die Verantwortung dafür, dass diese richtig konzipiert sind. Ein wichtiger Punkt ist das Commitment im jeweiligen Sprint auf bestimmte User Stories. Dabei entscheidet das Team, welche und wie viele User Stories es im nächsten Sprint umsetzen kann. Jeder Mitarbeiter kann unabhängig von seiner Position eine der drei Rollen annehmen.

Ein zentrales Merkmal von Scrum ist das feste Inventar von regelmäßigen, kompakten und ergebnisorientierten Meetings. Das sogenannte Sprint Planning ist das Startmeeting für den jeweiligen Sprint und wird in zwei Schritten ausgeführt, nämlich der Festlegung des Inhalts des nächsten Sprints und der Erstellung des Sprint Backlogs. Die Sprint Review wird am Ende jedes Sprints durchgeführt und dient zur Überprüfung und Abnahme des Produktinkrements durch den Product Owner. Dabei muss das Team die Ergebnisse präsentieren und zeigen, dass die von ihm entwickelten Features funktionieren. Durch die regelmäßige Durchführung bekommt es immer sofort ein Feedback und eine Entwicklung in die falsche Richtung kann ausgeschlossen werden. Die Sprint Retrospective zielt im Gegensatz zur Sprint Review auf eine Analyse der Zusammenarbeit während des vorangegangenen Sprints ab. Es ist für die Qualität nicht nur wichtig, dass die Features auch funktionieren, sondern es ist für alle Beteiligten auch äußerst interessant, wie Sie die Ergebnisse erreicht haben. Welchen Programmieransatz haben Sie gewählt? Gibt es vielleicht eine einfachere Lösung zum Ziel? Aus der gemeinsamen Betrachtung zieht man Schlüsse für den nächsten Sprint. Daily Scrums sind tägliche 15-minütige Meetings des Teams unter Beteiligung des Scrum Masters und eventuell des Product Owner. Sie dienen hauptsächlich dem Informationsaustausch des Teams untereinander. Hier wird Ihnen meist kein Sitzplatz zur Verfügung gestellt. Ganz im Sinne der Dynamik und der effizienten Arbeitsweise erfolgt die kurzfristige Zusammenkunft meist im Stehen. Eine Tasse Kaffee ist aber erlaubt.

Grundsätzlich verfolgt jedes Scrum-Meeting das Ziel, die Ergebnisse zu analysieren und die daraus resultierenden Anpassungen unmittelbar umzusetzen. Ein wichtiges Kriterium für den Projekterfolg ist die Teamarbeit. Eine weitgehend reibungslose Zusammenarbeit zwischen den Teammitgliedern wird angestrebt. Bei der Teamgröße sollte man sich daher auf maximal zehn Mitglieder beschränken. Bewährt haben sich Teilteams von durchaus nur sechs Mitgliedern. Diese müssen Programmier-, Projekt- und Teamerfahrung haben. Die Kernfeatures von Scrum sind in Abbildung 2 dargestellt.

krypczyk_bochkor_agil_devops_2.tif_fmt1.jpgAbb. 2: Scrum als bekannteste agile Methode [1]

Einige weitere agile Methoden im kurzen Überblick:

  • Unified Process: Es handelt sich um ein iteratives und inkrementelles Framework für Softwareentwicklungsprozesse. Die bekannteste und umfassend dokumentierte Verfeinerung des vereinheitlichten Prozesses ist der Rational Unified Process.

  • Feature-driven Development (FDD): Eine Sammlung von Methoden, Arbeitstechniken, Strukturen und Rollen für das agile Projektmanagement, die den Begriff Feature in den Mittelpunkt stellen; die gesamte Entwicklung wird anhand eines Featureplans organisiert und umgesetzt.

  • Agile Model-driven Development (AMDD): Ist eine agile Version des Model-driven Developments (MDD), ein Ansatz zur Softwareentwicklung, bei dem umfangreiche Modelle erstellt werden, bevor der Quellcode geschrieben wird; bei AMDD erstellen Sie nur minimale Modelle. Diese müssen gerade noch gut genug sein, um die Entwicklung voranzutreiben.

  • Dynamic Systems Development Method (DSDM): Dabei handelt es sich um eine Erweiterung des Rapid-Application-Development-Ansatzes, die die kontinuierliche Einbindung der Anwender betont. Die Methode wurde ursprünglich in den 1900er Jahren in Großbritannien durch das DSDM-Konsortium geschaffen und wird heute noch weiterentwickelt und erfolgreich eingesetzt.

Nach diesem kurzen Abriss zu den agilen Methoden der Softwareentwicklung kommen wir nun zum Grundgedanken von DevOps, dem zweiten Baustein unserer Symbiose moderner Projektsteuerung und -durchführung.

Kein Gerangel um Zuständigkeiten durch DevOps

Die Idee hinter dem Begriff DevOps ist – wie bereits in der Einführung angedeutet – schnell erklärt. Er setzt sich zusammen aus Dev (für Development) und Ops (für Operations), also den IT-Betrieb. Damit wird ein Schulterschluss zwischen den Mitarbeitern beider Bereiche symbolisiert und versucht, den traditionellen Graben zwischen diesen beiden wichtigen Bereichen der IT zu überwinden. Dieser Graben resultiert aus Zielkonflikten, d. h. der Entwicklungsbereich ist stark daran interessiert, die implementierten Features möglichst kurzfristig an die Nutzer der Software auszuliefern. Dem IT-Betrieb ist dagegen vorrangig an einem kontinuierlichen und sicheren Betrieb der gesamten IT-Landschaft gelegen. Die Übernahme von Softwareupdates stellt in diesem Sinne stets einen Eingriff in das laufende System dar und kann dessen Stabilität immer bedrohen. Statt kurzfristiger und häufiger Releases werden lange Laufzeiten und umfassend geplante Updates bevorzugt. Diese Form der Arbeitsteilung scheint überholt. Software hat in der heutigen digitalen Welt einen ganz anderen Stellenwert, als das vor Jahren der Fall war. Statt lediglich ein Werkzeug, ist Software heute oft der Treiber von Innovationen. Unternehmen konkurrieren um die besten Lösungen auf dem Markt und setzen dabei zunehmend auf innovative IT-Lösungen. Damit Software diese Aufgabe erfüllen kann, muss sie zeitnah zur Verfügung stehen. Dieser Trend nimmt weiter zu, denn der Bedarf an speziellen Softwarelösungen steigt stetig.

DevOps zielt also genau darauf ab, die Prozesse in Unternehmen, von der Entwicklung über die Bereitstellung in der Produktion bis hin zur Wartung, so miteinander zu verzahnen, dass die beschriebenen Herausforderungen gelöst werden. Das kann nur dann gelingen, wenn die gegenläufigen Ziele einer iterativen Softwareentwicklung und der Anspruch nach einem sicheren und stabilen Betrieb in Einklang gebracht werden. Die Vorteile von DevOps lassen sich zum Beispiel wie folgt zusammenfassen:

  • Geschwindigkeit: Man ist in der Lage, Kunden Softwarelösungen schneller bereitzustellen.

  • Zuverlässigkeit: IT-Lösungen werden zuverlässiger, wenn sie schneller an sich ändernde Bedingungen angepasst werden können.

  • Zusammenarbeit: Die Trennung zwischen IT-Entwicklung und -Betrieb wird überwunden.

DevOps basiert auch auf neueren technologischen Ansätzen. Dazu gehören Continuous Integration, Continuous Delivery, Microservices und Infrastructure as Code. Diese stehen jedoch nicht im Vordergrund, denn es handelt sich nicht um eine Technik, sondern um eine Kultur. Die Grundpfeiler sind gegenseitiger Respekt und Vertrauen. Angetrieben werden sie von dem gemeinsamen Ziel, das Projekt ständig zu verbessern. Erfolge und Misserfolge werden nicht einzelnen Personen, sondern dem Team zugeschrieben. Voraussetzung ist, dass sich die Personen und das Team als Ganzes stetig den neuen Herausforderungen anpassen. DevOps ist zwar ein Thema, das in der Informationstechnik angesiedelt ist, aber im Kern mit der Entwicklung der Organisation zu tun hat (Changemanagement). Damit ist auch klar, dass DevOps eine Aufgabe des Managements ist und kein technischer Ansatz. Der Lernprozess muss daher von der Managementebene aus initiiert werden, sonst bleibt er stecken. Zwar muss eine DevOps-Strategie stets unternehmens- bzw. projektindividuell umgesetzt werden, aber trotzdem lassen sich grundsätzliche Empfehlungen ableiten:

  • Ziel: Dieses besteht in der schnellen Bereitstellung von qualitativ hochwertigen Softwarelösungen. Dazu müssen Entwickler, Tester und Betriebler gemeinsam an diesem Ziel arbeiten. Es sollte ein durchgehender Prozess aufgestellt werden, der von der Anforderungsdefinition bis zur Überwachung der Produktion reicht. Durchgängigkeit vermeidet Schnittstellen durch Zuständigkeitsbereiche. Mit anderen Worten: Die Verantwortung endet nicht mit der Erledigung der Aufgaben, die zur eigenen Profession passen.

  • Start: Bevor DevOps in der gesamten Organisation umgesetzt wird, muss man die Arbeitsweise an kleineren Vorhaben studieren und ausprobieren. Es ist ratsam, mit einem überschaubaren Projekt und einem kleineren Team zu beginnen. Das Management muss die neuen Wege des Teams ausdrücklich unterstützen. Wie bei jeder Veränderung gilt es, Widerstände zu überwinden.

  • Team: Um erfolgreich ein Projekt auf der Basis von DevOps zu etablieren, wird ein Team aus den richtigen Personen benötigt. Initial kann es sinnvoll sein, die Verantwortlichen zusammenzubringen. Das können zum Beispiel ein Vertreter des Managements, ein Projektmanager, ein Entwicklungsleiter und ein Verantwortlicher aus dem operativen Bereich sein. Das interdisziplinäre Team muss bewusst geformt werden.

  • Tools: Natürlich wird man neue Tools einsetzen. Dennoch: Eine Entwicklung zu DevOps kann man nicht kaufen, d. h., die Anschaffung von Tools löst noch keine Probleme.

  • Fortschritt: Die Entwicklung muss man verfolgen und die erreichten Fortschritte anhand von Kennzahlen, zum Beispiel der Zahl der Releases oder der Zeitdauer von der Anforderung bis zur Produktion eines Features, messen. Diese Kennwerte geben bei aller Unschärfe eine Orientierung über die erreichten Erfolge.

Nachdem wir einen Streifzug durch die agile Softwareentwicklung und das Thema DevOps unternommen haben, untersuchen wir nun die Frage, wie beide Bereiche zusammenhängen.

Im Duo schlagkräftig

Beide Konzepte sind bereits für sich betrachtet mächtig und es bedarf großer Anstrengungen, sie in eine etablierte Arbeits- und Projektkultur zu integrieren. Eine agile Vorgehensweise, zum Beispiel Scrum, und der DevOps-Ansatz können sich ergänzen. Wie könnte denn nun ein solches kombiniertes Vorgehen aussehen? Ausgangspunkt unserer Überlegungen ist Abbildung 3.

krypczyk_bochkor_agil_devops_3.tif_fmt1.jpgAbb. 3: Lücken im Software-Lifecycle [2]

Wir haben es mit den beiden Bereichen Business und Informationstechnik (IT) zu tun. Business steht stellvertretend für den Anwender, den Kunden und letztendlich auch den Auftraggeber. Dieser möchte mit der Software arbeiten und erhofft sich durch den Einsatz von Software Vorteile bei der Erledigung seines Geschäfts. IT wird, wie gesagt, zunehmend zu einem Business-Innovator und damit immer wichtiger. Die andere Seite wird durch die IT repräsentiert. Für den Kunden spielt Struktur und Organisation der IT keine Rolle. Das Ziel ist die Bereitstellung von Software. Zwischen Business und IT klafft allzu oft eine Lücke (Gap). Diese Lücke resultiert aus mehreren Aspekten. Dem Anwender fällt es schwer, seine Anforderungen genau auszudrücken, und die IT kann aus dieser unspezifischen Formulierung der Anforderungen oft nur ein ungenügendes Bild von der zu entwickelnden Software zeichnen. Wie wir im Abschnitt über die agilen Methoden ausgeführt haben, entsteht die Lücke zwischen beiden Bereichen im Wesentlichen auch aus der Projektdynamik. Anforderungen können erst im Lauf des aktiven Projekts spezifiziert werden bzw. unterliegen sie stärkeren Änderungen. Die Lücke innerhalb der IT beschreibt die unterschiedlichen Zuständigkeiten zwischen Development und Operations.

Die gemeinsame Aufgabe der agilen Arbeitsweise und des DevOps-Ansatzes ist es, diese Lücken zu schließen. Wir nähern uns der Idee, wenn wir uns die Zuständigkeiten der Beteiligten genauer ansehen (Abb. 4).

krypczyk_bochkor_agil_devops_4.tif_fmt1.jpgAbb. 4: Zuständigkeiten der Beteiligten im Software-Lifecycle [2]

Beginnen wir auf der Seite des Business. Hier werden die Bedeutung und Integration des zu entwickelnden Softwaresystems festgelegt. Mit dem Begriff Enterprise Architecture meint man die grobgranulare IT-Architektur eines Unternehmens, die die Schnittstelle zwischen Geschäftswelt und Technik repräsentiert. Im Allgemeinen geht man davon aus, dass die folgenden Schichten enthalten sind:

  • Prozessarchitektur: Darunter wird die Abbildung der Geschäftsprozesse verstanden

  • Datenarchitektur: Stellt den Informationsfluss und die benötigten Daten für die Ausführung eines Geschäftsprozesses dar

  • Applikationsarchitektur: Es handelt sich um die konkreten Systeme in Form von Anwendungen und Produkt-Suites, d. h. die Zusammenfassung einzelner Anwendungen zu einer gesamten Softwarelösung

  • Technische Architektur: Die technologischen Aspekte umfassen die Hardware, die Netzwerk- und Virtualisierungstechnologie; auf Ebene der Software gehören die Betriebs- und Datenbanksysteme dazu

Einen entscheidenden Einfluss auf die Enterprise Architecture hat das zugrunde liegende Business, das durch die IT unterstützt werden soll. Damit ist auch klar, dass es die eine Enterprise-Architektur nicht gibt, sondern dass ihre konkrete Ausgestaltung durch das geschäftliche Umfeld bestimmt wird. Erweitert man die Enterprise Architecture um genau die spezifischen Elemente aus der geschäftlichen Ebene, so entsteht die sogenannte Business-Enterprise-Architektur. Die zweite Aufgabe auf Businessseite ist das Portfoliomanagement. Darunter versteht man grob formuliert die Zuteilung der finanziellen, sachlichen und persönlichen Ressourcen auf Projekte und Vorhaben.

Kommen wir zum Bereich der IT und beginnen mit den typischen Aufgaben der Entwicklung. Das sind Design und Development. Development beschäftigt sich mit der technischen Umsetzung des Anwendungssystems. Im Ergebnis entstehen lauffähige Applikationen für die unterschiedlichsten Systeme in Form verschiedenster Anwendungsarten. Kernaufgabe ist die Implementierung mit Hilfe von Werkzeugen und Programmiersprachen. Zunehmend wichtiger wird die Gestaltung des User Interface. Die Benutzerschnittstelle einer Anwendung ist der einzige Punkt, mit dem der Nutzer in Berührung kommt. Demzufolge ist die Gestaltung des User Interface für die Akzeptanz eines Softwaresystems von entscheidender Bedeutung. Dieser Vorgang wird hier als Design bezeichnet. Im betrieblichen Umfeld haben wir es oft noch mit grafischen Oberflächen zu tun. Andere Interaktionsmöglichkeiten wie Sprache, Blick- oder Gestensteuerung nehmen jedoch an Bedeutung zu. Die Ausgestaltung des User Interface überlässt man heute nicht mehr dem Zufall, vielmehr findet es im Rahmen eines professionellen Designprozesses statt.

Ebenso sind die Aufgaben der Operations auf der Seite der IT angesiedelt. Nach den ausführlichen Tests der Applikation muss diese zur Nutzung auf den Zielsystemen bereitgestellt werden. Produktionsfähige Versionen des Anwendungssystems werden als Releases bezeichnet. Die fortlaufende Sicherstellung der Betriebsfähigkeit und deren Überwachung erfordert ein Monitoring des Anwendungssystems und ist traditionell auch eine Aufgabe des IT-Betriebs.

Neue Ansätze zur Gestaltung des Entwicklungs- und Bereitstellungsprozesses haben das Ziel, die Aufgaben zusammenzufassen, künstliche Schnittstellen zu beseitigen und für einen viel schnelleren Durchlauf des Softwarelebenszyklus zu sorgen. Die Rede ist von Collaborative Development, Continuous Testing, Continuous Release und Continuous Monitoring and Optimization. Die Bedeutung dieser Schlagworte wird sofort klar, wenn Sie die Weiterentwicklung der Infografik betrachten, wie sie in Abbildung 5 dargestellt ist.

krypczyk_bochkor_agil_devops_5.tif_fmt1.jpgAbb. 5: Zusammenfassung von Prozessen zu kontinuierlichen Zyklen [2]

Mittels agiler Softwarewareentwicklung und DevOps wird nun versucht, die oben beschriebenen Lücken zu schließen. Bereits aus den Ausführungen zu den agilen Ansätzen hat sich ergeben, dass die Problemlage zwischen Business und IT auf der ungenauen Formulierung der Anforderungen beruht. Das Requirements Engineering stößt an seine Grenzen, wenn es versucht, die Anforderungen an das zu entwickelnde System im Vorhinein generalstabsmäßig zu planen und umzusetzen. Wie man es besser machen kann, zeigen die agilen Methoden, wie zum Beispiel Scrum.

DevOps schließt die Lücke innerhalb der IT und hat sehr viel mit Organisation und Changemanagement zu tun. Werden beide Ansätze umfassend, korrekt und vor allem durchgängig angewendet, dann entsteht eine neue Form des Software-Lifecycle (Abb. 6).

krypczyk_bochkor_agil_devops_6.tif_fmt1.jpgAbb. 6: Agile Methoden und DevOps im Zusammenhang

Von der Idee über die Erhebung der Anforderungen und das Development bis hin zur Bereitstellung arbeiten alle Beteiligten zusammen. Lücken und Systembrüche werden weitgehend vermieden. Der gesamte Vorgang wird agil gestaltet, sodass Änderungen in den Anforderungen und Rahmenbedingungen kein Dilemma mehr darstellen. Zudem ist die Verantwortung der Beteiligten auf Seiten der IT stets umfassend und nicht an Silos (Development, Operations) gebunden. Vielmehr hat man es mit einem multidisziplinären Team zu tun, dessen Ziel eine schnell verfügbare und qualitativ hochwertige Software ist.

Fazit

Aus Sicht der Autoren passen die agile Arbeitsweise und DevOps wunderbar zusammen. DevOps bietet gewissermaßen den Rahmen für die Zusammenarbeit innerhalb der IT. Es wird nicht mehr danach gefragt, wer zuständig ist, sondern die IT liefert die Innovation für den künftigen Geschäftserfolg. Mit agilem Arbeiten wird dem Umstand Rechnung getragen, dass sich Anforderungen und Bedingungen jederzeit ändern können.

Nach diesen Ausführungen in der Theorie stellt sich natürlich die Frage, wie es in der Praxis aussieht [3]. Mit anderen Worten: Wie groß ist die Lücke zwischen Theorie und Praxis? Diese Frage beantworten wir im kommenden zweiten Teil der Artikelserie. Wir werden Erfahrungen, Hemmnisse und Lösungen unter anderem anhand von Praxisprojekten beleuchten.

krypczyk_veikko_dr_sw.tif_fmt1.jpgDr. Veikko Krypczyk ist begeisterter Entwickler und Fachautor.

bochkor_olena_sw.tif_fmt1.jpgElena Bochkor arbeitet am Entwurf und Design mobiler Anwendungen und Webseiten.

Weitere Informationen zu diesen und anderen Themen der IT finden Sie unter http://larinet.com.

Die Cloud ist nach wie vor eines der großen Hypethemen. Versprechungen werden gemacht und Hoffnungen geschürt. Doch welche davon löst die Cloud ein? Auch wenn der Begriff der Cloud schon länger verwendet wird, ist die Abgrenzung zwischen den verschiedenen Varianten nicht immer eindeutig. Für diese Kolumne unterscheide ich sie in die nachfolgenden Abschnitte.

Infrastructure as a Service

Die Grundidee von IaaS basiert auf einer vorhandenen (physikalischen) Maschine, die in die Cloud verschoben wird. So wird man innerhalb von Minuten zum stolzen Betreiber einer Cloudlösung. Systemadministratoren können erleichtert aufatmen – die Verantwortung, die Maschine zu betreiben, liegt nun nicht mehr in ihren Händen.

Die virtuelle Maschine in den unterschiedlichsten Hardwareausprägungen und -größen kann einfach und bequem zusammengestellt werden. Aber auch unterschiedliche Laufzeitumgebungen für beispielsweise fertige Java Enterprise, JavaScript und ähnliche Anwendungen werden angeboten. In ihnen muss die Anwendung nur noch deployt werden. Sollten die schon existierenden Umgebungen den Ansprüchen nicht genügen, können auch neue Container, meistens in Form von Docker Image, als Basis hochgeladen werden.

Platform as a Service

Bei PaaS werden die häufig benötigten Komponenten wie Datenbanken, Netzwerkspeicher, Messaging und ähnliches als fertige Lösungen in der Cloud angeboten. Im Gegensatz zur vorherigen Variante muss für diese Managed Services die virtuelle Maschine nicht mehr selbst verwaltet werden. Von der relationalen Datenbank bis hin zu selbst entwickelten Datenbanken der Cloudanbieter werden alle Wünsche ohne großen Aufwand erfüllt. Wer lieber auf NoSQL setzen möchte, findet auch hier ein entsprechendes Angebot. Auch Datenspeicher in nahezu unendlichen Größen, die von überall schnell und einfach zugreifbar sind, können einfach realisiert werden.

Backend as a Service

Fast jede Anwendung steht früher oder später vor der Anforderung, eine Benutzerverwaltung einsetzen zu müssen – warum also nicht auch hier die Cloud einsetzen? BaaS bietet fertige Lösungen für verschiedene Anwendungsszenarien wie Benutzerverwaltung mit einer Single-Sign-on-Unterstützung und OAuth-Integration. Aber auch Push-Benachrichtigungen für mobile Geräte, der Schutz für die eigene Anwendung in Form von beispielsweise einer Web Application Firewall (WAF) oder Analytics können als fertige Lösungen konfiguriert werden. Verschiedene Tools wie gehostete Quellcodeverwaltung bis hin zur Build Pipeline sowie eine in der Cloud gehostete Entwicklungsumgebung (IDE) decken alle Anforderungen der Entwicklungsteams ab.

Function as a Service

FaaS ist eine weitere Variante der Cloud, die hinter dem Hypewort „Serverless“ steht. Die eigene Businesslogik wird in kleine Funktionen aufgeteilt, die dann direkt in der Cloud deployt und ausgeführt werden. Ein eigener Applikationsserver wird dann nicht mehr benötigt. Das Deployment erfolgt mit wenig Aufwand in die entsprechenden Umgebungen. Bei Bedarf werden innerhalb von Millisekunden viele parallele Ausführungen der Funktion gestartet, womit Probleme der Skalierung der Vergangenheit angehören.

Die Cloud als Allheilmittel?

Kein wochenlanges Warten auf eine beantragte Maschine, keine Downtime durch parallele Updateausspielung, Kosten fallen nur an, wenn der Service gebraucht wird, beliebige Skalierungen durch eine gefühlt endlose Zahl von Maschinen und keine Personalkosten im Hardwarebetrieb – die scheinbar endlosen Möglichkeiten der Cloud lassen die Augen von Entwickler und Management leuchten. Die Cloud als das Allheilmittel für alle Infrastrukturprobleme scheint überzeugend. Doch was ist die Schattenseite der schönen neuen Welt?

Die negativen Aspekte der Cloud sind nicht nur technischer Natur. Es werden viele Bereiche berührt, über die bei der eigenen Infrastruktur nie nachgedacht werden musste.

Zunächst erstmal das leidige Thema mit dem Datenschutz. Sicherlich möchte es niemand mehr hören, doch sobald die eigenen Daten aus dem Rechenzentrum verlagert werden, muss zumindest geprüft werden, welche datenschutzrechtlichen Aspekte betroffen sind. Die verschiedenen Cloudprovider bieten zwar meistens unterschiedliche Regionen an, in denen auch der entsprechende Service gehostet werden kann. Auf diese Weise kann sichergestellt werden, dass die Daten immer in dem Land des betreibenden Unternehmens liegen. Dennoch muss hierbei beachtet werden, dass es unter Umständen Dienste gibt, die nicht in den entsprechenden Regionen vorhanden sind und somit nicht für die eigenen Zwecke eingesetzt werden können bzw. dürfen.

Neben dem Datenschutz müssen auch immer die rechtlichen und vertraglichen Rahmenbedingungen beachtet werden, da es in der Regel nicht gewünscht ist, dass Dritte Zugriff auf die eigenen Daten haben. Vor diesem Hintergrund sollten die umfangreichen Nutzungsbedingungen der unterschiedlichen Services aufmerksam studiert werden.

Damit einher geht der nächste Punkt: der Kontrollverlust. Durch das Auslagern in die Cloud, hat die eigene IT-Abteilung nicht mehr die Herrschaft über die eingesetzten Ressourcen und Prozesse. Diese liegt nun in den Händen des Cloudbetreibers und in dessen Automatisierungslösung. Verschiedene Vorfälle zeigen, dass es in der Cloud nicht immer einfach ist, die Kontrolle zu behalten. Plötzlich sind firmeninterne Dokumente, Datenbanken und andere Ressourcen für die ganze Welt öffentlich. Ein aktuelles Beispiel ist der Datenverlust einer bekannten Hotelkette [1]. Häufig handelt es sich dabei um Konfigurationsfehler. Diese können meistens auf fehlende Schulungen zurückgeführt werden. Die eigene IT-Abteilung wird gerne nur mit minimalen Mitteln ausgebildet und soll anschließend in der Lage sein, ein hochkomplexes Cloudumfeld zu managen. Fehler und Sicherheitsprobleme sind automatisch vorprogrammiert.

Auch der cloudeigene Support bietet nicht immer eine adäquate Lösung für die vorhandenen Probleme. Häufig wird man im Stich gelassen. Zum Beispiel wird empfohlen, die Symptome mit mehr Hardware (Geld) zu lösen, anstatt das zugrunde liegende Problem herauszufinden und anzugehen [2].

Die Cloud als Kostensenker

Häufig wird bei der Einführung der Cloud eine Kostensenkung beim Personal und dem Betrieb versprochen. Aus dem oben Beschriebenen geht allerdings hervor, dass durch eine Cloudeinführung zunächst einmal höhere Kosten entstehen, z. B. durch notwendige Schulungen der eigenen Mitarbeiter.

Auch das Versprechen des Senkens der Personalkosten muss differenziert betrachtet werden. Zum einen erzeugt man durch solch eine Aussage Unruhe in den eigenen Reihen. Die gesamte Betriebsabteilung muss auf einmal um ihren Job bangen, weil z. B. Updates für die Betriebssysteme und Datenbanken automatisch eingespielt werden und sie somit überflüssig werden. Tatsächlich werden Teile des Berufs durch den Wechsel in die Cloud obsolet, da die Aufgaben dort bereits abgedeckt sind. Allerdings darf hier die Gefahr des Wissensabgangs nicht unterschätzt werden. Nur, weil die Cloud heute läuft, heißt das nicht, dass das morgen auch noch der Fall sein wird. Eigenes Administrationspersonal, das das grundlegende System kennt, selbst, wenn es in der Cloud läuft, kann goldwert sein. Auch durch den bereits erwähnten häufig schlechten Anbietersupport ist eigenes Personal, das sich in die Materie einarbeitet, sinnvoll.

Selbst die versprochene Senkung der Betriebskosten durch die Cloudeinführung ist nicht immer so einfach zu erzielen, wie erwartet. Hierzu müssen die Preismodelle genau studiert werden, da sie sich zwischen den verschiedenen Services häufig unterscheiden. Es gibt meistens ein Freikontingent oder einen kleineren Nutzungsumfang, der schnell verbraucht ist. Danach können die Kosten aber schnell in die Höhe schießen. Zum Beispiel sind virtuelle Maschinen mit kleiner Hardwareausstattung sehr günstig, durch die Verwendung größerer Maschinen können die entstehenden Kosten schnell die Kosten eines eigenen Rechenzentrums übersteigen.

Teilweise verstecken sich auch in den Standardeinstellungen schon Kostentreiber, die erst bei der Monatsabrechnung auffallen, falls diese überhaupt überprüft wird. Bei der automatischen Skalierung sollte z. B. immer mit Vorsicht gehandelt werden. Sie wird gerne standardmäßig zu großzügig erweitert. Eine falsch eingestellte Skalierung für die Datenbank, und schon läuft ein ganzes Cluster, das auch bezahlt werden muss. Die Bezahlung erfolgt natürlich pro verwendete Instanz.

Die Kosten stellen noch ein weiteres Problem dar, falls der Rahmen der hinterlegten Kreditkarte überschritten wird. Denn dann ist die eigene Software, die in der Cloud betrieben wird, innerhalb von Minuten nicht mehr erreichbar. Die Betreiber fahren Instanzen, die sie nicht berechnen können, automatisiert herunter. Niemand möchte mit seinem Produktivsystem einen Totalausfall haben, nur weil der Verfügungsrahmen erschöpft ist.

Nicht nur fehlende Geldmittel können das Produktivsystem in der Cloud lahmlegen. Immer wieder gibt es Berichte, dass Accounts (scheinbar willkürlich) gesperrt werden. Damit werden auch alle gekauften Dienstleistungen automatisch suspendiert.

Werden Dienstleistungen in der Cloud genutzt, müssen die Nutzungsbestimmungen der Cloud beachtet werden. Diese umfassen auch den Inhalt der in die Cloud geladenen Daten. Sollte hier ein Verstoß festgestellt werden, kann der Account ohne Vorwarnung gesperrt werden. Für den Käufer der Dienstleistungen haben fehlerhafte Überwachungen des Inhalts oder fehlende Sorgfalt beim Lesen von Nutzungsbestimmungen schwerwiegende Konsequenzen. Im Gegensatz dazu schaltet sich das eigene Rechenzentrum nicht automatisch aus, falls es tatsächlich mal nicht erlaubter Inhalt versehentlich auf die Server geschafft hat [3].

Komplexität wider Erwarten

Durch das komplexe Cloudumfeld entstehen noch andere Probleme, die in der eigenen Infrastruktur eher selten bzw. in einer anderen Art auftreten können. Beispielsweise sind die einzelnen Services meistens durch ein umfangreiches Rechtemanagement zu konfigurieren. Das soll ermöglichen, dass die Anwendung A, die den Service A benötigt, nicht auf den Bereich der Anwendung B im Service A zugreifen kann. Gerade bei scheinbar einfachen Aufgaben wie dem Erstellen einer Lambdafunktion kann man hier schnell mit einer Vielzahl von Problemen mit den Rechten konfrontiert sein, wenn die Lambdafunktion weitere Services benötigt. Durch die häufig riesige Auswahl entsteht zudem schnell eine Überforderung. Es geht eigentlich um einen kleinen und einfachen Service – aber welche der Umsetzungsmöglichkeiten soll nun gewählt werden?

Ist dann die Entscheidung getroffen und ein Service wurde gegen ein bestimmtes API des Cloudanbieters entwickelt, kommt es nicht selten zu Änderungen eben dieses API, die leider auch nicht immer der semantischen Versionierung folgen und somit unvorhersehbare Probleme auslösen. Häufig stellt man dann im Nachhinein fest, dass der ausgewählte Cloud-Service doch nicht (mehr) für den geplanten Zweck geeignet ist. Es bleibt einem nichts anderes übrig, als die eigene Anwendung auf einen neuen Service zu migrieren.

Ein weiteres Problem mit den Cloud-Services, gerade im Bereich der Datenbanken und anderen Managed Services, besteht darin, dass Cloudanbieter ihre eigenen Anpassungen vornehmen, damit sie besser laufen. Ein Beispiel dafür ist Elasticsearch, das bei manchen Anbietern so stark verbogen worden ist, dass wichtige Funktionalitäten zur Analyse von Problemen fehlen.

Die Anpassungen der Cloudanbieter führen zu einem weiteren Problem: Gibt es eine neue Version einer Software, die als Managed Service angeboten wird, so benötigt der Cloudanbieter einige Zeit, um seine Anpassungen an der neuen Version vorzunehmen. Es kann also einige Zeit vergehen, bis eine neue Version auch in der Cloud zur Verfügung steht [2].

Neben der schwierigen Entscheidung für einen Cloud-Service besteht noch das Problem des sogenannten Vendor Lock. Die APIs der Cloudanbieter sind häufig proprietär. Hat man seine Anwendung erst einmal gegen eine solche API entwickelt, kann sie nicht ohne Weiteres zu einem anderen Anbieter verlagert werden. Eine Migration verursacht dann wieder Kosten, die eigentlich mit der Cloud vermieden werden sollten.

Fazit

Auch wenn der letzte Abschnitt das vielleicht suggeriert, ist die Cloud nicht per se schlecht, sie muss allerdings richtig eingesetzt werden. Dazu ist es immer sinnvoll, wenn man als Unternehmen aus der Erfahrung anderer lernt, um nicht in die gleichen Fallstricke zu tappen. Bei jedem einzelnen Service sollte geprüft werden, ob, wann und in welcher Variante er in die Cloud migriert wird. Auf keinen Fall sollte man seine Anwendung nur deshalb in die Cloud bringen, weil das eben heute jeder so macht.

Stattdessen sollten gezielt Anwendungsfälle gesucht werden, in denen die Cloud eine wirklich sinnvolle Erweiterung zu der vorhandenen Infrastruktur bietet, z. B. Szenarien, in denen kurzfristig viel Rechenpower benötigt wird, die den Rest der Zeit aber brachliegt, wie etwa ein monatlicher Rechnungslauf. In jedem Fall sollte man schrittweise beginnen, die Cloud in die eigene Landschaft zu integrieren.

schulz_christian_sw.tif_fmt1.jpgChristian Schulz ist Enterprise Developer bei der OPEN KNOWLEDGE GmbH in Oldenburg. Er verfügt über mehrjährige Erfahrung als Entwickler im Enterprise- und SPA-Umfeld. Zu seinen Interessenschwerpunkten gehören API-Design und -Dokumentation via Swagger, das SPA Framework Angular sowie Continuous Integration.

In einer Serie von mehreren Artikeln werden wir die Migration einer „klassischen“ Java-Webanwendung in die AWS-Cloud beleuchten. Dieser erste Artikel behandelt zunächst mögliche Gründe für einen Wechsel, die aktuelle Architektur und eine Migration nach dem „Lift and Shift“-Ansatz. Alle weiteren Artikel optimieren die Anwendung iterativ durch den Einsatz verwalteter Dienste.

Gewichtige Argumente für die Verlagerung in die Cloud sind die Agilität und Geschwindigkeit, mit der sich Unternehmen bewegen können. Mit Cloud-Computing kann eine Vielzahl von Anwendungen nebst Servern innerhalb weniger Minuten (anstatt wie bei lokalen Rechenzentren innerhalb mehrerer Wochen) eingerichtet werden. Services in der Cloud – von Rechenleistung über Speicher und Datenbanken bis hin zu Continuous Integration, Datenanalysen, künstlicher Intelligenz und vielen mehr – sind jederzeit und sofort verfügbar. Die Zeitspanne von der Idee bis zur Implementierung wird so um ein Vielfaches verkürzt.

Der Entwicklungsprozess mit Ressourcen im eigenen Rechenzentrum greift häufig nur auf jeweils eine Umgebung für Integration, Abnahme, Vorproduktion und Produktion zu. Die Umgebungen werden dabei dauerhaft betrieben. In der Cloud ist es möglich, nach Bedarf und Notwendigkeit neue Umgebungen aufzusetzen und sie nach der Nutzung auch wieder zu entfernen. Während die durchschnittliche Auslastung eigener Rechenzentren relativ gering ist [1], ist es möglich, Parameter wie Speicherkapazität oder Prozessorleistung bei Cloudressourcen so zu variieren, dass deutlich höhere Auslastungen erreicht und dadurch Kosten gespart werden. Typischerweise sind Umgebungen in Form von Infrastructure as Code als AWS-CloudFormation- oder HashiCorp-Terraform-Templates definiert. Diese Templates können und sollten wie Quellcode für Anwendungen behandelt werden, was bedeutet, dass sie dem gleichen Entwicklungs- und Releaseprozess unterliegen und entsprechend getestet werden. Durch dieses Vorgehen – Infrastructure as Code und DevOps in Kombination mit der Cloud – wird die Agilität erhöht und die Einführungszeit für neue Produkte reduziert.

Es gibt auch viele Unternehmen, die im Zuge von Rechenzentrumskonsolidierungs- oder Rationalisierungsprojekten in die Cloud migrieren. Vorteilhaft für geringere Gesamtbetriebskosten (Total Cost of Ownership, TCO) ist das Pay-as-you-go-Modell. Anstatt vorab Kapital in Infrastrukturkomponenten zu investieren, kann die erforderliche Kapazität flexibel angefordert werden. Durch die Möglichkeit, Angebot und Nachfrage aufeinander abzustimmen, ergibt sich eine elastische, transparente Kostenbasis. Sollten sich beispielsweise die Erwartungen an ein Produkt nicht erfüllen, können die dafür bereitgestellten Ressourcen jederzeit abgezogen werden und es fallen ab diesem Zeitpunkt keine Betriebskosten mehr an. Ein weiteres Beispiel sind Entwicklungs- und Testumgebungen, die außerhalb der Arbeitszeit abgeschaltet werden können.

Warum eine Migration nach AWS?

Genau wie im eigenen Rechenzentrum ist es bei AWS möglich, ein individuelles Netzwerkdesign zu definieren: Eigene logisch isolierte Abschnitte können bereitgestellt werden. Mit den entsprechenden Rechten haben Administratoren die volle Kontrolle über die virtuelle Netzwerkumgebung, um einen sicheren und einfachen Zugriff auf Ressourcen und Anwendungen zu ermöglichen. AWS Shield ist ein verwalteter Distributed-Denial-of-Service-(DDoS-)Schutzdienst für Anwendungen, die auf der AWS-Plattform laufen. Dieser Service bietet eine permanente Überwachung und trifft automatisch Gegenmaßnahmen, um Ausfallzeiten und Latenzzeiten von Anwendungen zu minimieren. Alle AWS-Kunden profitieren automatisch von den AWS-Shield-Standard-Schutzfunktionen – ohne Aufpreis. AWS Shield Standard schützt vor den am häufigsten auftretenden DDoS-Angriffen auf Netzwerk- und Transportschicht. Wenn AWS Shield Standard in Kombination mit Amazon CloudFront und Amazon Route 53 verwendet wird, bietet das umfassenden Verfügbarkeitsschutz gegen alle bekannten Infrastrukturangriffe auf die Layer drei und vier. Die Konfiguration der einzelnen Services findet ausschließlich über APIs statt. Mit AWS CloudTrail können diese API-Kontoaktivitäten protokolliert, kontinuierlich überwacht und aufbewahrt werden. AWS CloudTrail bietet einen Ereignisverlauf der Kontoaktivitäten, einschließlich Aktionen, die über die AWS Management Console, AWS SDKs, Kommandozeilentools und andere AWS-Dienste auseführt werden.

Diese Ereignishistorie vereinfacht die Sicherheitsanalyse, die Verfolgung von Ressourcenänderungen und die Fehlerbehebung. Um auf bestimmte Ereignisse in der Historie automatisiert reagieren zu können, kann AWS CloudTrail mit Amazon CloudWatch Logs kombiniert werden, um bei bestimmten Aktivitäten CloudWatch-Alarme für Ereignisse zu versenden. Bei entsprechender Konfiguration wird beliebiger Programmcode aufgrund dieser Alarme ausgeführt.

Ein wichtiger Aspekt der Ausfallsicherheit: Mit derzeit 22 Regionen, die 69 Availability Zones (AZ) umfassen, besitzt AWS globale Präsenz, um die Verfügbarkeit zu verbessern und die Latenz zu reduzieren. Jede AZ kann mehrere Rechenzentren und bei maximaler Kapazität hunderttausende von Servern umfassen. Es handelt sich dabei um vollständig isolierte Bereiche der globalen AWS-Infrastruktur. Mit ihrer eigenen Strominfrastruktur sind die AZs physisch viele Kilometer von den anderen AZ entfernt, obwohl alle innerhalb einer Distanz von 100 Kilometern liegen. Hinzu kommen 189 Points of Presence, die rund um den Globus in unmittelbarer Nähe zum Anwender für schnelle Antwortzeiten sorgen. Kunden haben immer die volle Kontrolle darüber, wo ihre Daten gespeichert werden. Eine Übersicht ist unter [2] zu finden.

Es existieren noch viele weitere Gründe, warum Unternehmen eine Migration in die Cloud durchführen. Einige möchten so die Produktivität ihrer Mitarbeiter steigern. Üblicherweise wird Produktivität durch zwei wichtige Faktoren angetrieben: zum einen durch den Umstand, nicht auf Infrastruktur warten zu müssen, zum anderen durch den Zugriff auf die Vielfalt und die Bandbreite der mehr als 165 Services von AWS, die andernfalls selbst entwickelt, gewartet und betrieben werden müssten. Tatsächlich lassen sich nach einer Migration häufig Steigerungen der Mitarbeiterproduktivität von 30 bis 50 Prozent [3] beobachten. Services von AWS helfen Entwicklern dabei, sich stärker auf ihre Kernaufgabe fokussieren zu können: die Entwicklung von innovativen neuen Anwendungen und Services.

Des Weiteren werden Kosten für die Aktualisierung von Hardware vermieden. Ist der Lebenszyklus bestimmter Komponenten am Ende angelangt, entstehen beim notwendigen Aktualisierungszyklus häufig hohe Aufwände, da neben der Hardware oft auch Software individuell angepasst und aktualisiert werden muss.

Infrastruktur und Anwendung

In unserem Beispiel (Abb. 1) werden wir eine Three-Tier-Java-Anwendung betrachten, die in dieser oder ähnlicher Form in vielen Rechenzentren zu finden ist. Basis dieser Architektur auf Ebene der Infrastruktur ist eine Virtualisierungslösung, die beispielsweise auf Basis von VMware [4] (Kasten: „VMware Cloud on AWS“), Xen [5], KVM [6] oder ähnlich läuft.

Die Anwendung selbst wird auf verschiedenen virtuellen Servern auf Basis der Apache Tomcat Servlet Engine [7] ausgerollt. Um die Last gleichmäßig zu verteilen, wird ein Load Balancer wie F5 [8], Apache HTTP Server [9] oder NGINX [10] eingesetzt. Um die Anwendung vor unerwünschten Netzwerkzugriffen zu schützen, wird eine Firewall eingesetzt. Die Anwendung speichert ihre Daten in einem MySQL Galera Cluster [11]. Ein Redis Cluster [12] dient als Zwischenspeicher für häufig angefragte Daten, um die Datenbank zu entlasten.

moellering_kieselhorst_aws_1_1.tif_fmt1.jpgAbb. 1: Infrastruktur im eigenen Rechenzentrum

Lift and Shift

Wenn eine Organisation ihr Migrationsvorhaben schnell implementieren und skalieren möchte, ermöglicht der Lift and Shift oder auch Rehosting genannte Ansatz, viele Anwendungen in großen Migrationsszenarien zu migrieren. Die meisten Rehosting-Vorgänge können durch Tools wie AWS Server Migration Service [13] oder CloudEndure [14] automatisiert werden, häufig wird der Vorgang aber dennoch manuell durchgeführt, um zu lernen, wie das bestehende System in der Cloud aufgesetzt wird.

Die dabei gewonnenen Erkenntnisse und Fähigkeiten können dazu genutzt werden, um Anwendungen für die Cloud umzugestalten. Wenn einmal in die Cloud migriert wurde und die Anwendungen dort ausgeführt werden, können Optimierungen – agil und in iterativen Schritten – einfacher durchgeführt werden.

moellering_kieselhorst_aws_1_2.tif_fmt1.jpgAbb. 2: Infrastruktur in AWS

Eine oft gestellte Frage beim Start einer Migration ist: Wie kann ich Ressourcen, die in meinem Rechenzentrum laufen, in AWS abbilden?

Üblicherweise beginnt eine Migration mit der Netzwerkkonfiguration in einer Amazon Virtual Private Cloud (VPC). Kunden haben die volle Kontrolle über die virtuelle Netzwerkumgebung, einschließlich der Auswahl eines eigenen IP-Adressbereichs, der Erstellung von Subnetzen und der Konfiguration von Routingtabellen und Netzwerkgateways. Es können beispielsweise ein öffentlich zugängliches Subnetz für einen Webserver mit Internetzugang, und für Backend-Systeme wie Datenbanken oder Anwendungsserver ein privates Subnetz ohne Internetzugang erstellt werden. Kunden können mehrere Sicherheitsebenen nutzen (Kasten: „Sicherheit in AWS“), einschließlich Sicherheitsgruppen (Security Groups) und Netzwerkzugriffskontrolllisten (NACLs), um den Zugriff auf Amazon EC2-Instanzen in jedem Subnetz zu kontrollieren.

Eine wesentliche Eigenschaft – speziell bei Anwendungen, die nicht für die Cloud entwickelt worden sind – ist der Zustand einer Anwendung. Wenn wir uns zum Beispiel eine Java-Enterprise-Anwendung anschauen, die in einem JBoss-Server läuft, dann repliziert JGroups den Zustand bzw. die Sessions über UDP mit Hilfe von Buddy Replication an die benachbarten Knoten. Eine Möglichkeit, eine solche Anwendung in die Cloud zu migrieren, besteht darin, die Session-Informationen über JGroups mit einem Amazon S3-Bucket als Ziel über TCP zu replizieren. Wenn die Infrastruktur aber eine gewisse Größe erreicht hat, wird der Kommunikationsaufwand zu hoch und begrenzt die Performance. Eine deutlich bessere Lösung besteht darin, die Anwendung zustandslos zu implementieren und den Zustand in einem externen Service wie einem Redis Cluster abzulegen.

Für Anfragen der Browser und deren Verteilung auf die Apache HTTP Server nutzen wir den Application Load Balancer [15]. Dieser nimmt HTTP- und HTTPS-Anfragen an, terminiert TLS und reicht die Anfragen dann weiter. Der AWS Certificate Manager [16] stellt öffentliche TLS-Zertifikate für den Application Load Balancer kostenlos zur Verfügung.

In unserem Beispiel (Abb. 2) werden anstelle der bisherigen virtualisierten Maschinen nun Amazon-EC2-Instanzen eingesetzt. Amazon EC2 [17] ist eine virtuelle Compute-Umgebung. Mit APIs können Instanzen mit einer Vielzahl von Betriebssystemen gestartet, verwaltet und gestoppt werden. Amazon EC2 bietet eine große Auswahl von Instanztypen, die für unterschiedliche Anwendungsfälle optimiert sind. Instanztypen unterstützen verschiedene Kombinationen von CPU, Arbeitsspeicher, Speicher und Netzwerkkapazität. Instanztypen gibt es meist in mehreren Größen. Auf den EC2-Instanzen können beliebige Anwendungen ausgerollt werden. Netzwerkzugriffsberechtigungen müssen explizit freigeschaltet werden. Bei der Nutzung von Amazon EC2 fallen nur für die tatsächlich verbrauchten Ressourcen Kosten an, wie z. B. Instanzstunden oder Datentransfervolumen.

Am Beispiel von Amazon-EC2-Instanzen schauen wir uns exemplarisch an, wie schnell und unkompliziert Ressourcen verwendet werden können:

  1. Wir wählen ein vorkonfiguriertes Amazon Machine Image (AMI) aus, um sofort mit der Arbeit beginnen zu können. Alternativ kann ein eigenes AMI erstellt werden, in dem das Betriebssystem, Bibliotheken, Anwendungen und unter Umständen Daten enthalten sind.

  2. Wir konfigurieren Sicherheitsgruppen und den Netzwerkzugriff auf der Amazon-EC2-Instanz.

  3. Wir wählen einen passenden Instanztyp.

  4. Wir legen fest, ob EC2-Instanzen in mehreren Availability Zones ausgeführt werden sollen, statische IPs verwendet werden oder persistenter Blockspeicher benötigt wird.

Für die Verwaltung der MySQL-Datenbank für Daten der Anwendung nutzen wir den Amazon Relational Database Service (RDS) [18]. Dieser Service macht es einfach, eine relationale Datenbank in der Cloud einzurichten, zu betreiben und zu skalieren. Er stellt kosteneffiziente und skalierbare Kapazität zur Verfügung. Zeitaufwendige Administrationsaufgaben wie Hardwarebereitstellung, Datenbank-Set-up, Patching und Back-ups werden automatisiert durchgeführt. Dadurch bietet RDS die Möglichkeit, sich auf die Anwendungen zu konzentrieren. Amazon RDS ist für verschiedene Datenbankinstanztypen verfügbar – jeweils optimiert für Workloads, die mehr Arbeitsspeicher, Prozessorleistung oder I/O-Durchsatz benötigen. RDS unterstützt Amazon Aurora, PostgreSQL, MySQL, MariaDB, Oracle Database und Microsoft SQL Server. Für die Datenmigration verwenden wir den AWS Database Migration Service (DMS) [19], der auch für die Replikation zwischen Datenbanken verwendet werden kann.

Redis provisionieren wir mit Hilfe von Amazon ElastiCache [20]. Verwaltungsaufgaben wie Hardwarebereitstellung, Software-Patching, Einrichtung, Konfiguration, Überwachung, Fehlerbehebung und Back-ups werden mit Amazon ElastiCache automatisiert durchgeführt. Amazon ElastiCache for Redis basiert auf Open-Source-Redis und ist mit den Redis APIs kompatibel. Der Service verwendet das offene Redis-Datenformat zur Speicherung von Daten. Selbst geschriebene Redis-Anwendungen können ohne Codeänderungen nahtlos mit Amazon ElastiCache for Redis zusammenarbeiten.

Hilfsmittel bei der Migration

Im vorigen Abschnitt haben wir die einzelnen Services näher betrachtet, die bei der Migration als Ziel dienen, doch wie wird die eigentliche Migration durchgeführt und welche Hilfsmittel existieren?

AWS Migration Hub [21] bietet einen zentralen Ort, um den Fortschritt der Migration von Anwendungen mit verschiedenen AWS- und Partnerlösungen zu verfolgen. Migration Hub gibt darüber hinaus – ungeachtet der verwendeten Migrationstools – Auskunft über wichtige Kennzahlen und den Fortschritt einzelner Anwendungen.

Eins der integrierten Tools ist der AWS Application Discovery Service [22]. Rechenzentrumsmigrationen können tausende Workloads umfassen, die oft stark voneinander abhängig sind. Serverauslastungsdaten und die Abbildung dieser Abhängigkeiten sind daher wichtige erste Schritte im Migrationsprozess. Der AWS Application Discovery Service sammelt Daten zur Konfiguration, Nutzung und zum Verhalten der Server und stellt sie dar, um Workloads besser verstehen zu können.

Eine Alternative zum Application Discovery Service ist TSO Logic [23]. Hier werden präzise datenbasierte Empfehlungen zur richtigen Bemessung und Kostenkalkulation der Umgebungen bereitgestellt. Abbildung 3 zeigt ein Beispielszenario, bei dem die aktuellen Infrastrukturkosten (pink gefärbt) sowohl in gleicher Dimensionierung (orange gefärbt) als auch „Right Sized“ (gelb gefärbt) auf Grundlage der ermittelten Datenbasis gegenübergestellt werden. TSO Logic hilft somit, die Migrationsplanung durch einen Business Case schneller voranzubringen und berücksichtigt im Vergleich zum AWS Application Discovery Service auch Lizenzthemen (wie z. B. Bring Your Own License, BYOL).

moellering_kieselhorst_aws_1_3.tif_fmt1.jpgAbb. 3: Cost Modeler UI von TSO Logic

Fazit

Im ersten Teil unserer Artikelserie haben wir näher betrachtet, wie eine typische Lift-and-Shift-Migration aus dem eigenen Rechenzentrum nach AWS aussieht und welche Tools und Services dabei unterstützen. Hierbei steht im Vordergrund, die Flexibilität im IT-Betriebsbereich zu erhöhen und den Zugriff auf Infrastrukturressourcen zu beschleunigen und zu vereinfachen.

Für viele Anwendungen ist Lift and Shift der erste Schritt, auf den weitere Optimierungen hinsichtlich Sicherheit, Verfügbarkeit, Leistung, Betreibbarkeit und Kosten folgen. Im nächsten Artikel werden wir uns weiteren Services widmen, die das Aufsetzen und den Betrieb der Infrastruktur vereinfachen.

moellering_sascha_sw.tif_fmt1.jpgSascha Möllering arbeitet als Solutions Architect Manager bei der Amazon Web Services EMEA SARL. Seine Interessen liegen in den Bereichen Automation, Infrastructure as Code, Distributed Computing, Container, Serverless und der JVM.

kieselhorst_dennis_sw.tif_fmt1.jpgDennis Kieselhorst ist Solutions Architect bei der Amazon Web Services EMEA SARL. Er hat fünfzehn Jahre Erfahrung mit Java und verteilten, heterogenen Systemlandschaften. Dennis unterstützt verschiedene Open-Source-Projekte und ist Committer/PMC-Mitglied bei der Apache Software Foundation.

Die Einführung neuer QL-Sprachen erfreut sich momentan großer Beliebtheit. Erst im Sommer machte Amazon mit der Einführung von PartiQL einen neuen Vorstoß in diese Richtung. Bei diesem QL-Wildwuchs darf man das Original aber nicht aus den Augen verlieren. Was wurde eigentlich aus SQL? Steckt es tatsächlich noch in den 90ern fest? Natürlich nicht. Ein Update für Entwickler.

Das letzte Jahrzehnt war von neuen Datenbankkonzepten rund um das Thema NoSQL geprägt. Anfangs diente SQL dabei als Antithese, später wurde NoSQL als Backronym für „Not only SQL“ definiert. Zuletzt gab es dann eine regelrechte Explosion neuer QL-Sprachen. Viele davon erinnern nicht nur dem Namen nach, sondern auch der Syntax nach an SQL.

Das Versprechen dieser QL-Sprachen ist einfach: Die bekannte Grundstruktur erleichtert den Start, die punktuellen Erweiterungen liefern nach, was bei SQL fehlt. Dieses Versprechen hält einer genauen Prüfung aber nicht immer Stand. Denn zwischen dem SQL-92-Funktionsumfang, den viele aus Studium, Kursen oder Büchern kennen, und modernem SQL liegen Welten. So wurde das relationale Korsett von SQL zum Beispiel schon vor 20 Jahren aufgegeben.

Um diese und andere Überraschungen geht es in diesem Artikel. Um den Unterschied zwischen SQL und anderen Datenzugriffsmethoden zu verdeutlichen, teile ich den Funktionsumfang von SQL in vier Niveaustufen ein. Erst auf der letzten Stufe wird es um modernes SQL gehen. Die ersten drei Stufen handeln von älteren SQL-Funktionen, deren Nutzen im NoSQL-Jahrzehnt vielfach in Vergessenheit geraten ist – daher eine kurze Einführung.

Stufe 0: CRUD

Am Anfang jedes Datenzugriffs stehen die vier Grundoperationen Create, Read, Update und Delete – zusammen kurz CRUD. SQL stellt diese Grundoperationen über die Anweisungen Insert, Select, Update und Delete zur Verfügung. Das heißt aber nicht, dass diese Anweisungen jeweils nur eine dieser Grundoperationen umsetzen.

Der Unterschied zwischen reinem CRUD-Denken und SQL wird beim Inkrementieren eines Zählers deutlich. Im CRUD-Denken liest man dafür zuerst den aktuellen Zählerstand ein (Select), erhöht ihn dann in der Applikation, um den neuen Wert schließlich zu speichern (Update).

Denselben Effekt kann man in SQL aber auch mit einer einzelnen Update-Anweisung erreichen:

UPDATE Tabelle
  SET Spalte = Spalte + 1
WHERE ID = ?

Der Lesezugriff steckt zusammen mit der Inkrementierung im rechten Argument der Zuweisung. Dort kann man beliebig komplexe Ausdrücke verwenden – von einfachen Formeln über bedingte Ausdrücke (Case) bis hin zu Unterabfragen.

Auch Delete kann mehr als nur bestimmte Zeilen löschen. Da die Where-Klausel nicht auf Schlüsselwerte beschränkt ist, kann eine Delete-Anweisung selbst herausfinden, welche Zeilen zu löschen sind. Im folgenden Beispiel sind das jene Zeilen der Quelltabelle, deren ID in der Zieltabelle vorhanden ist:

DELETE FROM Quelltabelle
WHERE EXISTS (SELECT *
      FROM Zieltabelle
      WHERE Zieltabelle.ID = Quelltabelle.ID
             )

Nicht einmal die Insert-Anweisung ist auf das bloße Anlegen neuer Zeilen beschränkt. Durch Kombination mit Select können Daten zum Beispiel kopiert und transformiert werden:

INSERT INTO Zieltabelle
SELECT *
  FROM Quelltabelle

Verwendet man die Anweisungen Insert, Select, Update und Delete nur, um den CRUD-Zyklus über einen Schlüssel abzubilden, verwendet man zwar die Syntax von SQL, nicht aber die SQL-Philosophie. Daher habe ich diesem Muster das Niveau 0 zugewiesen – es sieht zwar aus wie SQL, ist aber eigentlich nicht SQL.

Oft entsteht das CRUD-Denken durch die Reduktion einer SQL-Datenbank auf die bloße Persistenz. Der Name JPA leistet hier einen traurigen Beitrag. Die von ORM-Tools generierten SQL-Anweisungen fallen dann auch großteils in diese Kategorie.

Stufe 1: Transaktionen

Die korrekte Verwendung von Transaktionen – selbst wenn ansonsten nur CRUD-Operationen verwendet werden – begründet bereits das nächste Niveau. Da Transaktionen gut in JPA integriert sind, kann man dieses Niveau noch leicht erreichen.

Der Grund, warum dieses Niveau dennoch nicht immer erreicht wird, liegt darin, dass der volle Funktionsumfang von Transaktionen nicht allgemein bekannt ist. Oft werden Transaktionen nur für die Alles-oder-Nichts-Logik beim Schreiben genutzt. Transaktionen ermöglichen aber auch konsistente Lesezugriffe und können vor Problemen durch Nebenläufigkeit schützen.

Zur Illustration kann wieder das Beispiel mit dem Zähler herangezogen werden. Bei der CRUD-Umsetzung werden zwei SQL-Anweisungen verwendet (Listing 1). Dabei stellt sich die Frage, was schiefgehen kann, wenn dieser Code mehrfach parallel abläuft.

Listing 1

SELECT Zaehler
  FROM Tabelle
 WHERE ID = ?;
 
-- Erhöhung des Werts
-- in der Applikation
 
UPDATE Tabelle
   SET Zaehler = ?
 WHERE ID = ?;

Ohne weitere Vorsichtsmaßnahmen kann es zu dem als Lost-Update bekannten Problem kommen. Wenn zwei Threads den Lesezugriff (Select) gleichzeitig durchführen, werden beide denselben Zählerwert erhalten. In weiterer Folge werden beide denselben (erhöhten) Wert in die Datenbank schreiben. Letztendlich wurde der Zähler in der Datenbank also nur einmal erhöht – ein Update ging scheinbar verloren.

Zur Lösung dieses und ähnlicher Probleme bietet SQL die sogenannte Transaktionsisolation an. Damit kann der Code so ablaufen, als gäbe es keine anderen Aktivitäten in der Datenbank. Voraussetzung ist lediglich das richtige Setzen der Transaktionsgrenzen und die Verwendung des richtigen Transaktionsisolationslevels.

Die Ermittlung des richtigen Transaktionsisolationslevels ist eine Wissenschaft für sich – unter anderem, weil die angebotenen Level sowie die damit verbundenen Vor- und Nachteile produktspezifisch sind. Grundsätzlich kann man aber immer mit dem stärksten Level – Serializeable – anfangen und dann punktuell dort nachbessern, wo es nötig ist. Daher sieht der SQL-Standard Serializeable auch als Voreinstellung vor. Tatsächlich verwenden aber alle gängigen SQL-Datenbanken eine schwächere Voreinstellung – vermutlich, um in der Standardkonfiguration bessere Performance zu erreichen.

Bei einer Transaktion im Level Serializeable stellt die Datenbank sicher, dass sie nur dann erfolgreich abgeschlossen werden kann (Commit), wenn Änderungen, die andere Transaktionen währenddessen durchgeführt haben, zu keinem Ergebnis führen, das bei isolierter Ausführung unmöglich gewesen wäre. Im Beispiel von Listing 1 heißt das, dass das Commit nur dann erfolgreich sein darf, wenn die gelesenen Daten (Select) bis zum Schreiben (Update) nicht geändert wurden. Dafür muss die Datenbank natürlich wissen, dass der Lesezugriff zur Transaktion gehört – die Transaktion muss also vor dem Select beginnen und darf erst nach dem Update enden.

Das von JPA angebotene pessimistische Locking verfolgt dasselbe Ziel wie die Transaktionsisolation. Pessimistisches Locking verhindert, dass die gelesenen Daten während der Transaktion verändert werden. Die dafür verwendeten expliziten Locks gehen aber mit größeren Performanceeinbußen als die Transaktionsisolation einher. Pessimistisches Locking ist daher nur dann sinnvoll, wenn die verwendete Datenbank den Transaktionsisolationslevel Serializeable nicht anbietet oder nicht richtig umsetzt (wie die Oracle-Datenbank).

Da sowohl Transaktionsisolation als auch pessimistisches Locking auf Transaktionen aufbauen, kann man damit nur Abläufe schützen, die man sinnvoll in eine Transaktion zusammenfassen kann. Das schließt insbesondere Vorgänge aus, die auf Benutzereingaben warten. Diese Lücke schließt das optimistische Locking, mit dem man die Konsistenz bei Nebenläufigkeit auch über Transaktionen hinweg sicherstellen kann. Da der Verwaltungsaufwand hierbei noch größer ist – u. a. durch eine Versionsspalte – sollte das optimistische Locking die letzte Wahl sein.

In der Praxis ist oft das Gegenteil der Fall. Da das optimistische Locking völlig ohne Verständnis von Transaktionen und Transaktionsisolation funktioniert, ist es oft die erste Wahl. Das ist ein weiteres Symptom der Reduktion einer SQL-Datenbank auf die Persistenz.

Stufe 2: SQL-92

Niveaustufe 2 ist dadurch gekennzeichnet, dass die aus dem alten SQL-92-Standard bekannte Funktionalität sinnvoll genutzt wird. An dieser Stelle wird es mit der Toolunterstützung schwierig – viele Funktionen stehen nur noch über QL-Sprachen zur Verfügung (JPQL, HQL, SQL). Da diese Sprachen in ORM-fokussierten Architekturen oft verpönt, manchmal sogar verboten sind, trifft man dieses Niveau nur noch selten an.

Das bekannteste Symptom, das darauf hindeutet, dass Niveau 2 nicht erreicht wird, ist das N+1-Selects-Problem. Dieses Problem tritt auf, wenn für jede Zeile eines Ergebnisses eine neue Abfrage abgesetzt wird – in Summe dann also N+1 Abfragen, wenn die erste Abfrage N Zeilen liefert. Dieses Problem führt stets zu schlechter Performance – unter anderem wegen der Anhäufung von Latenzen. Aus SQL-Sicht sollte man daher alle für einen Verarbeitungsschritt benötigten Daten auf einmal beschaffen. Dafür muss man aber schon bei der ersten Abfrage wissen, welche Abfragen dann für jede Zeile ausgeführt werden. Diese Information ist in objektorientiertem Java-Code aber nicht so ohne Weiteres ersichtlich. Die von JPA dafür angebotenen Features Eager Fetching oder @NamedEntityGraph helfen zwar, haben aber auch ihre Grenzen.

Diese Grenzen werden bei Schreiboperationen noch deutlicher. So kann keines der Insert-, Update- und Delete-Beispiele nur mit O/R Mapping sinnvoll umgesetzt werden. Greift man auf JPQL zurück, gibt es zwar für die Update- und Delete-Beispiele eine Entsprechung, nicht aber für das Insert-Beispiel – dafür bleibt dann nur HQL oder eben SQL.

Stufe 3: SQL aus dem 21. Jahrhundert

Das vierte und letzte SQL-Niveau ist SQL. Eigentlich sollte damit klar sein, dass aktuelles SQL gemeint ist. Oft wird mit dem Begriff SQL aber nur der Funktionsumfang von SQL-92 assoziiert. Daher muss ich etwas deutlicher werden. Gemeint ist das „Post-SQL-92“-SQL. Anders gesagt, alles, was ab dem SQL:1999-Standard eingeführt wurde. Ich verwende dafür auch die Begriffe „modernes SQL“ oder eben „SQL aus dem 21. Jahrhundert“. Diese Abgrenzung zur vorherigen Stufe mag willkürlich erscheinen, es gibt jedoch mehrere Gründe, die für diese Trennung sprechen:

  • Mit dem Standard von 1999 endete die rein relationale Ära von SQL.

  • Die neuen SQL-Funktionen sind nicht allgemein bekannt.

  • Sprachen wie JPQL oder HQL unterstützen diese Funktionen nicht.

Im Folgenden stelle ich einige dieser neuen SQL-Funktionen anhand typischer Anwendungsfälle vor.

Rekursive Abfragen für Graphen

Eine der Einschränkungen von SQL-92 betraf das Bearbeiten von Graphen im Sinne der Graphentheorie. So war es mit SQL-92 zum Beispiel nicht möglich, den kürzesten Pfad zwischen zwei Knoten in einem Graphen zu ermitteln. Diese Lücke wurde mit rekursiven Abfragen geschlossen.

Der Einfachheit halber möchte ich die Idee der rekursiven Abfragen am folgenden Beispiel einer Hierarchie erklären:

CREATE TABLE Hierarchie (
  ID       INTEGER NOT NULL PRIMARY KEY,
  ParentID INTEGER,
  …
)

Dafür sollen von einem bekannten Knoten ausgehend alle darunterliegenden Knoten abgerufen werden. Das könnte zum Beispiel die Mitarbeiterliste einer Organisationseinheit oder die Stückliste einer Baugruppe sein. Mit dem Union-All-Operator hatte SQL-92 nur eine Teillösung geliefert:

SELECT *
  FROM Hierarchie
WHERE ID = 42
UNION ALL
SELECT *
  FROM Hierarchie
WHERE ParentID = 42

Die Abfrage liefert sowohl den Ausgangsknoten (ID=42) als auch alle Knoten, deren ParentID denselben Wert haben – das sind alle, die hierarchisch unmittelbar darunter liegen. Um auch die dritte Ebene auszugeben, könnte man zwar grundsätzlich einen weiteren Union-All-Zweig verwenden, nur ist der Suchwert in der Where-Klausel dann nicht mehr trivial. Schließlich braucht man dort die ID-Werte der zweiten Ebene.

Dieses Problem löst man heutzutage mit einer rekursiven Abfrage. Dadurch kann die Where-Klausel indirekt auf das eigene Ergebnis – und damit auf die ID-Werte der vorherigen Ebene – zugreifen. Die Syntax dafür ist erstaunlich kurz. Listing 2 zeigt die vollständige Abfrage, die nun für beliebig tiefe Hierarchien funktioniert. Im Kern steht nach wie vor die Union-All-Abfrage. Die wesentliche Änderung ist, dass der zweite Zweig nicht mehr fest kodiert nach ParentID = 42 sucht, sondern nach dem Wert der Spalte ID in der Tabelle cte (Listing 2: cte.ID). Die Tabelle cte ist jedoch das Ergebnis der Abfrage selbst. Das ergibt sich daraus, dass die Abfrage in eine With-Klausel gekapselt ist, und dem Ergebnis der Abfrage somit der Name cte zugewiesen wird (Listing 2: WITH RECURSIVE cte).

Listing 2

WITH RECURISVE cte (ID, ParentID)
AS (
 
SELECT *
  FROM Hierarchie
 WHERE ID = 42
UNION ALL
SELECT *
  FROM Hierarchie h
  JOIN cte ON h.ParentID = cte.ID
 
)
SELECT *
  FROM cte 

Zur Laufzeit beginnt die Datenbank mit dem ersten Union-All-Zweig. Das Ergebnis wird unmittelbar Teil der Tabelle cte und ist damit überall dort sichtbar, wo auf diese Tabelle zugegriffen wird. Das passiert zum einen in der Hauptabfrage ganz am Ende des Listings, aber eben auch im zweiten Union-All-Zweig. Zu diesem Zeitpunkt steht die Referenz cte.ID nur für eine einzelne Zeile mit dem ID-Wert 42. Der zweite Union-All-Zweig liefert daraufhin alle Zeilen der zweiten Ebene – wie beim SQL-92-Beispiel zuvor. Diese Zeilen gehören nun aber auch zur Tabelle cte, sodass die Referenz cte.ID de facto für alle ID-Werte der ersten beiden Ebenen steht. Damit das Ergebnis des zweiten Union-All-Zweigs wieder stimmt, muss die Datenbank also noch die Zeilen der dritten Ebene hinzufügen. Das Ganze geht so lange weiter, bis keine neuen Zeilen mehr gefunden werden.

Rekursionen sind auf den ersten Blick oft verwirrend – nicht nur in SQL. Das sollte aber niemanden davon abhalten, sich näher damit zu beschäftigten (Abb. 1).

winand_sql_1.tif_fmt1.jpgAbb. 1: Verfügbarkeit von rekursiven Abfragen

Window-Funktionen gegen Self-Joins

Eine andere Einschränkung von SQL-92 war, dass die Verwendung von Aggregatfunktionen (sum, count, …) an die Zusammenfassung von Zeilen im Rahmen einer Gruppierung gebunden war. Die Abgrenzung zwischen den jeweils zusammengefassten Zeilen legt dabei gleichzeitig fest, über welche Zeilen die Aggregatfunktion anwendet wird. Mit Window-Funktionen wurde diese Verbindung aufgehoben. Jetzt kann man Aggregatfunktionen ohne Gruppierung nutzen, wenn man stattdessen direkt bei der Aggregatfunktion angibt, auf welche Zeilen sie angewendet wird (Abb. 2).

Ein gutes Illustrationsbeispiel ist die Ermittlung einer fortlaufenden Summe. Dabei soll für einen Kontoauszug eine Transaktionsliste ausgegeben werden, bei der auch der aktuelle Kontostand nach jeder Transaktion aufscheint. Der Kontostand ergibt sich dabei aus der Summe der vorangegangenen und der aktuellen Transaktion.

Listing 3 setzt diese Anforderung in alter SQL-92-Manier um. Erschwerend ist dabei, dass man zwar eine Aggregatfunktion braucht, die Transaktionen aber einzeln ausgegeben muss. Eine Gruppierung in der Hauptabfrage ist also nicht ohne Weiteres möglich. Deswegen verlagert das Beispiel die Aggregierung in eine Unterabfrage. Dort kann die Where-Klausel dann frei auswählen, welche Zeilen aufsummiert werden sollen. Im Beispiel sind das die angelaufenen Transaktionen (i.TxID <= o.TxID) zum aktuellen Konto (i.KontoNr = o.KontoNr).

Listing 3

SELECT KontoNr, TxID, Umsatz
  , (SELECT SUM(i.Umsatz)
      FROM Transaktionen i
      WHERE i.KontoNr = o.KontoNr
        AND i.TxID   <= o.TxID
    ) Kontostand
  FROM Transaktionen o
ORDER BY KontoNr, TxID 

Window-Funktionen bieten eine völlig andere Art auszuwählen, welche Zeilen einer Aggregatfunktion zugeführt werden. Dafür verwendet man, wie in Listing 4 dargestellt, die Over-Klausel unmittelbar hinter dem Aufruf der Aggregatfunktion.

Listing 4

SELECT KontoNr, TxID, Umsatz
  , SUM(Umsatz)
    OVER(PARTITION BY KontoNr
             ORDER BY TxID
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) Kontostand
  FROM Transaktionen
ORDER BY KontoNr, TxID

Um die Wirkung der Over-Klausel zu verstehen, kann man sich vorstellen, dass sie bei jeder Zeile einzeln angewendet wird, und als Ergebnis die ausgewählten Zeilen an die vorangestellte Aggregatfunktion liefert. Im Beispiel erfolgt die Auswahl der Zeilen mit drei Subklauseln: Partition by, Order by und dem sogenannten Framing (im Beispiel Between).

Partition by KontoNr begrenzt die Zeilen auf jene, die in der Spalte KontoNr denselben Wert wie die aktuelle Zeile haben. Damit ist jegliche Vermischung von Transaktionen zu verschiedenen Konten ausgeschlossen. Der nächste Schritt ist, dafür zu sorgen, dass nur die Zeilen bis zur aktuellen Transaktion herangezogen werden. Dafür wird mit Order by zuerst eine passende Reihenfolge festgelegt und mit Rows between unbounded preceding and current row auf den Bereich zwischen der ersten Zeile (unbounded preceding) und der aktuellen Zeile (current row) eingegrenzt. Diese Zeilen werden dann an die Aggregatfunktion übergeben – im Beispiel Sum.

Die Flexibilität der Over-Klausel kann nicht überschätzt werden. So kann man mit Rows between 3 preceding and 3 following jeweils drei Zeilen vor und nach der aktuellen Zeile auswählen und dann mit der Funktion Avg den gleitenden Mittelwert berechnen. Die Grundfunktion der Over-Klausel besteht darin, aus einer Zeile heraus auf andere Zeilen desselben Ergebnisses zuzugreifen. Das macht nicht nur bei Aggregatfunktionen Sinn. Zur Ermittlung der Position innerhalb des aktuellen Ergebnisses gibt es zum Beispiel auch die Funktionen Row_number, Rank und Dense_rank. Mit Funktionen wie Lag oder Lead kann man sogar direkt auf andere Zeilen zugreifen.

Um mit SQL-92 Ähnliches zu erreichen, muss man mehrmals auf dieselbe Tabelle zugreifen (Listing 3). Dieser Kunstgriff – der Self-Join im weitesten Sinn – macht zwar viele Anforderungen mit SQL-92 lösbar, ist aber weder intuitiv noch performant. Daher ist ein Self-Join in vielen Fällen keine praktikable, sondern lediglich eine akademische Lösung. Dieser Tatsache wird seit SQL:1999 verstärkt Rechnung getragen. Heutzutage bietet SQL für fast jeden Self-Join eine bessere Alternative. Window-Funktionen spielen dabei eine wichtige Rolle (Abb. 2).

winand_sql_2.tif_fmt1.jpgAbb. 2: Verfügbarkeit von Window-Funktionen

Zeitreisen für die Nachvollziehbarkeit

SQL selbst erfordert keine höhere Normalisierung. Vielleicht liegt es auch daran, dass SQL für manche Probleme, die mit höherer Normalisierung einhergehen, sehr lange keine expliziten Lösungen hatte. Ein solches Problem ist das Führen von Historien. Es ergibt sich erst durch die Aufteilung logisch zusammenhängender Daten auf mehrere Tabellen.

Das Problem tritt zum Beispiel bei der Trennung von Kundenstammdaten und Bestellungen auf – z. B. in einem Webshop. Ein einfaches Update auf den Kundenstamm führt einerseits dazu, dass die alten Kundendaten verloren gehen, andererseits erscheint es so, als wären die neuen Stammdaten schon bei früheren Bestellungen aktiv gewesen.

Will man den Kundenstamm einer Versionierung unterziehen, war man mit SQL-92 völlig auf sich gestellt. Lediglich die Einführung von Triggern hat später etwas geholfen. Wie im Folgenden gezeigt, kann man mit modernem SQL die Versionierung einer Tabelle ganz einfach an die Datenbank abtreten:

CREATE TABLE … (
  …, -- normale Spalten
 
  valid_from TIMESTAMP(9) GENERATED ALWAYS AS ROW START NOT NULL,
  valid_till TIMESTAMP(9) GENERATED ALWAYS AS ROW END   NOT NULL,
 
  PERIOD FOR SYSTEM_TIME (valid_from, valid_till)
) WITH SYSTEM VERSIONING

Dafür braucht man zwei Spalten, die den zeitlichen (temporalen) Gültigkeitsbereich jeder Zeile speichern. Aufgrund der Generated-always-as-Klauseln kümmert sich die Datenbank um die Befüllung dieser Spalten. Danach werden sie in eine Periode zusammengefasst, und die sogenannte Systemversionierung mit der Klausel With system versioning aktiviert. Damit erledigt die Datenbank die Versionierung dieser Tabelle völlig automatisch und für die Applikation transparent.

Bestehende Anweisungen müssen also nicht geändert werden – die Datenbank verhält sich trotzdem anders. Bei einer Insert-Anweisung wird zum Beispiel die aktuelle Systemzeit automatisch als Gültigkeitsbeginn der neuen Zeilen verwendet. Der Endzeitpunkt liegt in der fernen Zukunft – z. B. im Jahr 9999. Da der Sinn der Historisierung darin liegt, später noch auf alte Daten zugreifen zu können, setzt eine Delete-Anweisung lediglich das Enddatum auf den Zeitpunkt des Löschens. Auch die Update-Anweisung kümmert sich um die richtige Speicherung der alten Daten. Konkret heißt das, dass bei den entsprechenden Zeilen lediglich der Endzeitpunkt gesetzt wird, damit die alten Daten unverändert erhalten bleiben. Zusätzlich werden die Daten in neue Zeilen übertragen, entsprechend der Update-Anweisung geändert und mit dem passenden Gültigkeitsbereich gespeichert.

Die Transparenz der Systemversionierung betrifft natürlich auch Abfragen. Bestehende Abfragen können unverändert weiterverwendet werden, da sie nur aktuelle Zeilen liefern. Um auf alte Daten zuzugreifen, muss man ausdrücklich danach fragen. Für solche „Zeitreiseabfragen“ wurde die From-Klausel wie folgt erweitert:

SELECT *
  FROM … FOR SYSTEM_TIME AS OF TIMESTAMP'2019-01-01'

Da die Systemversionierung immer den Zeitpunkt der Transaktion verwendet, wird damit lediglich festgehalten, wann Änderungen in der Datenbank durchgeführt wurden. Oft muss man aber erfassen, wann Änderungen in der realen Welt eingetreten sind (oder eintreten werden). Von einer Namensänderung erfahren Geschäftspartner oft erst Wochen später – z. B. nach der Hochzeitsreise. In diesem Fall muss man eine Änderung rückwirkend erfassen. SQL bietet mit den sogenannten Applikationszeitperioden auch dafür Unterstützung an. Dabei wird die Update- und Delete-Syntax erweitert, damit man den Änderungszeitpunkt an die Datenbank übergeben kann. Er kann auch in der Zukunft liegen, um z. B. Preisänderungen ab nächstem Monat umzusetzen. Schlussendlich erlaubt SQL auch die Verwendung zweier Gültigkeitsperioden in einer Tabelle – sogenannte bitemporale Tabellen. Damit kann man sowohl abbilden, wie sich die Realität geändert hat – z. B. wann ein Name geändert wurde –, als auch festhalten, wann diese Änderung in der Datenbank erfasst wurde (Abb. 3).

winand_sql_3.tif_fmt1.jpgAbb. 3: Verfügbarkeit von Systemversionierung

JSON als Schnittstelle

Auch bei den Datentypen hat sich SQL weiterentwickelt – nämlich über das klassische Verständnis des relationalen Modells hinaus. Während SQL-92 noch auf „atomare“ Datentypen beschränkt war (Strings, Zahlen, Zeitangaben), wird seit SQL:1999 eine modernere Interpretation des relationalen Modells verfolgt. Damit wurden Arrays, Objekttypen, aber auch das Speichern und Verarbeiten von XML und JSON-Dokumenten möglich. Durch die Konvertierungsfunktionen, die damit einhergegangen sind, kann man solche Daten nicht nur speichern, sondern auch als Schnittstelle zur Applikation nutzen.

Als Beispiel möchte ich eine SQL/JSON-Funktion vorstellen, die ein JSON-Dokument in eine klassische Tabelle überführt. Dafür muss man zuerst die sogenannte SQL/JSON-Path-Sprache kennenlernen. Diese Sprache erfüllt für JSON jene Funktionen, die XPath für XML und CSS-Selektoren für HTML erfüllen. Sie ermöglicht also den Zugriff auf Teile eines JSON-Dokuments. Die JSON-Path-Ausdrücke, die im folgenden Beispiel (Listing 5 und 6) vorkommen, werden gleich erklärt.

Listing 5

[ 
  {
    "id": 42,
    "a1": "foo"
  },
  { 
    "id": 43,
    "a1": "bar"
  }
]

id

A1

42

foo

43

bar

Tabelle 1: JSON-Daten als Tabelle

Listing 6 zeigt den vollständigen Code zur Transformation des JSON-Dokuments aus Listing 5 in eine Tabelle (Tabelle 1). Die Transformation selbst wird von der Funktion Json_table durchgeführt. Als erstes Argument erwartet diese Funktion das JSON-Dokument – im Beispiel wird es durch einen Bind-Parameter (?) von der Applikation übergeben. Danach folgt ein JSON-Path-Ausdruck, der festlegt, wie viele Zeilen aus der Transformation hervorgehen. Der Ausdruck im Beispiel ('$[*]') bedeutet, dass ausgehend vom aktuellen Kontext ($) ein Array erwartet wird ([…]) und für jedes Element (*) eine Zeile ausgegeben werden soll. Daraus resultiert, dass aus dem JSON-Dokument in Listing 5 eine Tabelle mit zwei Zeilen wird. Bleibt nur noch die Definition der Spalten mit der Columns-Klausel. Dort wird jeder Spalte ein Name und Typ zugewiesen, und mit einem JSON-Path-Ausdruck ein Wert aus dem JSON-Dokument entnommen. Bei den JSON-Path-Ausdrücken in der Columns-Klausel ist der Kontext ($) das Element, das gerade in eine Zeile überführt wird. Daher genügt '$.id' und '$.a1', um die entsprechenden Attribute auszuwählen.

Listing 6

SELECT *
  FROM JSON_TABLE (
    ?
  , '$[*]'
    COLUMNS (
      Id INT        PATH '$.id'
    , A1 VARCHAR(…) PATH '$.a1'
    )
  ) r

Insgesamt macht das Beispiel etwas relativ Sinnloses: es erhält über den Parameter ? von der Applikation ein JSON-Dokument, transformiert es in eine tabellarische Form und retourniert diese Tabelle an die Applikation. Interessant wird das Beispiel erst, wenn man Insert into … voranstellt. Damit kann man ein JSON-Dokument an die Datenbank senden, dort deklarativ in eine tabellarische Form überführen und die so entstandenen Zeilen gleich in eine Tabelle schreiben.

In diesem Beispiel wird JSON also nur als Schnittstelle zur Applikation verwendet. Alternativ kann man die JSON-Dokumente natürlich auch als solche in einer Tabelle speichern. SQL selbst ist in dieser Hinsicht unvoreingenommen (Abb. 4).

winand_sql_4.tif_fmt1.jpgAbb. 4: Verfügbarkeit von JSON_TABLE

Fazit

Wenn die Beispiele aus diesem Artikel eines zeigen, dann, dass SQL-Datenbanken nicht nur passive Datenspeicher sind. SQL ist eine Sprache zum bedeutungsvollen Umgang mit Daten. Bedeutungsvoll heißt, dass es Datentypen zur richtigen Handhabung von Zeitangaben gibt – mit oder ohne Zeitzone. Bedeutungsvoll heißt auch, dass logisch zusammengehörige Zugriffe in Transaktionen zusammengefasst werden können, um einen konsistenten Blick auf die Daten zu gewähren. Der bedeutungsvolle Umgang umfasst die Transformation zwischen verschiedenen Darstellungsformen (relational, als Dokument usw.) und die Ableitung neuer Informationen aus den Grunddaten, wie das Bilden einer fortlaufenden Summe.

Daher ist SQL deutlich mehr als nur eine Abfragesprache. SQL ist auch eine Transformationssprache. Das war es schon immer. Neu ist nur, dass SQL nicht mehr auf die rigiden Konzepte seines relationalen Ursprungs beschränkt ist. Das ist eine tiefgreifende Änderung, die durchaus einen zweiten Blick auf SQL rechtfertigt.

winand_markus_sw.tif_fmt1.jpgMarkus Winand ist als SQL Renaissance Ambassador auf der Mission, Entwickler auf die Evolution von SQL im 21. Jahrhundert aufmerksam zu machen. Er arbeitet derzeit an einem Buch zu diesem Thema, das während seines Entstehens online gelesen werden kann.

Web