Application-Server, Container oder lieber gleich Serverless?

Rückblickend auf Software-Projekte der letzten Jahre ist ein klarer Trend erkennbar. Die Snowflake-Server haben in modernen Architekturen ausgedient. Was aber ist die nächste Stufe der Evolution: Automatisierung mit Ansible, Chef und Co., Container à la Docker, Cluster-Lösungen wie Kubernetes oder komplett ohne Server als Serverless-Functions?

Alle Ansätze haben ihre Vor- und Nachteile und sollten je nach Zweck richtig eingesetzt werden. Wie jede Architekturentscheidung ist auch die Laufzeitumgebung (und der darunterliegende Technologie-Stack bis zur Hardware) eine Trade-Off-Entscheidung zwischen verschiedenen Qualitätszielen und sollte bewusst getroffen werden.

Früher war in den meisten Projekten der Graben zwischen Development und Operation noch sehr tief. Die Server wurden in allen Umgebungen ausschließlich von den Administratoren des Betriebs betreut und jede Änderung an der Konfiguration oder Software musste mehr oder weniger aufwendig beauftragt werden. Das führte oftmals zu einem enormen Waste-in-Development, da man im Rahmen des Entwicklungsprozesses von (Team-) externen Arbeiten abhängig war. Für Operations bedeuten diese vielen kleinen manuellen Änderungen einen enormen Aufwand und erfordern ein großes Maß an Disziplin, alle Umgebungen immer konsistent zu halten. Eine Reproduzierbarkeit
beim Aufsetzen einer neuen Umgebung konnte nicht sichergestellt werden.

Weiterhin war die Plattform für Java-Anwendungen in dieser Zeit oftmals ein “fetter” Application-Server. Dieser benötigte oft viele Ressourcen, die Applikationen waren oft von dem eingesetzten Application-Server des jeweiligen Herstellers abhängig und die Administration dieser Server war meistens recht komplex. [Wenn man nicht sehr sauber gearbeitet hat, waren die Anwendungen oft von den speziellen Application-Servern abhängig. Selbst, wenn man im Code wirklich sauber entwickelt hat, so musste man trotzdem noch häufig vendorspezifische Deployment-Deskriptoren schreiben.]

In der letzter Zeit setzen sich die beiden Trends DevOps und Microservice [Mit Microservices ist hier die Abkehr von Deployment-Monolithen hin zu kleineren, unabhängig deploybaren Services gemeint. Die Rede ist eher vom Architekturstil, eine Software aus unabhängigen Prozessen mit sprachneutralen APIs zu bauen.] immer mehr durch und immer mehr Unternehmen beschäftigen sich damit. Bei der Software-Entwicklung wird heute oftmals auf autonome und interdisziplinäre Teams gesetzt, die die Skills und Kompetenzen haben, all ihre Aufgaben möglichst alleine zu bewältigen. Und auch bei den Applikationen geht der Trend weg von den in “liebevoller” Handarbeit administrierten Servern hin zu isolierten und self-contained Applikationen, die alles, was sie brauchen, selbst mitbringen. Gerade mit Containern hat man nun auch ein etabliertes Format und das entsprechende Tooling, um diese Anwendungen zu verpacken und zu transportieren. Für diese Container etablieren sich immer mehr Plattformen, mit denen man die einzelnen Container zu
größeren Anwendungen orchestrieren, skalieren und betreiben kann. Eine andere Möglichkeit ist der Betrieb komplett ohne Server – also quasi Serverless. Hier nutzt man eine von einem Anbieter angebotene Laufzeitumgebung und gibt betriebliche Aspekte wie Skalierung, Verfügbarkeit oder Patch-Management an den Anbieter ab.

Im Folgenden werden Tools und Technologien beleuchtet, die alle ihre Stärken und Schwächen haben und je nach Zweck richtig eingesetzt werden sollten. Die Entscheidung für ein Betriebsmodell für eine Software-Lösung ist nämlich eine Architekturentscheidung. Und solche Entscheidungen sollten ein gutes Zusammenspiel aus Anforderung, Lösung und Kontext darstellen, da eine nachträgliche Änderung bei solchen fundamentalen Entscheidungen oftmals teuer und aufwendig werden kann. [Als Kontext wird hier die Umgebung aufgefasst, in der die Anwendung eingebettet wird. Dieser ist nicht nur technologisch zu sehen, sondern betrifft auch das Unternehmen: Mitarbeiter, Skills, Prozesse, etc.]

