Ansible zaubert Spring Boot Apps auch auf Windows

Keine Kommentare

Manchmal kommt es tatsächlich vor, dass wir unsere Spring Boot Apps auf Windows ausführen müssen, anstatt wie gewohnt auf Linux. Das passiert insbesondere dann, wenn wir native Bibliotheken aufrufen müssen, die Windows voraussetzen. Trotzdem sollten wir dafür nicht auf unsere liebgewonnenen DevOps-Tools wie Ansible verzichten müssen!

Windows? Kein Problem. Aber nicht ohne Ansible!

Es kann doch heute kein Problem mehr darstellen, unsere Anwendungen auf Windows laufen zu lassen – oder?! Das konkrete Betriebssystem sollte nur noch eine untergeordnete Rolle spielen. Viel wichtiger ist, dass wir uns nicht von unseren Prinzipien moderner Softwareentwicklung abbringen lassen – sei es Continuous Integration (CI) und Deployment (CD) oder die Automatisierung von sich wiederholenden Aufgaben im Infrastrukturbereich. Dabei kann es sich z.B. um das Aufsetzen neuer Server oder auch die Provisionierung unserer Anwendungen handeln.

In unserer CI-Pipeline nutzen wir Jenkins um unsere Spring Boot Apps zu bauen und zu testen. Per Ansible provisionieren wir unsere (Linux) Maschinen und lassen unsere Apps auf ihnen laufen. Genau das wollen wir auch für Windows Server, wenn wir schon auf sie angewiesen sind.

Klingt nach Träumereien? Ansible war doch dieses Unixoide DevOps-Tool, dass ohne Agent auf den Servern auskommt und per SSH mit ihnen spricht, oder?! Wie soll das denn mit Windows funktionieren? Und Jenkins läuft auf Linux. Wie soll dieses Toolset in der Lage sein, Windows Umgebungen zu managen??!

windows_is_coming

Die Antwort ist einfach: Seit Version 1.7 ist Ansible in der Lage, auch Windows Server zu provisionieren. Statt SSH kommt hier freilich PowerShell remoting (aka Windows Remote Management WinRM) zum Einsatz. Hört sich gut an? Dann sollten wir das direkt mal ausprobieren!

Startpunkt: eine Windows Vagrant Box

Zu allererst benötigen wir natürlich eine Windows Maschine für unsere ersten Gehversuche mit Ansible. Falls man also gerade keine rumstehen hat, könnte sich ein Abstecher auf die Microsoft developer sites lohnen. Ziemlich überaschend bietet Microsoft dort nämlich ein Vagrant Image zum Download an! Auf der Seite https://developer.microsoft.com/en-us/microsoft-edge/tools/vms wählt man eine virtuelle Machine aus – wie z.B. die Microsoft Edge on Windows 10 Stable (15.xxx) – und klickt auf Vagrant im „Select platform“-Feld. Wichtig: man benötigt eine Vagrant-Installation sowie eine Virtualisierungsoftware auf dem eigenen Rechner. Für eigene Experimente anhand dieses Blogartikels kann das frei verfügbare VirtualBox empfohlen werden.

Nach dem Download kann das MSEdge.Win10.RS2.Vagrant.zip direkt entpackt werden. Damit sind wir fast am Ziel. Da Microsoft der Vagrant Box MSEdge - Win10_preview.box keine Metadaten mit auf den Weg gibt, müssen wir sie selbst hinzufügen. Das ist mit Hilfe eines Vagrantfile kein Problem – so wird die Konfigurationsdatei für Vagrant genannt. Für den schnellen Start liegt in diesem Beispielprojekt auf GitHub schon das fertige Vagrantfile bereit:

Vagrant.configure("2") do |config|
  config.vm.box = "windows10"
  config.vm.guest = :windows
 
  # Configure Vagrant to use WinRM instead of SSH
  config.vm.communicator = "winrm"
 
  # Configure WinRM Connectivity
  config.winrm.username = "IEUser"
  config.winrm.password = "Passw0rd!"
 
  config.vm.provider "virtualbox" do |vb|
     # Display the VirtualBox GUI when booting the machine
     vb.gui = true
   end
