Wir haben leider eine ganze Weile gebraucht, um die Fortsetzung unseres ersten Teils der Serie zu liefern. Es gab viel gute Arbeit zu verrichten für unsere Kunden. Aber hier ist er nun endlich.

Während Teil 1 das große Ganze im Blick hatte und beschrieb, wie Ansible in Kombination mit Gitlab CI zum Ausliefern von Software in verschiedene Umgebungen genutzt werden kann, wird sich dieser Artikel stärker darauf fokussieren, die Gitlab Artefakte mit Hilfe von Ansible zu installieren. Konkret schauen wir uns eine vollständige Pipeline an, die schlussendlich unsere Spring Boot-Applikation in einer Staging-Umgebung installieren wird.

Wir fangen an, einen Überblick über die IT-Landschaft zu geben, mit der wir es hier zu tun haben. Das sollte auch die Erinnerungen an den ersten Teil auffrischen:

Bild 1: Continuous Delivery
Bild 1: Continuous Delivery

Einige Teile dieser Skizze sollten uns bekannt vorkommen. Wir haben den Entwickler, der Code mit Hilfe von Git pusht auf einen Gitlab-Server (Schritt 0), welcher seinerseits die Pipeline anstößt (Schritt 1). Die Pipeline setzt alles weitere in Bewegung, ihre Jobs sind verantwortlich dafür, die Applikation zu bauen und zu testen (Schritt 2), die gebauten Artefakte in einem Maven Repository zur Verfügung zu stellen (Schritt 3), das Installieren via Ansible auf unserem Staging-Server anzustoßen (Schritt 4), wobei das Repository aus Schritt 3 genutzt wird, um die Artefakte herunterzuladen (Schritt 5). Zu guter Letzt startet sie die Applikation neu (Schritt 6).

Im Folgenden schauen wir uns die einzelnen Jobs der Pipeline genauer an. Wie wir gelernt haben, ist eine Pipeline nichts weiter als eine Reihe von Jobs, die Gitlab für uns ausführen wird. Der erste Job in unserer Pipeline ist, die Spring Boot-Applikation zu bauen und zu testen.

Die Pipeline für ein Code-Repository wird in einer Datei definiert, die .gitlab-ci.yml heißt. So sieht eine Definition der Schritte 2 und 3 unserer Pipeline aus:

variables:
  # Dies unterdrückt ein erneutes Herunterladen von Abhängigkeiten und Plugins und verhindert das Anzeigen von Upload-Nachrichten im Log.
  # `showDateTime` wird die vergangene Zeit in Millisekunden anzeigen. Es muss `--batch-mode` mitgegeben werden, damit das funktioniert.
  MAVEN_OPTS: "-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
  # Seit Maven 3.3.0 kann diesen Option alternativ auch in `.mvn/maven.config` definiert werden. Damit würde dieselbe Konfiguration genutzt werden,
  # wenn dies von der Kommandozeile aus ausgeführt würde.
  # `installAtEnd` und `deployAtEnd` funktionieren nur mit aktuellen Versionen der entsprechenden Erweiteurngen.
  MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
  POSTGRES_DB: our_test_project_database
  POSTGRES_USER: our_test_project_user
  POSTGRES_PASSWORD: our_test_project_password

cache:
  paths:
    - /root/.m2/repository

stages:
  - build
  - test
  - release

validate:jdk8:
  stage: build
  script:
    - 'mvn $MAVEN_CLI_OPTS test-compile' 
  image: maven:3.5.0-jdk-8

deploy:jdk8:
  stage: test
  services:
    - postgres:9.6
  script:
    - 'mvn --settings settings.xml $MAVEN_CLI_OPTS -Dspring.profiles.active=gitlab deploy'
  image: maven:3.5.0-jdk-8

release_staging:
  stage: release
  image: williamyeh/ansible:centos7
  only: 
    - master
  tags:
    - ansible
  script:
    - 'ansible-playbook -i staging deploy.yml'

Gehen wir dies Schritt für Schritt durch. Zu Beginn deklarieren wir einige Variablen, die wir später gebrauchen können:

  # Dies unterdrückt ein erneutes Herunterladen von Abhängigkeiten und Plugins und verhindert das Anzeigen von Upload-Nachrichten im Log.
  # `showDateTime` wird die vergangene Zeit in Millisekunden anzeigen. Es muss `--batch-mode` mitgegeben werden, damit das funktioniert.
  MAVEN_OPTS: "-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
  # Seit Maven 3.3.0 kann diesen Option alternativ auch in `.mvn/maven.config` definiert werden. Damit würde dieselbe Konfiguration genutzt werden,
  # wenn dies von der Kommandozeile aus ausgeführt würde.
  # `installAtEnd` und `deployAtEnd` funktionieren nur mit aktuellen Versionen der entsprechenden Erweiteurngen.
  MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
  POSTGRES_DB: our_test_project_database
  POSTGRES_USER: our_test_project_user
  POSTGRES_PASSWORD: our_test_project_password