Um die einzelnen Lösungen etwas näher zu beleuchten, wird ein einfaches Hello-World-Beispiel mithilfe der verschiedenen Technologien in eine lauffähige Umgebung deployt. Für die ersten Beispiele wird eine sehr einfache Spring-Boot-Anwendung benutzt, die einfach eine HTTP-Route mit einem freundlichen Hallo beantwortet.

(Listing 1) – Einfache Spring-Boot-Anwendung
@SpringBootApplication
@Controller
public class App {
  public static void main(String… args) {
    SpringApplication.run(App.class, args);
  }
  @GetMapping(value = { „/“, „/hello“ })
  @ResponseBody
  public String getHello(@RequestParam(

name = „name“, required = false, defaultValue =
„World“)
     String name) {
       return „Hello “ + name + „!“;
  }
}

Automatische Software-Provisionierung und -Konfiguration

Ansible ist ein Konfigurations- und Management-Tool, mit dem man auf einer beliebigen Anzahl an Servern eine Reihe an definierten Kommandos und Konfigurationen ausrollen kann. Es ist etwas jünger (erstes Release in 2012) als andere Konfigurations-Tools, wie Puppet oder Chef und benötigt anders als seine Konkurrenz keine Agenten auf den zu konfigurierenden Systemen. Dadurch wird der Einsatz sehr vereinfacht, da im Grunde nur ein Ansible-Host benötigt wird, auf dem ein sogenanntes Playbook gestartet wird. Der Host verbindet sich per
SSH mit den zu konfigurierenden Servern und führt die im Playbook enthaltenen Befehle aus. Die Server, bzw. die Gruppen von Servern, werden vorher aus einem Inventory selektiert. Die Selektion erfolgt dabei durch Patterns, die einzelne Server, Gruppen von Servern oder aber auch Schnittmengen oder disjunkte Mengen zwischen den Gruppen selektiert.

Ein Playbook ist eine einfache YAML-Datei, in der eine Reihe von Variablen definiert werden, die während der Ausführung des Playbooks genutzt werden können. Dabei können die Variablen je nach Gruppe oder Host aus dem Inventory anders belegt werden. So kann z.B. die Java-Version für Application-Server auf 9 und für Datenbank-Server auf 8 gesetzt werden. Neben den Group- und Host-Vars enthält das Playbook noch eine Liste von Tasks, die auf den selektierten Servern ausgeführt werden. Diese Tasks verwenden dabei üblicherweise die definierten Variablen und können so für jede Gruppe spezifische Einstellungen vornehmen. Ein weiteres Element der Playbooks sind Rollen,
welche eine Menge an Variablen und Tasks umfassen und für die selektierten Server ausgeführt werden. Die Rollen bilden also eine Art wiederverwendbaren Building-Block und können auch unabhängig vom Playbook versioniert und referenziert werden.

(Abb. 1) zeigt vereinfacht den Ablauf einer Playbook-Ausführung. Aus einer Menge an Servern werden einzelne Server oder Gruppen aus dem Inventory selektiert. Es werden evtl. noch Rollen oder Tasks in das Playbook inkludiert und dann per SSH auf den selektierten Rechnern zur Ausführung gebracht.

 

Funktionsweise von Ansible.

Funktionsweise von Ansible. (Abb. 1)

Aufbauend auf der Theorie wird nachfolgend ein einfaches Playbook für die sehr einfache Hello-World-Anwendung betrachtet. In diesem simplen Playbook wurden keine Rollen benutzt, aber man sieht, dass z.B. die Installation einer Applikation aus einem Maven-Repo oder das Einrichten einer SystemD-Unit als wiederverwendbare Rolle definiert werden kann. Allerdings sieht man auch, dass die einzelnen Tasks, die man definiert, oftmals von dem verwendeten Betriebssystem abhängig sind. In dem Beispiel sind die verwendeten Tasks speziell für den apt-Paket-Manager, der in einigen Linux-Distributionen verwendet wird. Aus diesem Grund gibt es in Ansible auch die Möglichkeit Tasks
bedingt auszuführen. Ein Task oder eine ganze Rolle kann z.B. vor der Ausführung auf eine Betriebssystemfamilie oder ein spezielles
Betriebssystem prüfen und nur bei einem Match ausgeführt werden. Ein robustes Playbook oder eine robuste Rolle, welche auf vielen verschiedenen Systemen einwandfrei funktioniert, ist also eine Menge Arbeit und verlangt viele Tests.

