Was der Wandel für die Java-Community bedeutet

Technologien für den Schritt nach Agilität

Eberhard Wolff
© shutterstock.com/SOMMAI

Durch den „Wirtschaftsdarwinismus“ ändern sich die Herausforderungen, denen sich die IT stellen muss. Aber welche konkreten Technologien sind relevant? Und wie beeinflussen die Herausforderungen den typischen Java-Entwickler?

Das Java-Ökosystem ist vor allem im Enterprise-Bereich führend und das auch schon sehr lange. Viele Technologien können auf eine ganze Dekade Geschichte zurückblicken – aber das Umfeld ändert sich. Das wird sicher an Java nicht spurlos vorbei gehen. Oft ist aber kein radikaler Wandel, sondern eine Ergänzung des „üblichen“ Technologiestacks ausreichend. Dieser Artikel gibt einen Überblick über die Technologien und dient als Hinweis auf die verschiedenen Trends. Er geht also in die Breite – und nicht so sehr in die Tiefe bei den einzelnen Technologien.

Continuous Delivery

Agile Prozesse versprechen, dass Features in Software wesentlich schneller umgesetzt werden können. Allerdings ist das Ergebnis eines agilen Prozesses „nur“ auslieferbare Software – ob sie dann wirklich in Produktion laufen wird, steht auf einem anderen Blatt. Genau dort setzt Continuous Deployment an: Das Ziel ist es, Software regelmäßig in Produktion zu bringen. Dazu dient eine Deployment-Pipeline, in der die Software verschiedene Stufen durchläuft, bis sie schließlich in Produktion landet (Abb. 1). Diese Pipeline soll möglichst schnell und zuverlässig durchlaufen werden – und darauf zielen die Technologien für die Pipeline auch ab.

Abb. 1: Deployment-Pipeline

Der erste Schritt in der Pipeline ist der „Commit Stage“. Er wird angestoßen, wenn ein Entwickler Code in die Versionskontrolle eincheckt. Konkret wird in diesem Schritt der Code kompiliert, Unit Tests durchgeführt und gegebenenfalls eine statische Codeanalyse durchgeführt. Werkzeuge für diesen Schritt sind: Continuous-Integration-Server wie Jenkins [2] oder Hudson [3] und Werkzeuge für statische Codeanalyse wie SonarQube [4]. Diese Technologien haben sich weitestgehend durchgesetzt. Also ändert sich in diesem Bereich für Java-Entwickler noch nicht so viel.
Der nächste Schritt sind Akzeptanztests. Im Gegensatz zu Unit Tests werden in diesem Schritt die fachlichen Anforderungen getestet. Klassisch werden dazu oft Testpläne aufgestellt und die einzelnen Anforderungen manuell abgetestet. Oft werden die Tests schon sehr detailliert beispielsweise in Excel-Checklisten beschrieben. Tests so genau zu beschreiben, ist aufwändig. Die Durchführung ist dann nur noch Routine. Aber solche Routineaufgaben sollten automatisiert werden. Dazu sind die Testpläne wegen der aufwändigen Erstellung oft schon detailliert genug – aber noch nicht so formal, dass sie automatisch ausgeführt werden können.
Grundsätzlich gibt es für diesen letzten Schritt unterschiedliche Werkzeuge: Die Tests können auf der Ebene der Oberfläche ansetzten. Dann kann das Team beispielsweise Selenium [5] einsetzen, um die Oberfläche automatisiert zu testen. Dann muss der Testplan nur einmal „aufgenommen“ werden, um dann automatisiert „abgespielt“ zu werden.
Alternativ können Werkzeuge für BDD (Behaviour-driven Design) genutzt werden. Dabei werden Anforderungen textuell beschrieben und automatisiert getestet. Sie entsprechen einem vorgegebenen Format, das durch natürliche Sprache auch für Endanwender verständlich ist und durch das Framework abgetestet werden kann (Listing 1). BDD-Tools mit Java-Unterstützung sind beispielsweise JBehave [6] oder Thucydides [7]. Sie interpretieren die textuelle Beschreibung des Tests und rufen dann die Logik des Systems auf, um zu testen, ob es sich wie erwartet verhält. Gerade im Bereich BDD gibt es aber auch zahlreiche alternative Werkzeuge.