Als nächstes deklarieren wir eine Liste von Pfaden (tatsächlich ist es in diesem Fall nur einer), die zwischen den einzelnen Jobs aufbewahrt werden soll. Wir wollen nicht, dass Maven alle Abhängigkeiten wieder und wieder herunterlädt.

cache:
  paths:
    - /root/.m2/repository

Nachdem das erledigt ist, deklarieren wir drei sogenannte Stages. Ein Stage (auf Deutsch vielleicht am besten mit Phase übersetzt) kann genutzt werden, verschiedene Jobs zu gruppieren. Jobs, die zur gleichen Phase gehören, werden parallel ausgeführt. Unsere drei Stages sind build, test und release:

stages:
  - build
  - test
  - release

Es gibt noch einen weiteren wichtigen Aspekt die definierten Phasen betreffend: Jobs, die zu einer bestimmten Phase gehören, werden nur dann ausgeführt, wenn die Jobs der vorhergehenden Phase erfolgreich durchgeführt werden konnten. Das würde in unserem Fall bedeuten, dass die Jobs der test-Phase nur dann getriggert würden, wenn die Jobs der build-Phase alle erfolgreich ausgeführt wurden. Das ist sehr nützlich.

So sieht diese Sequenz der Phasen der Pipeline in Gitlab aus:

Bild 2: Überblick über die Pipeline.
Bild 2: Überblick über die Pipeline.

Nachdem die Grundlagen gelegt wurden, können wir uns dem Definieren der Jobs widmen, die Gitlab für uns ausführen soll. Der erste Job wird validate:jdk8 genannt:

validate:jdk8:
  stage: build
  script:
    - 'mvn $MAVEN_CLI_OPTS test-compile' 
  image: maven:3.5.0-jdk-8              

Dieser Job gehört zur build-Stage und hat die folgenden zwei Merkmale: (1) Es führt mvn test-compile mit den $MVN_CLI_OPTS aus, die wir oben in dem variables-Abschnitt definiert haben aus. Maven wird dann die Ressourcen verarbeiten, die Applikation und die dazugehörigen Tests kompilieren. (2) Es tut all dies innerhalb eines Docker-Images, in diesem Fall des maven:3.5.0-jdk-8-Images, welches auf Docker Hub gefunden werden kann.

Der nächste Job ist deploy:jdk8. Die Definition dieses Jobs sieht so aus:

deploy:jdk8:
  stage: test
  services:
    - postgres:9.6
  script:
    - 'mvn --settings settings.xml $MAVEN_CLI_OPTS -Dspring.profiles.active=gitlab deploy'
  image: maven:3.5.0-jdk-8

Dies ist ein Job der test-Phase. Es führt erneut Maven aus und nutzt das gleiche Docker-Image wie der validate:djk8-Job. Zusätzlich benötigt dieser Job noch eine laufende PostgreSQL-Datenbank. Es nutzt die Variablen, die wir oben im variables-Abschnitt gesetzt haben. Ja, so einfach ist das. Die Datenbank, die Gitlab uns dann zur Nutzung bereitstellt, ist erreichbar unter dem Hostnamen postgres. Mit Hilfe eines Spring-Profils , das wir für diesen Zweck erzeugt haben, passen wir die Verbindungs-URL für JDBC entsprechend an.

Der letzte Job ist der release-Job auf den wir alle gewartet haben. Schauen wir uns ihn noch einmal genauer an:

release_staging:
  stage: release
  image: williamyeh/ansible:centos7
  only: 
    - master
  tags:
    - ansible
  script:
    - 'ansible-playbook -i staging deploy.yml'

In diesem Fall installieren wir unsere Spring Boot-Applikation in einer Staging-Umgebung. Dieser Job ist Teil der release-Phase und wird nur dann ausgeführt, wenn der master-Branch aktualisiert wird. (Dies gilt nicht für die Jobs, die wir uns zuvor angeschaut haben, diese werden bei jedem gepushten Gitcommit ausgeführt.)

Eine Anmerkung: Es ist möglich, die Applikation nicht automatisch zu installieren oder zu aktualisieren, jedes Mal wenn der master-Branch aktualisiert wurde. Um zu erreichen, dass die Applikation nur durch manuelles Auslösen installiert oder aktualisiert wird, kann die Job-Definition um folgendes ergänzt werden: when: manual. Im Anschluss daran kann durch einen Klick auf den entsprechenden Button in Gitlab das Deployment ausgelöst werden. (Siehe "Play-Button" unten.) Die angepasste Job-Definition sähe dann so aus:

release_staging:
  stage: release
  image: williamyeh/ansible:centos7
  only: 
    - master
  when: manual
  tags:
    - ansible
  script:
    - 'ansible-playbook -i staging deploy.yml'

Bild 3: Pipeline mit manuallem Deployment.
Bild 3: Pipeline mit manuallem Deployment.

In beiden Fällen tut dieser Job genau eine Sache: Es führt ein Ansible-Playbook aus:

  script:
    - 'ansible-playbook -i staging deploy.yml'

Eine anderes Detail, dass wir noch beachten sollten, ist das tags-Element dieser Job-Beschreibung. Ein tag dient der Auswahl des GitLab-Runners fuer diesen Job. Jeder Runner kann einen oder mehrere Tags haben. Dieser Job hat denansible-Tag. Das bedeutet, dass der Gitlab-Runner für diesen Tag hier ausgewählt wird.

Das Docker-Image, dass wir hier verwenden, ist ein nicht-offizielles, aber sehr gutes Dockerimage mit CentOS 7, welches Ansible für uns bereitstellt. Es führt, wie oben schon besprochen, das Playbook aus.

Das Playbook ist das nächste, das wir uns genauer ansehen sollten:

---
- hosts: web
  become: true
  tasks:
    - name: Install python setuptools
      apt: name=python-setuptools state=present
    - name: Install pip
      easy_install: name=pip state=present
    - name: Install lxml
      pip: name=lxml
    - name: Download latest snapshot
      maven_artifact:
       group_id: our.springboot.application
       artifact_id: our_springboot_artifact_id
       repository_url: http://our_springboot_server.bevuta.com:8080/artifactory/our_springboot_application/
       username: our_springboot_user
       password: our_springboot_password
       dest: /home/our/springboot_application.jar
    - name: Copy systemd unit file
      copy: src=our_springboot_application.service dest=/etc/systemd/system/our_springboot_application.service
    - name: Start web interface
      systemd:
        state: restarted
        name: our_springboot_application
        enabled: yes
        daemon_reload: yes

Der erste Teil ist recht einfach: Dieses Playbook definiert Aufgaben für alle Hosts, die zur Gruppe web gehören. Und ja, Ansible soll auf den Zielsystemen als Root arbeiten dürfen, dafuer setzen wir become: true.

Der Rest definiert, was Ansible für uns erledigen soll. Zu Beginn installieren wir einige Python-Werkzeuge, die Ansible benötigt, um die maven_artifact-Aufgabe zu erledigen.

- name: Install python setuptools
  apt: name=python-setuptools state=present
- name: Install pip
  easy_install: name=pip state=present
- name: Install lxml
  pip: name=lxml

Das ist leider wenig elegant. Vielleicht kennen Sie einen besseren Weg, das zu tun? Lasst es uns wissen.

Nachdem wir die Zielsysteme vorbereitet haben, laden wir unser Artefakt herunter, das Gitlab für uns in Schritt 2 gebaut und in das Maven Repository hochgeladen hat:

- name: Download latest snapshot
  maven_artifact:
    group_id: our.springboot.application
    artifact_id: our_springboot_artifact_id
    repository_url: http://our_springboot_server.bevuta.com:8080/artifactory/our_springboot_application/
    username: our_springboot_user
    password: our_springboot_password
    dest: /home/our/springboot_application.jar

Zu guter Letzt (und endlich) starten wir unsere Spring Boot-Applikation automatisch, in dem wir eine systemd-Unit aktivieren, die Ansible für uns auf Zielsystemen angelegt hat:

- name: Copy systemd unit file
  copy: src=our_springboot_application.service dest=/etc/systemd/system/our_springboot_application.service
- name: Start web interface
  systemd:
    state: restarted
    name: our_springboot_application
    enabled: yes
    daemon_reload: yes

Eine Unit-Datei beschreibt eine sogenannte "systemd unit". Was genau das ist? Schauen wir mal in die "man page" davon: Eine "systemd"-Unit ist "a service, a socket, a device, a mount point, an automount point, a swap file or partition, a start-up target, a watched file system path …" ( man/systemd.unit).

In unserem Falle erzeugt Ansible eine Unit-Datei, die systemd wissen lässt, wie es unsere Spring Boot-Applikation behandeln soll, nämlich, dafür zu sorgen, dass diese beim Starten des Systems ebenfalls gestartet wird.

Und damit wären wir dann am Ende angekommen. Unsere Spring Boot-Applikation wurde gebaut, getestet, in ein Maven-Repository hochgeladen, daraus auf Zielsysteme heruntergeladen und gestartet. All dies automatisiert.