end

Weil wir mit Hilfe von Vagrant eine Windows Maschine managen, müssen wir natürlich auch die Konfiguration darauf ausrichten. Per config.vm.communicator = "winrm" wird beispielsweise WinRM statt SSH konfiguriert. Außerdem müssen die korrekten User-Credentials konfiguriert werden, die man etwas versteckt in der Installations-Beschreibung der Microsoft Developer Seite findet. Weitere Konfigurationsoptionen zu Windows finden sich auch in der Vagrant WinRM Dokumentation.

Anschließend müssen wir die Vagrant Box noch der lokalen Vagrant-Installation hinzufügen:

vagrant box add "MSEdge - Win10_preview.box" --name "windows10"

Nach ein paar Sekunden sollte nun ist alles bereit sein, um ein virtuelles Windows 10 per vagrant up auf der Kommandozeile zu starten. Das Importieren der neuen Windows Vagrant Box wird dabei im Hintergrund angestoßen und kann eine Weile dauern. Danach bootet die Windows VM:

Leider vergisst Microsoft bei der eigenen Vagrant Box ein paar grundlegende Dinge, damit diese auch ohne Probleme funktioniert. Denn wie man vielleicht schon bemerkt hat, führt ein unvoreingenommenes vagrant up zu einem „Timed out while waiting for the machine to boot […]“. Das liegt an der Konfiguration der Network List Management Policies, die einen Zugriff per Windows Remote Management (WinRM) verhindern.

Doch dem kann man schnell Abhilfe verschaffen. Dazu einmalig nach dem ersten Start der Vagrant Box in die Local Security Policys gehen und darin in die Network List Management Policies wechseln. Dort auf Network klicken und im Tab Network Location den Location type auf private und die User permissions auf User can change location setzen. Danach noch schnell auf die PowerShell wechseln und winrm quickconfig ausführen. Nun sollte das vagrant up so funktionieren wie gedacht. Es wäre natürlich schön, wenn uns diese Arbeit schon Microsoft abnehmen würde.

Windows für Ansible vorbereiten

Damit Ansible mit der neuen Windows Maschine zusammenarbeiten kann, sind noch ein paar Schritte zu tun. Dabei gilt grundsätzlich: Je aktueller die Windows-Version, desto weniger ist zu tun. Wenn man beispielsweise auf eine ganz junge Version wie die oben genannte Vagrant Box von developer.microsoft.com setzt, muss man sich keine Gedanken machen. Weil Ansible minimal PowerShell 3.0 voraussetzt und Windows 10 mit 5.0 out-of-the-box kommt, muss lediglich das Windows-Konfigurationsskript für Ansible einmalig ausgeführt werden. Dabei werden alle für´s korrekte Powershell remoting notwendigen Konfigurationen aktiviert – inkl. WinRM und entsprechenden Firewallregeln. Am einfachsten läd man das Skript mit folgendem Befehl auf einer PowerShell mit administrativen Rechten herunter, der es nebenbei auch gleich noch ausführt:

iwr https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1 -UseBasicParsing | iex

Bei älteren Windows-Versionen kann es schwieriger werden. Erfolgreich provisioniert wurden im Kundenprojekt beispielsweise Windows 7 Maschinen. Hier muss evtl. die Ausführung von Skripten erstmal erlaubt werden – z.B. mit Hilfe des Set-ExecutionPolicy PowerShell Commandlets:

Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser

Oder es muss die korrekte PowerShell-Version ab 3.0 installiert werden – hierzu auch die Liste der Windows-Versionen und ihre zugehörigen PowerShell-Versionen. Welche das eigene System anbietet, findet man schnell selbst heraus:

get-host

Ein Updrade-Skript auf PowerShell 3.0 hilft dann, die notwendigen Voraussetzungen für Ansible zu erreichen.