(Listing 2) – Ansible-Playbook zum Ausrollen der Beispielanwendung

– name: installs Java Service on machine
  hosts: default
  become: yes

  vars:
    group_name: java
    user_name: java
    home_path: /opt/java-app
    java_pkg: default-jre
    mvn_repo: /home/thopaw/.m2/repository
    app_artifact: jcon-java
    app_group: thopaw
    app_version: 0.0.1-SNAPSHOT
    app_path: ‚{{ mvn_repo }}/{{ app_group }}/
    {{app_artifact }}/{{app_version }}/
    {{ app_artifact }}-{{ app_version }}.jar‘
    ansible_python_interpreter: ‚/usr/bin/python3‘

tasks:

   – name: Update all packages to the latest version
      apt:
        upgrade: dist
        update_cache: yes
   – name: Installs JRE
      apt:
        name: ‚{{ java_pkg }}‘
   – name: ‚Ensure group {{ group_name }} exists‘ group:
        name: ‚{{ group_name }}‘ state: present
   – name: ‚Adds user {{ user_name }} for application‘
       user:
         name: ‚{{ user_name }}‘
         group: ‚{{ group_name }}‘
         create_home: true
         home: ‚{{ home_path }}‘
         system: true
   – name: copy from maven repo to server
       copy:
         src: ‚{{ app_path }}‘
         dest: ‚{{ home_path }}/app.jar‘
         owner: ‚{{ user_name }}‘
         group: ‚{{ group_name }}‘
         mode: 0644
   – name: Ensure SystemD Unit is configured
       template:
         src: template/systemd.service.j2
         dest: ‚/etc/systemd/system/{{app_artifact}}.service‘
         owner: root
         group: ‚{{ group_name }}‘
         mode: 0644
   – name: Ensure Service is started
       systemd:
         name: ‚{{ app_artifact }}‘
         daemon_reload : true
         enabled: true
         state: started

An diesem einfachen Beispiel sieht man, dass man Ansible für die Automatisierung bei der Software-Verteilung und der Administration der Server einsetzen kann. Wenn man eine Analogie zu Programmiersprachen herstellt, ist man damit aber eher “low-level” unterwegs. Man muss sich auf diese Weise noch um alle Server-Aufgaben bei der Administration selber kümmern: Patch-Management, Updates, Backups, Housekeeping, etc. Viele Aufgaben können in zentral gewarteten Rollen ausgelagert werden, aber man muss diese immer noch warten, pflegen oder überprüfen. Allerdings hat man so auch alle Freiheiten und kann das System optimal für seinen Einsatz einrichten und nutzen.

Weiterhin wächst die Anzahl an built-in Ansible-Modulen, mit denen man auf höherer Ebene Aufgaben beschreiben und automatisieren
kann. So gibt es z.B. schon Module, mit denen Kubernetes-Objekte verwaltet oder AWS Lambda Functions erstellt werden können.

Ansible ist sehr nützlich und mächtig und kann in unterschiedlichen Szenarien eingesetzt werden. So kann es z.B. für die Provisionierung von Kauf-Software benutzt werden, die sich nicht in Container packen lassen oder deren Lizenzmodell es nicht zulässt. Ebenfalls eignet sich Ansible sehr gut, um betriebssystemnahe Aufgaben zu automatisieren: Systeme patchen, eine Grundkonfiguration ausrollen, bestimmte Härtungsmaßnahmen vornehmen, etc. Mit seinen Erweiterungen kann Ansible auch ein Kandidat sein, um Container, Kubernetes-Objekte oder sogar Serverless-Functions zu verwalten. Allerdings existieren wahrscheinlich bessere Tools, so dass sich die Komplexität die Ansible mit sich bringt, vermeiden lässt. Wenn die komplette IT aber sonst mittels Ansible automatisiert ist, kann man den Skill in dem Unternehmen nutzen, um beispielsweise auch Container mittels Ansible zu orchestrieren oder auf Container-Plattformen zu deployen.

Container