Gegeben ist, dass ein Kunde einen Sparplan abgeschlossen hat
und er hat 3 Monate 100 € eingezahlt
wenn er den Sparplan auflöst
sollte er 306 € bekommen

Es schließen sich dann automatisierte Kapazitättests an: Sie messen die Performance der Anwendung und können über das GUI erfolgen. Eine Alternative ist es, ein eigenes API zu implementieren, mit dem Szenarien direkt ausgeführt werden. Für beide Fälle können Werkzeuge wie Grinder [8] oder JMeter [9] genutzt werden.
Schließlich wird die Anwendung manuell getestet. In diesem Schritt ist es aber nur noch notwendig, neue Features zu testen, für die keine automatisierten Tests vorliegen. So können sich die Tester darauf fokussieren, explorativ nach Fehlern zu suchen, weil die Routinetätigkeiten durch Automatismen erledigt sind. Das bedeutet, dass nicht alle Tests sofort automatisiert werden. Wenn Tester immer wieder dieselben Tests manuell ausführen, ist eine Automatisierung natürlich sehr sinnvoll.

Am Ende steht das Release. Das ist aber nicht das erste Mal, dass die Software installiert wird: Schon für die Akzeptanztests und die automatisierten Kapazitätstests sind produktionsnahe Umgebungen notwendig – sodass auch Deployment-Prozesse automatisiert und optimiert werden müssen. Dazu gibt es unterschiedliche Möglichkeiten: DSLs wie Chef [10] oder Puppet [11] erlauben es, den gewünschten Zustand eines Servers zu definieren. Beispielsweise kann definiert werden, welche Linux-Packages installiert sein müssen oder wie bestimmte Konfigurationsdateien aussehen sollen. Ergeben sich Unterschiede zwischen dem aktuellen Zustand der Servern und der definierten Konfiguration, so können die Werkzeuge diese Unterschiede beheben. Auf diese Weise können Server nicht nur einmal installiert, sondern auch bei Konfigurationsänderungen aktualisiert werden. Die Konfiguration kann als DSL-Skript auch zusammen mit der eigentlichen Software versioniert werden. So kann sichergestellt werden, dass die Konfiguration auch immer zur Software passt.
Da verschiedene Releases parallel in verschiedenen Testphasen sein können, ist es nicht ausreichend, einfach nur einen Server auf das jeweils aktuelle Release zu bringen – es müssen ausreichend Server mit den entsprechenden Releases zur Verfügung stehen. Ideal wird das durch eine virtualisierte Umgebung unterstützt, in der neue virtuelle Server ohne größeren Aufwand erstellt werden. In diesem Bereich hat VMware mit seinen Produkten eine starke Marktdurchdringung gerade im Enterprise-Sektor. Diese Produkte bieten auch schon länger die Möglichkeit, neue virtuelle Rechner durch Aufrufe an entsprechende APIs zu starten. Dadurch ist es recht einfach möglich, in einer Continuous-Delivery-Pipeline automatisch neue Maschinen zu starten und die entsprechende Software auf ihnen zu installieren. In letzter Zeit werden aber auch in diesem Bereich Open-Source-Lösungen immer populärer wie beispielsweise OpenStack [12]. Hinter diesem Projekt haben sich mittlerweile zahlreiche Firmen versammelt, die die Technologie unterstützen.
Eine andere mögliche Basis für Continuous Delivery sind PaaS-Cloud-Dienste: Sie bieten eine Ablaufumgebung an, in der nur noch die Anwendungen installiert werden müssen und es können typischerweise auch beliebig viele Umgebungen mit unterschiedlichen Versionen der Anwendung installiert werden. Technologien im Bereich PaaS für Java sind bereits in einer Artikelserie im Java Magazin ausführlich dargestellt worden. Ein Tool, das im Rahmen der Serie noch nicht vorgestellt wurde, ist Docker – ein sehr einfaches PaaS, das ohne Weiteres auch auf eigenen Rechnern installiert werden kann [13]. Während also bei Chef oder Puppet immer noch die Server komplett ab Betriebssystem aufgebaut werden, stellt ein PaaS die Ablaufumgebung bereit, die sonst manuell installiert werden müsste. Die Ablaufumgebungen sind also standardisiert und werden nicht individuell erzeugt, wie dies mit Chef oder Puppet der Fall wäre.
Oft wird die Automatisierung des Deployments als zentraler Bestandteil von Continuous Delivery wahrgenommen – aber in Wirklichkeit ist sie nur eine Voraussetzung für andere Techniken im Bereich des Testings. Erst durch das Zusammenspiel der verschiedenen Praktiken können Anwendungen wesentlich schneller und zuverlässiger in Produktion gebracht werden. Die Automatisierung allein ist aber auch schon wichtig: So können Fehler beim Aufsetzen der Umgebungen vermieden und die Umgebungen exakt passend zum jeweiligen Softwarestand aufgebaut werden. Wenn also nur die Automatisierung allein umgesetzt wird, kann das schon erhebliche Vorteile für die Produktivität der Teams haben.