Nicht vergessen sollte man auch die Art der Anmeldung. Man kann z.B. Kerberos einsetzen – allerdings ist dies nur in der aktuellsten Ansible Version zu empfehlen. Für den Start tut es auch ein normaler administrativer Account auf der Windows Maschine.

Ansible mit Windows sprechen lassen…

Damit ist alles vorbereitet, um mit Ansible loszulegen. Das erste sollte immer ein Test der Verbindung per Ansible sein. Dazu benötigt man ein korrekt konfiguriertes Ansible playbook. Das Beispielprojekt auf GitHub enthält alles Notwendige. Im hostsfile sollte die korrekte IP konfiguriert sein – in unserem Fall mit der lokalen Vagrant Box natürlich 127.0.0.1. Die Konfigurationsdatei restexample-windows-dev.yml aus dem Beispielprojekt enthält die notwendigen Parameter für eine erfolgreiche Verbindung von Ansible mit Windows:

ansible_user: IEUser
ansible_password: Passw0rd!
ansible_port: 55986
ansible_connection: winrm
ansible_winrm_server_cert_validation: ignore

Zusammen mit der laufenden Windows 10 Vagrant Box ist nun alles bereit für einen ersten Verbindungstest. Empfehlenswert ist hier das Ansible Modul win_ping – eines der vielen Ansible Windows Module. Voraussetzung für die Ausführung von Ansible-Befehlen ist natürlich eine lokal lauffähige Version. Idealer Weise handelt es sich um ein möglichst aktuelles Ansible Release. Auf einem Mac erledigt man die Ansible-Installation beispielsweise elegant per homebrew: brew install ansible. Neben dem ebenfalls unproblematischen Linux, stellt Windows als Ansible Host selbst schon ein größeres Problem dar. Hier wird aber sicher das neue Linux-Subsystem gute Hilfestellung leisten können…

Um die Verbindung von Ansible zu Windows dann endlich zu testen, genügt der folgende Befehl innerhalb des zuvor geclonten Beispielprojekts ansible-windows-springboot:

ansible restexample-windows-dev -i hostsfile -m win_ping

Wird man hier direkt mit einem SUCCESS belohnt ist alles in Ordnung:

127.0.0.1 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

Falls man allerdings mit einem UNREACHABLE! konfrontiert wird, geht die Ursachenforschung los. Und diese kann leider aus der Erfahrung heraus schnell ausarten und für eine Menge Frust sorgen. Neben den erwähnten Voraussetzungen und einer möglichst aktuellen Windows Version hat sich noch ein Tipp bewährt: das (auch gern mehrmalige) Überprüfen der Credentials hilft manchmal Tippfehler zu finden. Falls es zu Timeouts kommt, sollte nochmals ein genauer Blick dem Kapitel Windows für Ansible vorbereiten gelten.

Eine lauffähige Spring Boot App…

Wenn eine Spring Boot App auf Windows zum Laufen gebracht werden soll, braucht man natürlich auch eine solche 🙂 Das ist freilich der einfache Teil. Entweder man legt sich mit ein paar Minuten Zeitaufwand eine eigene an – z.B. mit Hilfe des Spring Initializr und/oder einer der sehr empfehlenswerten Spring Starter Guides. Oder man hat sowieso eine lauffähige Anwendung parat. Falls beides nicht zutrifft, kann auch gern auf die hier verwendete Spring Boot App zurückgegriffen werden: ein zugegebenermaßen simpler REST Service.

Egal welcher Weg gewählt wird: am Ende sollte eine lauffähige Spring Boot jar-Datei bereitstehen, die sich per gewohntem java -jar JarDateiName.jar starten lässt. Im Beispielprojekt erzeugt man das benötigte restexamples-0.0.1-SNAPSHOT.jar per Maven auf der Kommandozeile:

mvn clean package

Empfehlungen für ein unbesorgtes Leben mit Ansible & Windows