Eine Technologie, die mittlerweile einen großen Einfluss auf die Software-Entwicklung und deren Architektur, die Paketierung und Verteilung hat, ist die Container-Technologie. Selbst eher konservative Unternehmen setzen immer mehr auf diese Technologie, weil sie eine Menge Vorteile bringt. Entwickler sehen einen der Hauptvorteile in Containern wie Docker oder Rocket, dass sie Entwickler davon befreit, die Software für ein bestimmtes Zielsystem zu erstellen. Ähnlich wie eine Java-Klasse auf jeder (versions-kompatiblen) JVM läuft, läuft ein Docker-Container auf allen Docker-Hosts. Der Entwickler kann einfach in einer für ihn bereits gewohnten Form alle Systemvoraussetzungen in einer Datei definieren, beschreiben und daraus ein portables Paket mit der Anwendung und all ihren Abhängigkeiten bauen, dieses lokal
testen und in einem Repository bereitstellen. Weiterhin lassen sich eine Unmenge an Containern, die von anderen bereitgestellt werden, wie Bausteine benutzen oder für eigene Zwecke erweitern.

Führt man das Beispiel weiter fort und schreibt ein einfaches Docker-File für die Hello-World-Applikation, so könnte das wie folgt aussehen:

(Listing 3) – Docker-File für das Beispiel
FROM java:alpine
COPY app.jar app.jar
CMD java -jar app.jar

Mit diesen drei Zeilen wird ein neues Image basierend auf dem Image java:alpine erzeugt und das Applikations-JAR wird in das Image kopiert. Die dritte Zeile definiert was passieren soll, wenn ein Container mit diesem Image gestartet wird. Dieses Beispiel zeigt, dass man bestehende Images wiederverwenden und erweitern kann, anstatt ein neues Image für seine Anwendung von Scratch aufbauen zu müssen. Dies bietet für die Entwicklung enorme Vorteile, da man Images als Basis für eine Laufzeitumgebung seiner eigenen Anwendung oder Images als fertige Services mit seinen eigenen Anwendungen nutzen kann, um diesen weitere Dienste wie z.B. Datenbanken, Proxies oder
Queues anzubieten.

Allerdings schafft dies in einem Unternehmen auch eine zusätzliche Komplexität. Neben den verwendeten Bibliotheken, die in der Anwendung benötigt werden, müssen nun auch die verwendeten Images (und deren Ableitungskette bis hin zum Ursprung) überprüft und gemanagt werden. Folgende Fragen müssen nun beispielhaft beantwortet und für jedes Image nachgehalten werden:

  • Sind in den verwendeten Images bekannte CVEs [Sicherheitslücken, Common Vulnerabilities and Exposures]?
  • Werden durch die Images Lizenzen verwendet, die nicht passen?
  • Sind die verwendeten Images aktuell?

Wenn man “nur” Docker-Container verwendet um seine Anwendung zu betreiben, muss weiterhin geklärt werden, wie die Images auf den einzelnen Servern verteilt, gestartet und überwacht werden. Dazu könnte man Technologien wie Ansible verwenden, aber damit müssten alle betrieblichen Themen rund um die Container-Instanzen selber gelöst werden. [Dies wären z.B. Themen wie Monitoring, Skalierung und Deployment.]
Allerdings würde man das Potenzial der Container-Technologie nicht ohne großen Aufwand ausschöpfen können, sondern man würde das Container-Format vielmehr als Paketierungs- und Transportformat benutzen. Diese Technologie erlaubt aber noch andere Dinge, wie z.B. das sehr schnelle skalieren einer Applikation, indem neue Instanzen gestartet oder Instanzen gestoppt werden. Oder es können auch defekte Instanzen einer Anwendung einfach gestoppt und zerstört werden, wenn die Anwendungen diese Elastizität unterstützen. Da das Management dieser dynamisch schwankenden Anzahl an Container-Instanzen manuell nur sehr schwer möglich ist, haben sich hier auch Tools etabliert, um die Menge an Container zu managen und zu überwachen. Dabei hat sich Kubernetes als äußerst potenter Kandidat
etabliert.

Kubernetes

Das initial von Google entwickelte Kubernetes ist eine Container-Management- und Orchestrierung-Plattform, auf der Container zu Anwendungen geschnürt und betrieben werden können. Neben der reinen Plattform bietet Kubernetes auch noch eine Menge an Tools und Services an, mit denen der Lebenszyklus der Container automatisiert, gemanagt und überwacht werden kann.