Aufmacherbild: businessman pushing a touch screen interface von Shutterstock / Urheberrecht: SOMMAI

[ header = Seite 2: DevOps = Entwicklung + Betrieb ]

DevOps = Entwicklung + Betrieb

Klassisch sind IT-Organisationen oft in den Betrieb und die Entwicklung aufgeteilt. Diese Abteilungen haben auch unterschiedliche Ziele: Der Betrieb ist um Stabilität bemüht, während die Entwicklung neue Features in Produktion bringen will – aber am Ende müssen beide Abteilungen gemeinsam für die Nutzer und die Organisation möglichst optimale IT-Dienste anbieten.
Mindestens bei der Installation neuer Releases müssen die Abteilungen eng zusammenarbeiten. Continous Delivery ändert gerade in diesem Bereich den technologischen Ansatz. DevOps ist eine organisatorische Reaktion. Der Name ist schon Programm: Die Bereich Entwicklung (Development) und Betrieb (Operations) wachsen zusammen. Dafür gibt es unterschiedliche Gründe: Als Konsequenz aus Continuous Delivery werden viele klassische Betriebsprozesse anders. Es geht nicht mehr darum, einen Server zu installieren, sondern die Installation der Anwendungen und Infrastrukturen muss automatisiert werden. Dadurch werden klassische Betriebsaufgaben eher zu Softwareentwicklung – eben der Entwicklung von Software für die automatisierte Installation von Anwendungen. Betriebler wissen am besten, welche Anforderungen solche System erfüllen müssen. Letztendlich entstehen so gemischte Teams – wie sie bei agilen Teams auch schon Gang und Gäbe sind. Nur bleiben agile Teams bei den Skills auf Entwicklung und Anforderungsanalyse beschränkt, während DevOps auch den Betrieb in Betracht zieht.
Durch DevOps ergeben sich auf technischer Ebene weitere Potenziale. So kann die Entwicklung beispielsweise beim Monitoring zusätzliche Werte liefern, um so mehr über den Zustand der Anwendung zu erfahren. Oder die Entwicklung kann in die Software Schalter einbauen, um bestimmte Features zu deaktivieren. Dadurch kann bei Problemen in Produktion ein Teil der Software deaktiviert werden, statt das gesamte System ausfallen zu lassen. Solche Ansätze wären zwar auch ohne gemischte Teams mögliche, aber oft entstehen erst so die notwendigen Diskussionen und auch die Tools können dann auf dem „kurzen Dienstweg“ entstehen. Technisch können beispielsweise Klassiker wie JMX für die Integration der Software aus dem Betrieb dienen.
DevOps ist also keine Technologie – sondern ein Vorgehen. Letztendlich wird die agile Vision erweitert, möglichst schnell möglichst viel Wert zu schaffen – und zwar dadurch, dass auch der Betrieb sich an den in der Entwicklung schon dominierenden agilen Werten orientiert.

Sizing

Continuous Delivery und DevOps helfen dabei, neue Features in Produktion zu bringen. Aber auch nicht funktionale Anforderungen können mit neuen Technologien ganz anders gelöst werden. Ein Beispiel ist Sizing und Performance: Es wäre optimal, wenn die Anwendungen einfach immer gerade so viele Ressourcen bekommen, dass sie für die Anwender ausreichend performant sind. Meistens wird aber die gewünschte Performance festgelegt und ein Mengengerüst für die Anzahl der Nutzer und Daten erstellt. Anhand dessen wird dann die notwendige Hardware beschafft. Dieses Konzept hat einige Nachteile:

• Die Kosteneffizenz ist gering. Hardwareressourcen für Lastspitzen werden beschafft und vorgehalten – selbst wenn diese nur sehr selten auftauchen.
• Es kann nicht flexibel auf ungeplante Spitzen reagiert werden. Wenn die Hardware eben doch nicht ausreicht, muss ein neuer, aufwändiger Beschaffungsprozess angestoßen werden.
• Der Ausbau ist oft nur grobgranular möglich. Wenn die Anwendung auf einem Cluster von zwei Servern läuft, kann er nur sinnvoll um mindestens einen Server erweitert werden – was die Kapazität gleich um 50 Prozent erhöht.

Schon wegen Continuous Deployment kann dieser Ansatz nicht durchgehalten werden. Wenn mehrere Umgebungen zur Verfügung stehen sollen, auf denen die Software installiert und getestet werden kann, erzwingt dieses unflexible Modell die Beschaffung von vielen Hardwareressourcen, die aber nur während der Entwicklung genutzt werden. Daher ist Continuous Delivery nur auf virtualisierten Umgebungen oder in einer Cloud wirklich sinnvoll realisierbar. So können die Ressourcen flexibel nur dann zur Verfügung gestellt werden, wenn sie wirklich benötigt werden.
Wenn diese Möglichkeiten einmal zur Verfügung stehen, kann auch mit dem Sizing der Produktionsumgebungen anders umgegangen werden: Statt statisch die notwendigen Ressourcen zur Verfügung zu stellen, können je nach Last neue Server in das System integriert werden. So werden nur jeweils die Ressourcen genutzt, die bei der aktuellen Last tatsächlich notwendig sind. Und bei einer ungeplanten Spitze werden entsprechend mehr Server hinzugefügt. Technisch ist es für diese Ansätze notwendig, dass neue Server zur Laufzeit gestartet werden. Dazu sind Clouds oder entsprechende Virtualisierungsmechanismen notwendig.
Damit geht ein radikal anderes Verständnis von Systemen einher: Während früher Systeme statisch Ressourcen belegt haben, werden ihnen mit diesem Paradigma dynamisch Ressourcen zugewiesen. Das ist kosteneffizienter und flexibler.
Die Softwarearchitektur muss natürlich damit umgehen können, dass die Ressourcen nicht mehr „für immer“ zur Verfügung stehen. Beim Hochskalieren muss die Last also möglichst schnell auf mehr Server verteilt werden. Und wenn die Last geringer wird, muss es möglich sein, Server zu deaktivieren. Damit dies möglich ist, darf auf den Servern kein Zustand gehalten werden wie beispielsweise die Session von Benutzern. Beim Hochskalieren ist auf den neuen Servern noch kein Zustand vorhanden, sodass sie nur für neue Nutzer zur Verfügung stehen. Und die Server können auch nicht mehr ohne Weiteres heruntergefahren werden, weil dann die Daten einiger Nutzer nicht mehr zur Verfügung stehen. So groß ist die Änderung aber nicht: Die Server möglichst zustandsfrei zu halten, ist schon lange als eine Best Practice etabliert.
Wesentlich für dieses Vorgehen ist also lediglich, dass die Zahl der genutzten Server je nach Last reguliert wird. PaaS-Clouds bieten dazu von Haus aus Möglichkeiten. Aber auch andere Clouds, die nur Infrastruktur anbieten, haben in diesem Bereich Möglichkeiten. So kann die Amazon-Cloud durch den Elastic Load Balancer aufgrund bestimmter Regeln neue Server starten – beispielsweise wenn die Bearbeitung eines Requests zu lange dauert [15].
Solche fortgeschrittenen Technologien sind aber nicht immer notwendig: Wenn die Infrastruktur es möglich macht, neue Server zu starten, kann ein einfaches Skript, das die aktuelle Performance misst und dann gegebenenfalls neue Server startet, bereits ausreichend sein.

[ header = Seite 3: Ausfallsicherheit ]

Ausfallsicherheit