Bevor man mit der Erstellung erster eigener playbooks zur Provisionierung von Windows beginnt, hilft es, dabei ein paar Punkte im Hinterkopf zu haben. Auch wenn man bereits Erfahrung mit dem Management von Linux Maschinen per Ansible hat, ist einem das ein oder andere evtl. nicht bewusst:

Auf die neueste Ansible-Version updaten: Der Ansible Windows-Support wird aktuell mit jedem Release massiv ausgebaut und verbessert. Das zeigt sich auch daran, dass viele der Windows Module nur mit der neuesten Ansible Version verfügbar sind.

Die Ansible-Dokumentation für Windows hat leider nicht den gewohnt großen Umfang wie für Linux… Hier soll niemand vor den Kopf gestoßen werden – das Ansible Team leistet großartige Arbeit! Aber man muss sich bewusst sein, dass Ansible zwar schon sehr gut in der Praxis mit Windows nutzbar ist. Aber es kann eben doch an einigen Stellen passieren, dass erstmal viele Wege (und Module!) probiert werden wollen, bis das gewünschte Ergebnis erreicht wird.

Bitte IMMER die Backslashes in den Pfaden per vorangestelltem Backslash escapen Hat man beispielsweise einen Pfad wie C:\temp, dann sollte das im playbook unbedingt so aussehen (auch, wenn die Doku nicht explizit darauf hinweißt):

"C:\\temp"

Pfade wie C:\ProgramFiles (x86)\XYZ werden nicht funktionieren Besonders in unserem Fall ist das sehr wichtig, weil wir ein Java Runtime Environment (JRE) benötigen, um unsere Spring Boot- bzw. Java-Anwendung starten zu können. Falls man hier eine installierte Variante nutzen will, sollte man auf alternative Pfade ausweichen – wie Oracle z.B. nach einer erfolgreichen JRE-Installation auf dem System einrichtet:

"C:\\ProgramData\\Oracle\\Java\\javapath\\java.exe"

Ein vollständiges Beispiel

Das Beispielprojekt dieses Artikels beinhaltet auch ein komplettes Ansible playbook. Es zeigt, wie eine Windows Maschine mit Ansible provisioniert und anschließend eine Spring Boot App darauf deployed und ausgeführt werden kann. Um zu verstehen, wie das im Detail funktioniert, lohnt ein genauerer Blick. Zuerst bereiten wir unsere Windows Vagrant Box vor, damit wir später die Anwendung deployen können:

- hosts: "{{host}}"
  vars:
    spring_boot_app_path: "C:\\spring-boot\\{{spring_boot_app_name}}"
    path_to_java_exe: "C:\\ProgramData\\Oracle\\Java\\javapath\\java.exe"
 
  tasks:
  - name: Create directory C:\spring-boot\spring_boot_app_name
    win_file: path={{spring_boot_app_path}} state=directory
 
  - name: Install nssm (non-sucking service manager) via chocolatey
    win_chocolatey:
      name: nssm

Nachdem ein paar benötigte Pfade definiert wurden, erstellen wir ein Verzeichnis für unsere Spring Boot App und installieren den Non-Sucking Service Manager (nssm) mit Hilfe des Windows Package Managers Chocolatey. Beide sind später sehr hilfreich für das Arbeiten mit unserer Windows Maschine.

Chocolatey bringt die Möglichkeiten eines Package Managments in die Windows-Welt, welche man unter Linux und Mac bereits liebgewonnen hat. Und nssm ermöglicht später den Betrieb unserer Spring Boot App als echten Windows Service. Mit all den daraus resultierenden Vorteilen wie z.B. Statusabfrage und automatischen Restarts nach Reboots. Nach einer ganzen Reihe an Experimenten mit verschiedensten Möglichkeiten stellte sich diese Lösung als sehr elegant heraus.