Ein Kubernetes-Cluster selbst besteht aus Master- und Worker-Nodes. Die Pods – so heißt in Kubernetes die kleinste Einheit aus einem oder mehreren Containern – laufen auf den Worker-Nodes. Die Master-Nodes überwachen die Worker-Nodes und die dort laufenden Container und stellen die API bereit, mit der man mit dem Cluster kommunizieren kann. Sie starten und stoppen bei Bedarf Container oder andere Objekte, die vom Cluster gemanaged werden. Die API ermöglicht es neben Containern noch eine Reihe anderer Objekte auf dem Cluster zu betreiben.

Mit der API bietet Kubernetes eine sehr einfache und für Entwickler gewohnte Schnittstelle, um Objekte in dem Cluster anzulegen, zu löschen und zu ändern. Objekte werden dabei als Ressourcen bezeichnet. Typische Ressourcen sind dabei z.B. die schon angesprochenen Pods, aber auch Firewall-Regeln, Services, Konfigurationsparameter oder Berechtigungen. Um die unterschiedlichen Ressourcen voneinander unterscheiden oder auch selektieren zu können, besteht die Möglichkeit, diese mit Label oder Annotationen zu versehen. Ein Label oder eine Annotation besteht dabei aus einem Namen und einem Wert. Neben dem Wiederfinden der Ressourcen werden Labels und Annotationen auch zur Konfiguration verwendet. Denn Kubernetes erlaubt es auch, Ressourcen auf dem Cluster zu betreiben, die z.B. auf das Erzeugen einer neuen Ressource reagieren und diese konfigurieren – oder Dienste für diese anzubieten, wenn die neu erzeugte Ressource mit bestimmten Annotationen ausgestattet ist. Auf diese Weise kann ein Kubernetes-Cluster um zusätzliche Funktionen erweitert werden und so einen größeren Funktionsumfang anbieten.

Ein Beispiel hierzu ist external-DNS. Diese Komponente hat für neu angelegte Ingress-Rules (eine Regel, die beschreibt wie und wo das Cluster Netzwerk-Anfragen erhalten soll) einen DNS-Record in einem zuvor konfigurierten DNS-Server angelegt. Auf diese Weise kann man Applikationen auf dem Cluster sehr einfach einen DNS-Namen zuordnen und auch wieder löschen, wenn die entsprechende Ingress-Ressource vom Cluster entfernt wird. Dieser Mechanismus lässt sich noch mit einer weiteren Komponente mit dem Namen kube-lego [Dieser wird nicht weiterentwickelt und wurde durch cert-manager ersetzt.] kombinieren. Für speziell annotierte Ingress-Rules kann kube-lego dann für den angelegten DNS-Eintrag ein Let’s-Encrypt-X.509-Zertifikat anfragen und einrichten. Auf diese Weise können auf dem Cluster sehr komfortabel für neue Applikationen DNS-Einträge und auch Zertifikate eingerichtet und gemanaget werden, so dass die Anwendungen direkt per HTTPS erreichbar ist.

Übersicht eines Kubernetes-Clusters. (Abb. 2)

Übersicht eines Kubernetes-Clusters. (Abb. 2)

Damit die verschiedenen Ressourcen auch komfortabel und programmatisch (und damit automatisierbar) angelegt, gelöscht und verändert werden können, bietet Kubernetes neben der API auch noch verschiedene Tools an. Das wohl wichtigste Tool ist dabei kubectl, das Command-Line-Tool zur Cluster-Verwaltung. Dieses Tool wird auch in unserem Beispiel verwendet, um die Docker-Container auf dem Cluster zum Laufen zu bringen. Dazu müssen aber zunächst die Kubernetes-Objekte deklariert werden, die eine Anwendung ausmachen.

(Listing 4) – Definition der Kubernetes Objekte
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-app-deployment
spec:
  replicas: 3
  selector:
      matchLabels:
      app: hello-pod
  template:
    metadata:
      labels:
         app: hello-pod
    spec:
      containers:
      – name: nginx
         image: app
         imagePullPolicy: Never # Wird wegen minikube benötigt
         ports:
         – containerPort: 8080

kind: Service
apiVersion: v1
metadata:
  name: hello-service
  labels:
     app: hello-app