Auch bei der Ausfallsicherheit ergeben sich neue Ansätze: Klassisch wird vor allem auf eine hohe Verfügbarkeit der Hardware gesetzt. Aber dieser Ansatz hat seine Grenzen: Es hat sich wohl fast schon jeder in einer Situation wiedergefunden, in der die angeblich so ausfallsicheren Systeme dann doch ausfallen. Üblicherweise sind die Auswirkungen dann erheblich: Die Anwendung stellt ihren Dienst ein und gegebenenfalls gehen auch Daten verloren. Daher streben IT-Organisationen an, die Verfügbarkeit der Hardware immer weiter zu optimieren.
Aber ab einer bestimmten Anzahl Server werden Ausfälle nicht mehr zu einem Sonderfall, sondern kommen ständig vor. Firmen wie Google haben tausende von Servern ständig im Einsatz – spätestens dann ist es komplett unrealistisch, sich darauf zu verlassen, dass die Hardware schon nicht ausfallen wird. Dann muss die Software damit umgehen können, dass einzelne Rechner nicht zur Verfügung stehen. Oft werden daher Server redundant vorgehalten – in einigen Fällen sogar in unterschiedlichen Rechenzentren. Das führt dann zu erheblich höheren Kosten.

Wenn allerdings schon die beschriebenen Voraussetzungen für Flexibilität gegeben sind, kann auf den Ausfall eines Servers reagiert werden: Es können neue Server gestartet werden – gegebenenfalls auch in anderen Rechenzentren oder in einer Cloud. Die Software muss mit dem Ausfall eines Servers umgehen können, denn wie bereits erläutert, werden Server abgeschaltet, wenn die Last zurückgeht – und das ist einem Ausfall nicht unähnlich.
Auch für die Ausfallsicherheit gilt also: Wenn die Anwendungen keinen Zustand auf den Servern halten, ist der Ausfall eines einzelnen Servers durchaus verschmerzbar. Irgendwo müssen Daten aber aufbewahrt werden – dazu bieten sich NoSQL-Datenbanken an, wie der Kasten: „Ein Beispiel: NoSQL“ im Detail diskutiert.
Auch für die Softwarearchitektur hat diese Idee Auswirkungen: Systeme müssen mit Ausfällen rechnen und sinnvoll mit ihnen umgehen. Beispielsweise gibt Hinweise, wie mit solchen Situationen sinnvoll umgegangen werden kann. Oft kann bei dem Ausfall eines Diensts statt den gelesen Werten einfach ein Default-Wert genutzt oder bei schreibenden Zugriffen die Werte zunächst gepuffert werden. Dieser triviale Schritt sorgt dann dafür, dass der Ausfall des Diensts zwar schlechtere Ergebnisse liefert, aber nicht zu einem vollständigen Ausfall führt. Während also eine klassische Architektur den Ausfall eines Servers direkt an den Nutzer weitergibt, können diese Architekturen mit dem Ausfall einiger Server umgehen. So entstehen Systeme, die gegenüber der Infrastruktur widerstandsfähig sind.
Was bedeutet das für Java? Im Wesentlichen ergeben sich Konsequenzen weniger für die Technologien sondern für die Architektur. Beispielsweise sollte die Anwendung zustandslos sein, sodass der Ausfall eines Servers nicht zu einem Datenverlust führt. Letztendlich werden die Anwendungen so ausfallsicherer: Sie sind nicht mehr an die Verfügbarkeit einzelner Server gebunden, sondern gehen in der Software mit dem Ausfall um.
Die wesentliche Änderung ist die Ablaufumgebung: Statt einige hoch performante und ausfallsichere Server zu nutzen, laufen die Anwendungen in virtualisierten Umgebungen. Je nachdem, wie viele Kapazitäten gerade benötigt werden, können mehr oder weniger Ressourcen hinzugefügt werden. Dazu kann eine virtualisierte Infrastruktur, eine Cloud oder auch eine PaaS-Lösung genutzt werden.

Fazit

Mit Continuous Delivery und DevOps gibt es zwei Konzepte, mit denen agile Ansätze auch auf den Betrieb und die Produktivstellung von Anwendungen adaptiert werden. Java-Entwickler sollten sich daher mit Technologien für Continuous-Delivery-Pipelines vertraut machen. Durch DevOps werden Entwickler und Architekten in Zukunft viel dichter mit dem Betrieb zusammenarbeiten und auch Skills im Bereich von Betrieb aufbauen. Gerade mit Continuous Delivery geht eine deutlich flexiblere Infrastruktur einher, die dann beim Sizing und bei der Flexibilität auch in der Produktion weitere Vorteile ermöglichen – wenn die Softwarearchitektur entsprechend angepasst wird. Neue Technologien wie NoSQL setzen diese Ansätze schon geschickt um und können daher als Vorbild dienen.