Der Sinn der nächsten Schritte im playbook erschließt sich vielleicht nicht auf den ersten Blick:

  - name: Stop Spring Boot service, if there - so we can extract JRE & other necessary files without Windows file handle problems
    win_service:
      name: "{{spring_boot_app_name}}"
      state: stopped
    ignore_errors: yes
 
  - name: Install Java Runtime Environment (JRE) 8 via chocolatey
    win_chocolatey:
      name: jre8
 
  - name: Copy Spring Boot app´s jar-File to directory C:\spring-boot\spring_boot_app_name
    win_copy:
      src: "{{spring_boot_app_jar}}"
      dest: "{{spring_boot_app_path}}\\{{spring_boot_app_name}}.jar"

Das beginnt schon damit, dass wir den Windows Service stoppen, der unsere Spring Boot App managed. Das wirkt sicher erstmal etwas komisch. Aber es hat auch nichts mit der ersten Ausführung des playbooks zu tun – dafür aber mit allen folgenden. Windows bringt nämlich ein äußerst beliebtes Feature namens sharing violation error mit. Wenn ein laufender Prozess ein „handle“ auf eine Datei oder ein Verzeichnis hält, dann lässt Windows nicht zu, dass diese geändert oder entfernt werden. Aber genau das ist ja, was wir tun wollen: Um das JRE zu updaten, müssen wir in der Lage sein, andere Dateien unserer Anwendung zu verändern. Hört sich also ganz nach einer weiteren Empfehlung an: Bevor Dateien geändert oder gelöscht werden sollen, IMMER die Windows-Prozesse oder -Services stoppen!

Wie gesagt trifft dies aber alles nicht auf die erste Ausführung des playbooks zu – diese würde abbrechen, denn der Service existiert ja noch gar nicht. Genau dafür gibt es aber ein nützliches Ansible-Feature: Wir ignorieren einfach einen etwaigen Fehler in der Modul-Ausführung per ignore_errors: yes. Auf diese Weise wird der Service gestopped, falls er bereits installiert ist und wir umgehen den sharing violation error. Oder aber das win_service Modul reagiert mit einem Fehler, den wir geflissentlich ignorieren können, da wohl noch kein Service installiert wurde.

Danach kann dann das JRE heruntergeladen und entpackt werden – bzw. wie in unserem Fall einfach das Chocolatey Paket jre8 installiert werden. Zuletzt wird dann noch die .jar-Datei unserer Spring Boot Anwendung auf die Windows Maschine kopiert.

Installation und Konfiguration des Windows Service

Endlich können wir unsere Spring Boot App als Windows Service installieren und laufen lassen:

  - name: Install Spring Boot app as Windows service (via nssm), if not already there - but remain stopped to configure Application directory
    win_nssm:
      name: "{{spring_boot_app_name}}"
      application: "{{path_to_java_exe}}"
      app_parameters_free_form: "-jar {{spring_boot_app_path}}\\{{spring_boot_app_name}}.jar"
      state: stopped
 
  - name: Set the Application path for the Spring Boot app to the folder where the needed native libraries reside
    raw: nssm set {{spring_boot_app_name}} AppDirectory {{spring_boot_app_path}}
 
  - name: Fire up Spring Boot app Windows service
    win_service:
      name: "{{spring_boot_app_name}}"
      state: restarted

Dazu definieren wir einen Windows Service mit Hilfe des zuvor installierten nssm und dem Ansible Modul win_nssm. Wichtig hierbei ist der Pfad zur java.exe in der Option application und das -jar spring-boot-app.jar innerhalb der app_parameters_free_form (vor Ansible 2.3 hat das auch mit den app_parameters funktioniert, was jetzt aber auf einen sehr komischen Data line ‚System.Collections.Hashtable‘ is not in ’name=value‘ format.-Fehler läuft). Den Status setzen wir kurzerhand auf stopped – allerdings nur vorübergehend, da wir noch eine andere nssm Service Option konfigurieren wollen.