spec:
  selector:
     app: hello-pod
  ports:
  – name: hello-http
     protocol: TCP
     port: 80
     targetPort: 8080

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: hello-ingress
  annotations: nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  – http:
    paths:
    – path: /hello
backend:
serviceName: hello-service
servicePort: 80

In der YAML-Datei in (Listing 4) werden drei Objekte (getrennt durch —) definiert:

  1. Das erste Objekt ist ein Deployment, welches definiert, mit wie vielen Instanzen ein Pod auf dem Cluster laufen soll. Weiter wird in der Sektion template definiert, welche Container zu dem Pod gehören und welche Ports genutzt werden sollen.
  2. Das zweite Objekt definiert einen Service. Dieser selektiert eine Reihe von Pods (über ihr Label app) und definiert einen Port, über welchen diese Pods erreichbar sind. Ein Service ist langlebiger als ein Pod und sollte für die Cluster-interne und -externe Kommunikation genutzt werden. Ein Pod kann nämlich z.B. von den Master-Nodes plötzlich gestoppt und auf einen anderen Worker geschoben werden, wenn der Master entschieden hat, dass die Ressourcen des Workers anders genutzt werden sollen.
  3. Das dritte Objekt ist eine Ingress-Regel. Sie wird von einem Pod mit dem Namen ingress controller genutzt, der auf dem Cluster läuft und quasi die Tür zu Applikationen im Cluster darstellt. Er erzeugt aus allen Ingress-Regeln ein Routing für Netzwerkanfragen an das Cluster. Dabei verteilt die Ingress-Regel den Traffic an den Pfad /hello auf den Service, der unter Punkt 2 beschrieben wurde.

Ein kubectl apply -f app.yml erzeugt nun die Objekte, die in der Datei app.yml auf dem Cluster definiert wurden.

Wie man schon an diesem kleinen Beispiel sieht, bietet Kubernetes eine große Flexibilität bei der Gestaltung einer ganzen Plattform, die für die Container-Orchestrierung genutzt werden kann. Zusammen mit den entsprechenden Tools, wie kubctl oder auch Helm, kann man die Prozesse auch recht einfach automatisieren und z.B. in Continuous-Delivery-Pipelines einbauen.

Allerdings ist der Betrieb eines Kubernetes-Clusters auch eine Herausforderung und benötigt spezielle Kenntnisse, die im Unternehmen vorhanden sein oder aufgebaut werden müssen. Auch kann schon alleine die Geschwindigkeit, mit der Kubernetes weiterentwickelt wird, problematisch sein, da in der Vergangenheit ungefähr alle zwei Monate eine neue Kubernetes-Version veröffentlicht wurde. Das Update des Clusters selber funktioniert i.d.R. problemlos, die Erweiterungen, die auf dem Cluster betrieben wurden, finden die neue Version nicht immer gut. Es kann vorkommen, dass bestimmte Plugins mit einer neueren Kubernetes-Version einfach nicht mehr funktionieren. Eine andere Möglichkeit die Vorteile von Kubernetes zu nutzen, ohne den Aufwand eines Cluster-Betriebs selbst zu stemmen, ist die Nutzung eines Managed-Kubernetes-Clusters. Aufgrund des Erfolges von Kubernetes gibt es immer mehr Angebote von Kubernetes-Cluster, die von dem Anbieter gemanaged werden. Wenn man Kubernetes nur nutzen und es nicht betreiben möchte, dann lohnt sich hier eine Recherche.

Serverless-Functions

Ein anderer interessanter Ansatz, mit dem Software entwickelt und betrieben werden kann, stellen Serverless-Functions dar. Die großen Cloud-Anbieter bieten für diese Art des Anwendungsbetriebs eine Lösung an. Bei AWS heißen diese Funktionen Lambda, bei Google heißen sie Cloud Functions und bei Microsoft Azure einfach nur Functions.

Bei Serverless-Functions abstrahiert man sich noch weiter von Betriebsthemen und selektiert nur noch eine Laufzeitumgebung des Cloud-Anbieters, in der der Code ausgeführt wird. Den einzelnen Funktionen kann man zusätzlich rudimentär Ressourcen wie Speicher, Berechtigung oder ein Timeout mitgeben, ansonsten überlässt man alle Betriebsthemen dem Cloud-Anbieter.