Ein Beispiel: NoSQL
Gerade für das Sizing und die Flexibilität sind zustandslose Anwendungen wünschenswert. Dabei gibt es allerdings ein Problem: Eine Anwendung muss einen Zustand verwalten können. Es gibt also eigentlich nur die Möglichkeit, den Zustand nicht in der Anwendung sondern in der Datenbank zu halten. Dennoch sollte die Datenbank den Kriterien aus dem Artikel bezüglich Ausfallsicherheit und Sizing gerecht werden. Das ist bei relationalen Datenbanken oft nicht der Fall – daher ist es in diesem Zusammenhang sinnvoll, einen Blick auf NoSQL-Datenbanken zu werfen, die ganz andere Lösungen parat haben:
• Für das Sizing bieten NoSQL-Datenbanken meistens horizontale Skalierbarkeit an. Dabei werden die Daten und Anfragen auf mehrere Server verteilt. Abhängig von der Datenmenge können so also mehr oder weniger Server genutzt werden. Dadurch können Kapazitäten dynamisch erweitert werden – gegebenenfalls müssen dazu natürlich Daten auf neue Server verteilt werden, was die Dynamik etwas einschränkt.
• Für die Ausfallsicherheit werden die Daten auf mehrere Server repliziert. Wenn ein Server ausfällt, sind die Daten immer noch auf mehreren anderen Servern vorhanden, sodass der Ausfall toleriert werden kann. Dafür muss es allerdings Kompromisse geben: Die Replikation führt dazu, dass die Daten auf Server kopiert werden müssen. Das bedeutet, dass die Daten nicht sofort auf allen Servern zur Verfügung stehen. Sie sind also nicht immer konsistent: Eine Anfrage an einen Server kann ein anderes Ergebnis bringen, als die Anfrage bei einem anderen Server, wenn die Daten noch nicht vollständig kopiert worden sind.

Deutlich wird hier das Konzept hinter der Architektur von NoSQL-Datenbanken: Durch Redundanz kann Ausfallsicherheit erzeugt werden – und zwar in diesem Fall mithilfe der Software. Und durch die Aufteilung der Last auf viele Maschinen kann ein flexibles Sizing erreicht werden. Diese Ansätze können auch in eigenen Architekturen genutzt werden. Außerdem können NoSQL-Datenbanken also Daten auch entsprechend den Anforderungen aus der neuen Welt gespeichert werden – sodass die Anwendungen trotzdem einen Zustand halten können.
Interessanterweise haben NoSQL-Datenbanken auch Auswirkungen auf Continuous Delivery und DevOps: Die Datenbanken sind auch bezüglich der Schemas viel flexibler, sodass bei Continuous Delivery neue Releases der Software einfacher installiert werden können, da aufwändige Schemamigrationen entfallen. Bezüglich DevOps ändern NoSQL-Datenbanken das Zusammenspiel zwischen Entwicklung und Betrieb: Bei relationalen Datenbanken ist der Betrieb im Cluster für die Software transparent. Bei NoSQL haben Entwickler hingegen die Wahl: Lieber Daten schneller lesen – aber potenziell Inkonsistenzen in Kauf nehmen – oder ist die Konsistenz doch wichtiger? Die Daten werden repliziert, sodass Änderungen bei Repliken erst mit Verzögerung eintreffen. Bei Leseoperationen kann also von den Replikaten gelesen werden – die Daten sind dann möglicherweise veraltet – oder die Konsistenz ist wichtiger. Gerade beim Betrieb von Clustern verlagern sich also Herausforderungen vom Betrieb zur Entwicklung.

Geschrieben von
Eberhard Wolff
Eberhard Wolff
Eberhard Wolff ist Fellow bei innoQ und arbeitet seit mehr als fünfzehn Jahren als Architekt und Berater, oft an der Schnittstelle zwischen Business und Technologie. Er ist Autor zahlreicher Artikel und Bücher, u.a. zu Continuous Delivery und Microservices und trägt regelmäßig als Sprecher auf internationalen Konferenz vor. Sein technologischer Schwerpunkt sind moderne Architektur- und Entwicklungsansätze wie Cloud, Continuous Delivery, DevOps, Microservices und NoSQL.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.