Dies tun wir im nächsten Schritt. Die nssm Service Option AppDirectory kann sehr wichtig sein, falls die Spring Boot Anwendung native Bibliotheken wie z.B. dll-Dateien im selben Verzeichnis erwartet, in dem sie läuft. Diese nssm Option kann manuell auf der Kommandozeile per nssm edit servicename konfiguriert werden. Der Befehl öffnet daraufhin einen Konfigurationsdialog:

nssm_startup_directory

Aber wir benötigen diesen Schritt natürlich automatisiert innerhalb unseres Ansible Skripts. Dazu müssten wir an den Wert der Variable Startup Directory herankommen. Doch leider bietet das win_nssm Modul dafür keine Konfigurationsoption, weshalb wir auf das raw Modul ausweichen müssen. Mit dem kaum dokumentierten Befehl nssm set servicename AppDirectory path konfigurieren wir das Startup Directory per Ansible.

Im letzten Schritt nutzen wir dann einfach das Modul win_service um unsere Spring Boot Anwendung als korrekten Windows Service zu starten – und dafür zu sorgen, dass dieser auch nach einem Reboot ebenfalls wieder hochgefahren wird.

Nun ist es so weit: Wir können das playbook selbst auszuprobieren! Vorausgesetzt, die Windows Vagrant Box läuft noch, startet der folgende Befehl unser playbook mit dem eben beschriebenen Ablauf:

ansible-playbook -i hostsfile restexample-windows.yml --extra-vars "spring_boot_app_jar=../restexamples/target/restexamples-0.0.1-SNAPSHOT.jar spring_boot_app_name=restexample-springboot host=restexample-windows-dev"

Das Skript sollte in etwa folgenden Output produzieren:

running_ansible_playbook_windows

Smoketest

Der geneigte Leser hat es vielleicht bemerkt: Wir haben den letzten Schritt des playbooks nicht diskutiert. Doch der ist durchaus wichtig:

  - name: Wait until our Spring Boot app is up & running
    win_uri:
      url: "http://localhost:8080/swagger-ui.html"
      method: GET
    register: result
    until: result.status_code == 200  
    retries: 5
    delay: 5

Es hat sich in der Praxis bewährt zuletzt immer noch zu überprüfen, ob die Anwendung auch korrekt läuft. Dazu kann das Ansible Modul win_uri zum Einsatz kommen. Da unsere Beispielanwendung für einen einfachen Zugriff auf die REST Services SpringFox nutzt, können wir auf die entsprechend generierte Weboberfläche einen einfachen HTTP GET ausführen. Manuell kann man das auch im eigenen Browser bei laufender Anwendung auf http://localhost:8080/swagger-ui.html ausprobieren. In unserem playbook gehen wir einfach davon aus, dass unsere Anwendung läuft. Genauso gut kann man aber auch den Spring Boot Actuator Endpoint /health benutzen – dafür sind dann nur die entsprechenden Abhängigkeiten in der pom.xml einzutragen.

Endlose Möglichkeiten

Nun können wir unsere Spring Boot Anwendungen mit Hilfe von Ansible elegant auf Windows Maschinen laufen lassen – und sind bereit, auch weitaus komplexere Szenarien anzugehen – wie sie in der Praxis natürlich häufig vorzufinden sind. Wie wäre es z.B. mit mehreren Microservices auf Basis von Spring Boot und Spring Cloud? Mit Hilfe von Ansible sind wir nun in der Lage, reproduzierbar die kompliziertesten Anforderungen an unsere Infrastruktur abzubilden. Und das auch auf Windows Maschinen – ohne dabei unsere Prinzipien moderner Softwareentwicklung aufzugeben.

Jonas Hecht

Die Überzeugung, dass Softwarearchitektur und Hands-on Entwicklung zusammengehören, führte Jonas zu codecentric. Tiefgreifende Erfahrungen in allen Bereichen der Softwareentwicklung großer Unternehmen treffen auf Leidenschaft für neue Technologien. Im Fokus stand dabei immer die Integration verschiedenster Systeme (und Menschen).

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

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