Den wirklichen Vorteil bei dieser Art des Application-Betriebs konnte der Autor bei der Arbeit in einem kleinen Team an einem MVP/Prototyp bemerken. Es war keinerlei Infrastruktur vorhanden, auf der man aufsetzen konnte. Es stellte sich also die Frage, wie die Software betrieben werden soll und dabei gleichzeitig noch genügend Geschwindigkeit bei der Entwicklung sichergestellt werden kann. Da lag die Entscheidung für Serverless-Functions nahe, weil hier nur eine minimale Konfiguration notwendig ist und diese von Tools wie Serverless auch noch sehr komfortabel ist.

Allerdings muss man bei der Nutzung von Serverless-Functions auch einiges beachten. Das erste, was auffällt ist, dass man Software für eine bestimmte Cloud-Plattform schreibt. Hier kann zwar durch entsprechende Pattern bei der Implementierung eine Minderung geschaffen werden, aber an manchen Stellen muss einfach Glue-Code geschrieben werden, der speziell auf die API der Provider passen muss. Ein weiterer Punkt, der erwähnt werden sollte, ist die Gefahr des Vendor-Lock-Ins. Die oftmals nahtlose Integration und Kombination weiterer Services mit den Serverless-Functions erhöht die Gefahr, dass die entwickelte Lösung nicht ohne große Aufwände zu einem anderen
Cloud-Provider portiert werden kann. Ob das ein Problem ist, muss man abwägen. In dem genannten Fall war durch die sehr regulierte Branche Versicherungen, in der sich das Projekt bewegt, und die Anforderung der BaFin eine Portierung zu einem anderen Cloud-Anbieter sowie eine Exit-Strategie zwingend notwendig.

Ein weiterer Nachteil, der bei der Verwendung von Serverless-Functions schmerzhaft werden kann, ist die fehlende Freiheit, die ein Betrieb von z.B. Containern ermöglicht. Es stehen nur die Laufzeitumgebungen zur Verfügung, die der Cloud-Provider anbietet. Und es stehen auch nur die Versionen der Laufzeitumgebungen zur Verfügung, die angeboten werden. Ob man nun also Java 8, 9 oder sogar noch 7 verwendet, ist keine freie Entscheidung mehr. Auch die Nutzung von schon fertigen Software-Bausteinen, wie z.B. Containern, in denen eine SQL, NoSQL, eine Queue oder Kafka läuft, ist nicht möglich. Hier ist man auf SaaS-Lösungen des Cloud-Providers oder eines anderen Anbieters
angewiesen.

Als Beispiel haben wir mittels Serverless ein Projekt generiert und minimal angepasst. Mit folgendem Befehl kann man sich das Projekt erzeugen lassen: serverless create -t aws-java-gradle

(Listing 5) zeigt den RequestHandler, der als Lambda ausgeführt wird. Als Input wird eine Map in die Methode gegeben und als Rückgabe wird ein Response-Objekt zurückgegeben, welches mit in das Projekt generiert wurde.

(Listing 5) – Java RequestHandler für das Serverless-Beispiel
public class Handler implements RequestHandler<Map<String,Object>,
ApiGatewayResponse> {
  private static final Logger LOG = Logger.getLogger(Handler.class);
  @Override
  public ApiGatewayResponse handleRequest(Map<String, Object>
  input, Context context) {
    LOG.info(„received: “ + input);
    @SuppressWarnings(„unchecked“)
    Optional<Map<String,String>> query = Optional.ofNullable(
     (Map<String,String>)input.get(„queryStringParameters“)
    );
    String name = query
       .orElse(Collections.emptyMap())
       .getOrDefault(„name“, „World“);
    Response responseBody = new Response(„Hello “ + name + „!“, input);
    return ApiGatewayResponse.builder()
       .setStatusCode(200)
       .setObjectBody(responseBody)
       .build();
    }
}

Die Datei serverless.yml, die ebenfalls in das Projekt generiert wurde, muss an die eigenen Bedürfnisse angepasst werden. Nach den Anpassungen sieht die Konfiguration wie in (Listing 6) zu sehen ist, aus. Man sieht in der Konfiguration, dass als Cloud-Anbieter AWS und eine Java 8 Umgebung ausgewählt wurde. Interessant ist noch die Deklaration der Funktionen und das Mapping der Events zu den Funktionen. In diesem Fall sollen alle Requests an den Pfad hello von dem RequestHandler beantwortet werden. Als Event lassen sich noch sehr viele andere Typen auswählen, wie z.B. SNS, SQS, S3 oder Schedule. Dies sind meistens andere AWS-Dienste, die sich so nahtlos mit den Serverless-Functions verbinden. Allerdings zahlt man damit den Preis, dass die Lösung von dem jeweiligen Anbieter abhängig und eine Portierung zu einer anderen Plattform schwieriger ist.

(Listing 6) – Konfiguration des Serverless-Frameworks
service: jcon-java-serverless
provider:
  name: aws runtime:
  java8 region: eucentral-1
package:
  artifact: build/distributions/hello.zip
functions:
  hello:
    handler: com.serverless.Handler
    events:
    – http:
    path: hello
    method: get

Eine Automatisierung von Konfigurationen oder Deployments ist durch Tools wie Serverless meist schon problemlos möglich. In dem genannten Fall war der Betrieb und die Weiterentwicklung des MVPs selbst in einem sehr kleinen Team möglich, da die komplette Umgebung gemanaged wurde. Eine Fokussierung auf die Implementierung von Features war problemlos möglich, weil das Team sich auf die Plattform und den Anbieter geeinigt und die damit verbundenen Einschränkungen in Kauf genommen hat.

Fazit

Keine der aufgeführten Technologien ist als klarer Gewinner zu sehen, sondern nur besser oder weniger gut geeignet für die Umsetzung einer Architektur und die Entwicklung eines bestimmten Systems. Ansible oder andere Tools zur Software-Provisionierung sind bestens geeignet, um administrative Prozesse zu automatisieren. Mit zunehmender Reife entstehen auch immer mehr Module und Rollen, die man in seinen Playbooks verwenden kann, um so auf eine höhere Abstraktionsebene zu kommen. Mittlerweile gibt es schon Ansible-Module, die sich um AWS-Ressourcen kümmern oder Kubernetes-Objekte managen. Auch eignet sich Ansible immer noch dafür, eine große Menge an Servern zu administrieren, deren Anwendungen sich nicht so einfach auf eine Container-Plattform portieren lassen. Gründe hierfür könnten z.B. Lagecy-Systeme, Lizenz-Restriktionen oder fehlende Kompatibilität bei eingekaufter Software sein.

Docker alleine ist schon eine hervorragende Lösung, um

  1. fertige Software-Bausteine inklusiver aller Abhängigkeiten zu nutzen ohne eine komplette Umgebung dafür aufzubauen,
  2. seine eigenen Bausteine mitsamt der benötigten Umgebung als Paket zur Verfügung zu stellen.

Kombiniert mit Technologien wie Kubernetes zur Container-Orchestrierung schafft man es, eine automatisierbare und sehr flexible Plattform anzubieten, auf der eine Vielzahl an unterschiedlichen Lösungen von unterschiedlichen Teams betrieben werden können. Allerdings muss man sich auch bewusst sein, dass schon Container alleine eine weitere Komplexitätsebene bedeuten. Die genutzten Images müssen zu den Policies des Unternehmens passen und separat auf Compliance und Sicherheit geprüft werden. Wenn man die Container dann auf einer
Plattform wie Kubernetes betreibt, bringt diese ebenfalls eine eigene Komplexität mit, die beherrscht oder als managed-Lösung eingekauft werden muss.

Software komplett Serverless zu betreiben, ermöglicht eine sehr starke Fokussierung auf die Funktionalität und das Design der Applikation, solange man mit den angebotenen Laufzeitumgebungen auskommt und weitere Anforderungen an die Umgebung der Software über andere Services oder andere Anbieter bedient. Auch sollte man darauf achten, dass die gebaute Software nicht zu stark von dem genutzten Cloud-Anbieter abhängig wird. Hierzu sollte man ein sauberes Design und eine klare Kapselung des Glue-Codes anstreben, der zwischen der eigentlichen Application-Logik und der API des Cloud-Anbieters eingezogen werden muss.


In den letzten Jahren hat Thomas Pawlitzki in verschiedenen Rollen Erfahrungen zu Architekturen, DevOps, Development und Automatisierung sammeln können. Dabei hat er mit Tools wie Ansible, Docker und Kubernetes gearbeitet. Der aktuelle Technologie-Stack umfasst zusätzlich Serverless-Functions.

Twitter: @thopaw

Carolyn Molski


Leave a Reply