Über unsMediaKontaktImpressum
Dr. Bernhard Hopfenmüller 21. Mai 2019

Einführung in Ansible

Genesis – mit diesem Titel beschrieb Michael de Haan am 23.2.2012 den Commit, der den Startschuss für Ansible bilden sollte. Heute (Stand März 2019) und knapp 44.000 Commits später ist Ansible vermutlich eines der meist etablierten Open-Source-Orchestrierungstools. Dass Red Hat Ansible 2014 gekauft hat, dürfte den finalen Schub gegeben haben. Höchste Zeit also, für alle die sich bisher nicht damit befasst haben, einen Einblick zu wagen und erste Schritte zu tätigen.

Zunächst einige Begrifflichkeiten: Mit Ansible kann ein Master-Server einen Slave-Server konfigurieren. Dazu verbindet sich der Master per SSH mit dem Slave und führt dann tasks aus. Jeder Task beschreibt einen Konfigurationsschritt, also zum Beispiel das Installieren eines Pakets mittels yum. Dabei ruft jeder Task ein Modul auf, dass die aktuelle Aufgabe umsetzt, beispielsweise das yum-Modul. Soll eine Datei kopiert werden, wird das copy-Modul benutzt, für das Managen für Systemd-Services kann das systemd-Modul zum Einsatz kommen, usw. Ansible liefert in Version 2.7 ca. 2.100 Module mit. Darüber hinaus können zusätzliche Module einfach importiert werden.

Erste Schritte

Der erste Schritt beim Benutzen von Ansible ist das Schreiben eines Inventories. Dort legen Nutzer fest, welche Hosts orchestriert werden sollen, darüber hinaus lassen sie sich zu Gruppen zusammenfassen. Inventories lassen sich entweder im YAML- oder im INI-Format verfassen. Im folgenden Beispiel soll ein Webserver deployed werden, dazu legt man folgendes Inventory an:


---
all:
  hosts:
    webserver1:
      ansible_host: 192.168.0.2

Die dict-keys all und hosts sind dabei von Ansible vorgegeben und auch reserviert, dürfen also nicht anderweitig verwendet werden. Zur Liste aller hosts wird nun ein neuer Server hinzugefügt, der in Ansible als webserver1 bekannt sein soll. Darüber hinaus wird Ansible mitgeteilt, dass er unter der IP 192.168.0.2 verfügbar sein soll. Dazu dient der Parameter ansible_host. Möchte man nicht nur einen Webserver, sondern drei und zusätzlich einen Datenbankserver verwalten, fügt man diese Server ebenfalls zum Inventar hinzu:


---
all:
  hosts:
    webserver1:
      ansible_host: 192.168.0.2
    webserver2:
      ansible_host: 192.168.0.3
    webserver3:
      ansible_host: 192.168.0.4
    dbserver:
      ansible_host: 192.168.1.1

Prinzipiell ist das Inventory an dieser Stelle fertig, allerdings empfiehlt es sich, gleich noch die Definition von Inventory-Gruppen anzulegen, um beispielsweise alle Webserver kollektiv anzusprechen. Dazu wird ein children dict-key eingeführt:


---
all:
  hosts:
    webserver1:
      ansible_host: 192.168.0.2
    webserver2:
      ansible_host: 192.168.0.3
    webserver3:
      ansible_host: 192.168.0.4
    dbserver1:
      ansible_host: 192.168.1.1
  children:
    webservers:
      hosts:
        webserver1:
        webserver2:
        webserver3:
    dbservers:
      dbserver1:

Nach Definition eines Inventories kann Ansible generell auf zwei Arten benutzt werden: Der Ad-hoc-Modus erlaubt das Ausführen einzelner Tasks, also einmaliger Modulaufrufe. Beispielsweise kann man zunächst testen, ob alle Server aus dem Inventory für Ansible erreichbar sind:

$ ansible -i inventory.yaml all -m ping

Mit -i wird das zu benutzende Inventory angegeben. Da Ansible alle hosts des Inventories ansprechen soll, benutzt man das Schlüsselwort all. Alternativ könnte man an dieser Stelle auch Gruppennamen webservers und dbservers oder die Namen einzelner Hosts (webserver1, ...) schreiben. Mit -m wird das Modul für den Task spezifiziert. Das Modul Ping bewirkt dabei mehr als ein klassischer ICMP-ping. Es prüft, ob eine SSH-Verbindung zwischen Master und Slave möglich ist und ob auf dem Slave-Server eine kompatible Python-Version installiert ist.

Die Ausgabe dieses Kommandos könnte folgendermaßen aussehen:

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

Nach diesem erfolgreichem Check können nun weitere Ad-Hoc-Kommandos ausgeführt werden, etwa die Installation eines Paketes:

$ ansible -i inventory.yaml webservers -m package -a "name=nginx state=present" -b

webserver1 | SUCCESS => {
    "cache_update_time": 1553085209,
    "cache_updated": false,
    "changed": true,
    "stderr": "",
    "stderr_lines": [],
    "stdout": "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following additional packages will be installed:\n  libnginx-mod-http-geoip libnginx-mod-http-image-filter\n  libnginx-mod-http-xslt-filter libnginx-mod-mail libnginx-mod-stream\n  nginx-common nginx-core\nSuggested packages:\n  fcgiwrap nginx-doc\nThe following NEW packages will be installed:\n  libnginx-mod-http-geoip libnginx-mod-http-image-filter\n  libnginx-mod-http-xslt-filter libnginx-mod-mail libnginx-mod-stream nginx\n  nginx-comm
    .........
}

Das Flag -b bewirkt eine priviledge escalation, sodass bspw. ein neues Paket installiert werden kann. Mit dem Flag -a werden die Argumente übergeben, die ein Modulaufruf beinhalten soll. In diesem Fall ist das der Name des Pakets und der gewünschte Zustand, also zum Beispiel, dass ein Paket installiert werden soll.

Ansible genügt – wie die meisten anderen Tools dieser Art – dem Idempotenzprinzip. Nutzer schreiben keine konkreten Befehle, sondern definieren, welchen Zustand ein System erreichen soll, also dass ein Paket installiert sein soll. Falls – auf den konkreten Fall zugeschnitten – nginx auf einem Webserver vorhanden sein soll, wird Ansible entweder das Paket neu installieren, oder einfach nichts tun, wenn das Paket schon vorhanden ist. Dieses Verhalten lässt sich auch beobachten, wenn der Aufruf ein zweites Mal getätigt wird:

$ ansible -i inventory.yaml webservers -m package -a "name=nginx state=present" -b

webserver1 | SUCCESS => {
    "cache_update_time": 1553085209,
    "cache_updated": false,
    "changed": false
}

webserver2 | SUCCESS => {
    "cache_update_time": 1553085209,
    "cache_updated": false,
    "changed": false
}

webserver3 | SUCCESS => {
    "cache_update_time": 1553085209,
    "cache_updated": false,
    "changed": false
}

Ansible benötigt – wie jedes Konfiguration-Management-System – Informationen über den Host, den es verwalten soll:

  • Betriebsystem
  • CPU
  • RAM
  • Netzwerk
  • installierte Pakete
  • ...

Um diese einzusammeln bzw. anzuzeigen gibt es das Modul setup:

$ ansible  -i inventory.yaml webserver1 -m setup

webserver2 | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "192.168.0.3",
            "192.168.122.1",
        ],
        "ansible_all_ipv6_addresses": [
            "45f8:f3c3:fdca:4667:3a27:70d1:dc72:112c",
            "c3e9:1192:aeb6:c013:497b:51b0:e1b4:5910",
        ],
        "ansible_apparmor": {
            "status": "enabled"
        },
        "ansible_architecture": "x86_64",
        "ansible_bios_date": "05/08/2017",
        "ansible_bios_version": "1.3.3",
        ...
      }
}

Installation und Konfiguration

Das Installieren und Konfigurieren eines Webservers beinhaltet normalerweise mehr als einen Task:

  1. Konfiguration der Firewall
  2. Installieren des nginx-Pakets
  3. Anpassen der nginx-Konfiguration
  4. Neustarten des nginx-Services
  5. Setzen einer message-of-the-day pro Server

Will man mehrere Tasks kombinieren, zu benutzt man ein Playbook. In diesem wird in YAML-Syntax zunächst mit dem hosts key definiert, auf welchen hosts des Inventories das Playbook laufen soll, im nachstehenden Beispiel auf allen Mitgliedern der webservers-Gruppe. Anschließend werden unter dem key tasks alle Aufgaben definiert, die außerdem in eben dieser Reihenfolge ablaufen sollen. Nachstehend ist das fertige Playbook, das in seinen Einzelschritten erklärt werden soll.


- hosts: webservers
  tasks:
    - name: Open port 80 for http access
      become: true
      firewalld:
        service: http
        permanent: true
        state: enabled
      register: firewall_setting

    - name: Restart the firewalld service to load in the firewall changes
      become: true
      service:
        name: firewalld
        state: restarted
      when: firewall_setting.changed

    - name: Install packages for nginx
      become: true
      package:
        name: "nginx-{{ nginx_version }}"
        state: present

    - name: Copy Stylsheet in place
      become: true
      copy:
        src: stylsheet.css
        dest: "{{ nginx_root }}/stylesheet.css"
      register: stylesheet

    - name: Create default page
      become: true
      template:
        src: index.html.j2
        dest: "{{ nginx_root }}/index.html"
      register: indexfile

    - name: Restart nginx service
      become: true
      service:
        name: nginx
        state: restarted
      when: indexfile.changed or stylesheet.changed

    - name: Setup motd
      become: true
      template:
        src: motd.j2
        dest: /etc/motd

Die Ordnerstruktur sieht nun folgendermaßen aus:

.
|
├── inventory.yaml
└── playbook.yaml

Der erste Task benutzt das firewalld-Modul, um den http-Service innerhalb der Firewall zuzulassen. Der Task hat zusätzlich einen register-Parameter. Gibt an diesen an, speichert Ansible das Ergebnis des Tasks in einer Variable ab. Zusätzlich beinhaltet der Task den Parameter become: true. Damit wird dieser Task mit Rechteeskalation (Standard ist sudo) ausgeführt. Diese Berechtigung kann zwar auch global für das gesamte Playbook gesetzt werden, es empfiehlt sich aber aus Sicherheits- und Auditgründen fein-granular zu arbeiten und jeden Task einzeln zu "berechtigen".

Nach der Neukonfiguration der Firewall soll diese neu gestartet werden – dazu kommt das service-Modul zum Einsatz, dass unter anderem für systemd-basierte Services gedacht ist.

Das Neustarten soll allerdings nur passieren, falls der vorhergehende Task eine Änderung nach sich gezogen hat: Dieses Verhalten wird über den when-Parameter und die Abfrage der Variable firewall_setting, die im ersten Task definiert wurde gesteuert.

Im dritten Task installiert das package-Modul das nginx-Paket auf den Webservern. Dazu wird einerseits der gewünschte Zustand übergeben – in diesem Fall present –, zum anderen der Namen des Pakets nginx. Alternativ könnte auch der Zustand latest benutzt werden, dies würde die neueste Version referenzieren, die das System über seine jeweiligen Paketquellen bekommt. Im vorliegenden Fall wird als Name {{ nginx_version | default("1.15.5")}} angegeben. Dies soll das erste Beispiel sein, wie in Ansible Variablen zum Einsatz kommen können.

In Ansible wird dazu Jinja-Syntax benutzt. Variablen werden immer in {{ }}-Blöcke gefasst. Hier muss also eine Variable definiert werden, die nginx_version heißt. Diese kann z. B. in einer Datei ./group_vars/webservers.yaml gespeichert werden:


---
nginx_version: 1.1.15

Damit sieht die Ordnerstruktur folgendermaßen aus:

.
|
├── group_vars
│   └── webservers.yaml
├── inventory.yaml
└── playbook.yaml

Im späteren Verlauf sollen Variablen noch detaillierter zur Sprache kommen. Für den Moment soll aber noch erwähnt werden, dass alle Variablen die in group_vars/webservers.yaml definiert wurden, allen hosts zur Verfügung stehen, die zur Inventory-Gruppe webservers gehören. Analog kann man eine Datei group_vars/all.yaml anlegen, deren Werte dann für alle Hosts aller Inventory-Gruppen gelten.

Die nächsten beiden Tasks sollen nun eine statische Webseite ausliefern, die Informationen über das Betriebssystem des Servers anzeigen soll. Dazu wird zunächst mittels copy-Modul ein CSS-Stylesheet in den root-Ordner des nginx-Webservers kopiert, das die statische Webseite später benutzen soll. Bevor das passieren kann, muss noch die Variable nginx_root gesetzt werden:


#group_vars/all.yaml
---
nginx_version: 1.12.2
nginx_root: "/usr/share/nginx/html"

Für die Website selbst bedient man sich der Host-facts, die vorhin bereits im Zusammenhang mit dem setup-Modul verwendet wurden. Falls nicht explizit unterbunden, sammelt Ansible bei jedem Playbook-Run als allerersten "internen" Task diese facts ein, bevor der erste User-definierte Task läuft. Daher stehen dann im Verlauf des Playbooks Variablen wie: ansible_distribution, ansible_os_family, ansible_all_ipv4_adresses, usw. zur Verfügung.

Für die Webseite wird folgendes Template verwendet:

<html>
<head>
<title>Information page: {{ ansible_hostname }}</title>
<style type="text/css">@import url('./stylesheet.css') all;</style>
</head>
<body>
<h1>Information about running host</h1>

<table>
  <tr> <th colspan='2'>OS Facts</th> </tr>
  <tr> <td>This system is running on</td> <td>{{ ansible_distribution }}</td> </tr>
  <tr> <td>Version:</td> <td>{{ ansible_distribution_version }}</td> </tr>
  <tr> <td>OS Family:</td> <td>{{ ansible_os_family}}</td> </tr>
  <tr> <td>Used package manager:</td> <td>{{ ansible_pkg_mgr }}</td> </tr>
  <tr> <td>AppArmor</td> <td>{{ ansible_apparmor['status'] }}</td> </tr>
  <tr> <td>Selinux</td> <td>{{ ansible_selinux['status'] }}</td> </tr>
  <tr> <td>Python Version</td> <td>{{ ansible_python_version }}</td> </tr>
</table>

<table style='float:left;margin-right:50px'>
  <tr> <th colspan='2'>Network information</th> </tr>
  <tr> <td>Hostname</td> <td>{{ ansible_nodename }}</td> </tr>
  <tr> <td>IPv4 addresses</td> <td>{{ ", ".join(ansible_all_ipv4_addresses) }}</td> </tr>
  <tr> <td>IPv6 addresses</td> <td>{{ ", ".join(ansible_all_ipv6_addresses) }}</td> </tr>
  <tr> <td>DNS servers</td>    <td>{{ ", ".join(ansible_dns['nameservers']) }}</td> </tr>
  <tr> <td>DNS search domain</td> <td>{{ ", ".join(ansible_dns['search']) }}</td> </tr>
  {% for key, value in ansible_default_ipv4.iteritems() %}
  <tr> <td>Default IPv4 interface -- {{ key }}</td> <td>{{ value }}</td> </tr>
  {% endfor %}
  {% for key, value in ansible_default_ipv6.iteritems() %}
  <tr> <td>Default IPv6 interface -- {{ key }}</td> <td>{{ value }}</td> </tr>
  {% endfor %}
  <tr> <td>Hostname</td> <td>{{ ansible_hostname }}</td> </tr>
  <tr> <td>FQDN</td> <td>{{ ansible_fqdn }}</td> </tr>
</table>

<table>
  <tr> <th colspan='2'>Environment variables</th> </tr>
  {% for key, value in ansible_env.iteritems() %}
  <tr> <td>{{ key }}</td><td>{{ value }}</td> </tr>
  {% endfor %}
</table>

</body>
</html>

Die in Jinja-Syntax eingebundenen Variablen werden dann von Ansible ausgefüllt, wenn die Datei mittels template-Modul an die gewünschte Stelle kopiert wird. Template-Dateien bekommen die Dateiendung .j2. Mit der Template-Datei und dem Stylesheet erhält man dann folgende Ordnerstruktur:

.
|
├── group_vars
│   └── webservers.yaml
├── index.html.j2
├── inventory.yaml
├── playbook.yaml
└── stylesheet.css

Der nachfolgende Task verläuft analog zum Neustarten des Firewall-Dienstes: Sofern sich die index-Datei oder das Stylesheet beim Deployment ändern, soll nginx neu gestartet werden. Das braucht es an sich nicht unbedingt; die ausgelieferten Dateien könnte man auch auch im laufenden Betrieb ändern. Der letzte Task soll nun noch eine Message-of-the-day setzen, also eine Nachricht, die beim Einloggen in der Server per SSH angezeigt wird. Auch hier wird wieder das template-Modul benutzt, um die gewünschte Datei nach etc/motd zu kopieren. Es wird also eine motd.j2-Templatedatei hinzugefügt:

.
|
├── group_vars
│   └── webservers.yaml
├── index.html.j2
├── inventory.yaml
├── motd.j2
└── playbook.yaml

# motd.j2
Hello and welcome on {{ ansible_hostname }}.
This is a {{ ansible_distribution }}-Server running in Version {{ ansible_distribution_version }}.
This is the {{ webserver_name }} instance.
Please note: This server is deployed and managed with Ansible.
Hands off!

                        @@@@@@@@@@@@@@@
                    @@@@@@@@@@@@@@@@@@@@@@
                 @@@@@@@@@@@@@@@@@@@@@@@@@@@@
              @@@@@@@@@@@@@@@    @@@@@@@@@@@@@@
            @@@@@@@@@@@@@@@@      @@@@@@@@@@@@@@@
         @@@@@@@@@@@@@@@@@@       @@@@@@@@@@@@@@@@@@
       @@@@@@@@@@@@@@@@@@@        @@@@@@@@@@@@@@@@@@@
      @@@@@@@@@@@@@@@@@@@    @@    @@@@@@@@@@@@@@@@@@@
     @@@@@@@@@@@@@@@@@@@    @@@@    @@@@@@@@@@@@@@@@@@@
    @@@@@@@@@@@@@@@@@@@    @@@@@@    @@@@@@@@@@@@@@@@@@@
   @@@@@@@@@@@@@@@@@@@     @@@@@@@    @@@@@@@@@@@@@@@@@@@
   @@@@@@@@@@@@@@@@@@@     @@@@@@@     @@@@@@@@@@@@@@@@@@
   @@@@@@@@@@@@@@@@@@         @@@@@    @@@@@@@@@@@@@@@@@@
    @@@@@@@@@@@@@@@@@    @@      @@@@    @@@@@@@@@@@@@@@@
    @@@@@@@@@@@@@@@    @@@@@@      @@    @@@@@@@@@@@@@@@
     @@@@@@@@@@@@@     @@@@@@@@      @    @@@@@@@@@@@@@
      @@@@@@@@@@@     @@@@@@@@@@@@         @@@@@@@@@@@
       @@@@@@@@@@    @@@@@@@@@@@@@@@        @@@@@@@@@
        @@@@@@@@    @@@@@@@@@@@@@@@@@@@     @@@@@@@
           @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
             @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
                 @@@@@@@@@@@@@@@@@@@@@@@@

Die ansible_*-Variablen kommen analog zum Website-Template aus den Host-facts. Das gilt jedoch nicht für webserver_name. Da diese Variable pro Server verschieden sein soll, muss diese auch dementsprechend festgelegt werden. Dafür gibt es mehrere Möglichkeiten. An dieser Stelle sollen zwei vorgestellt werden.

1. Analog zu den Variablen die für Inventory-Gruppen im group_vars-Ordner festgelegt wurden, kann das auch für einzelnen hosts passieren:

.
|
├── group_vars
│   └── webservers.yaml
├── host_vars
│   ├── webserver1.yaml
│   ├── webserver2.yaml
│   └── webserver3.yaml
├── index.html.j2
├── inventory.yaml
├── motd.j2
└── playbook.yaml

# host_vars/webserver1.yaml
---
webserver_name: "Webserver 1"

# host_vars/webserver2.yaml
---
webserver_name: "Webserver 2"

# host_vars/webserver3.yaml
---
webserver_name: "Webserver 3"

Variablen, die so festgelegt wurden, gelten dann nur für den zum File gleichnamigen Host.

2. Variablen auf host-Ebene können auch im Inventory-file festgelegt werden:


# inventory.yaml
---
all:
  hosts:
    webserver1:
      ansible_host: 192.168.0.2
      webserver_name: "Webserver 1"
    webserver2:
      ansible_host: 192.168.0.3
      webserver_name: "Webserver 2"
    webserver3:
      ansible_host: 192.168.0.4
      webserver_name: "Webserver 3"
    dbserver1:
      ansible_host: 192.168.1.1
  children:
    webservers:
      hosts:
        webserver1:
        webserver2:
        webserver3:
    dbservers:
      dbserver1:

Damit ist das Playbook prinzipiell fertig und lauffähig:

$ ansible-playbook playbook.yaml -i inventory.yaml

PLAY [webservers] *************************************************************

TASK [Gathering Facts] ********************************************************
ok: [webserver1]
ok: [webserver2]
ok: [webserver3]

TASK [Open port 80 for http access] *******************************************
changed: [webserver1]
changed: [webserver2]
changed: [webserver3]

TASK [Restart the firewalld service to load in the firewall changes] **********
changed: [webserver1]
changed: [webserver2]
changed: [webserver3]

TASK [Install packages for nginx] *********************************************
changed: [webserver1]
changed: [webserver2]
changed: [webserver3]

TASK [Create default page] ****************************************************
changed: [webserver1]
changed: [webserver2]
changed: [webserver3]

TASK [Restart nginx service] **************************************************
changed: [webserver1]
changed: [webserver2]
changed: [webserver3]

TASK [Setup motd] *************************************************************
changed: [webserver1]
changed: [webserver2]
changed: [webserver3]

PLAY RECAP ********************************************************************
webserver1                 : ok=7    changed=6    unreachable=0    failed=0
webserver2                 : ok=7    changed=6    unreachable=0    failed=0
webserver3                 : ok=7    changed=6    unreachable=0    failed=0

Wie man sieht, wird das Einsammeln der Fakten als erster Task gewertet, dieser ist der einzige Task, der beim initialen Ausführen keine Änderung bewirkt. Damit man beim Ausführen eines Playbooks nicht immer das Inventory mit angeben muss, kann man noch eine Konfigurationsdatei ansible.cfg mit anlegen:

.
|
├── ansible.cfg
├── group_vars
│   └── webservers.yaml
├── host_vars
│   ├── webserver1.yaml
│   ├── webserver2.yaml
│   └── webserver3.yaml
├── index.html.j2
├── inventory.yaml
├── motd.j2
└── playbook.yaml
# ansible.cfg
[defaults]
inventory=./inventory

Ansible legt standardmäßig in /etc/ansible.cfg eine default-Konfigurationsdatei an. Diese lässt sich jedoch mit einer Datei ~/.ansible.cfg oder einer lokalen, wie in diesem Fall überschreiben. Ansible priorisiert diese Dateien in umgekehrter Reihenfolge:

  1. Pfad spezifiziert in ANSIBLE_CONFIG-Umgebungsvariable
  2. lokal in ./ansible.cfg
  3. ~/.ansible.cfg
  4. /etc/ansible.cfg

In der Datei lässt sich nicht nur ein (oder beliebig viele zusätzliche) Inventory angeben, sondern Ansible auch in zahlreichen anderen Wegen konfigurieren:

  • SSH-Parameter, Optionen,
  • zusätzliche externe Module,
  • Verbindungsparameter,
  • ...

Viele dieser Parameter lassen sich zudem auch als Umweltvariablen setzen. Eine komplette Liste lässt sich mit dem CLI-Tool ansible-config anzeigen.

Das Rollenkonzept

Das finale Playbook beinhaltet nun eine Anzahl von Tasks, allerdings sind diese nicht ohne weiteres wiederverwendbar. Um dieses Problem zu lösen und generell mehr Modularität zu schaffen, bietet Ansible das Rollenkonzept an. Überträgt man dieses auf das nginx-Beispiel, so ändert sich die Ordnerstruktur:
.
├── ansible.cfg
├── group_vars
│   └── webservers.yaml
├── host_vars
│   └── webserver1.yaml
├── inventory.yaml
├── playbook.yaml
└── roles
    └── nginx
        ├── defaults
        │   └── main.yaml
        ├── files
        │   └── stylsheet.css
        ├── tasks
        │   └── main.yaml
        ├── templates
        │   ├── index.html.j2
        │   └── motd.j2
        └── vars
            └── main.yaml

Auch das Playbook sieht nun anders aus:


- hosts: webservers
  roles:
    - nginx

Dort werden nun keine Tasks mehr aufgerufen, sondern eine Liste von roles. Im vorliegenden Fall beinhaltet diese List nur ein Element: die Rolle für die nginx-Installation. Diese Rolle findet sich im Unterordner nginx des neue Ordners roles. Der Haupteinstiegspunkt für eine Rolle ist die Datei main.yaml im Ordner tasks. Dort finden sich nun alle tasks wieder, die zuvor im Playbook vorhanden waren:


# roles/nginx/tasks/main.yaml
---
- name: Open port 80 for http access
  become: true
  firewalld:
    service: http
    permanent: true
    state: enabled
  register: firewall_setting

  [...]

Darüber hinaus hat eine Rolle folgende Ordner:

  • files: Hier finden sich Dateien (außer Templates!), die die Rolle verwendet. In diesem Fall stylesheet.css.
  • templates: Hier finden sich templates, die die Rolle verwendet. In diesem Fall die website selbst und die motd-Vorlage.
  • vars/defaults: Hier finden sich Variablen wieder, die eine Rolle verwendet. Je nach Verwendungszweck können diese sowohl in /vars/main.yaml als auch in defaults/main.yaml gespeichert werden.

Für die Platzierung von Variablen an verschiedenen Stellen ist es wichtig, die Rangfolge dieser Definitions-Stellen zu verstehen. Nachfolgende Liste beinhaltet (in aufsteigender Rangfolge) nur die Stellen, die bisher in diesem Artikel erwähnt wurden. Eine vollständige Liste findet sich in der Ansible-Dokumentation wieder [1].

  1. Variablen in /role/defaults/main.yaml,
  2. Variablen in inventory.yaml,
  3. Variablen in group_vars/*.yaml,
  4. Variablen in host_vars/*.yaml,
  5. Variablen die aus dem setup-Modul automatisch bestimmt wurden und
  6. Variablen in /role/vars/main.yaml.

Stellt man sich eine Rolle vor, die einen Apache-Webserver auf Debian installieren soll, ergeben sich folgende Möglichkeiten für die Variablenplatzierung:

In roles/apache/defaults/main.yaml wird die Standard-Versionsnummer der Apache-Installation festgelegt. Diese kann bei Bedarf später einfach überschrieben werden. Zudem wird auch der Standard-Pfad für das root-Verzeichnis des Webservers gesetzt und die Ports, die zum Einsatz kommen sollen.


--- roles/apache/defaults/main.yaml
apache_root: "/var/www/html"
apache_version: 2.4.29
apache_listen_port: 80
apache_listen_ssl_port: 443

roles/apache/vars/main.yaml beinhaltet den Namen der Pakete, die im Verlauf der Rolle installiert werden. Diese sind fix, d.h. ein Überschreiben ist nicht notwendig.


---
apache_packages:
  - apache2
  - apache2-utils

Eine gute Rolle ist wiederverwendbar und übertragbar, soll heißen: Sie ist nicht auf beispielsweise ein einzelnes Betriebssystem beschränkt. Greift man das Apache-Beispiel wieder auf, gibt es beispielsweise Unterschiede in der Paketnamensgebung für Debian und Redhat. Um eine Rolle hierzu generisch benutzen zu können, macht man sich wieder einmal Ansible facts zu Nutzen.

Die nachfolgenden Beispiele sind auf [2] zu finden. Die Verzeichnisstruktur sieht vereinfacht folgendermaßen aus:

.
├── playbook.yaml
└── roles
    └── apache
        ├── handlers
        │   └── main.yaml
        ├── tasks
        │   └── main.yaml
        └── vars
            ├── Debian.yaml
            └── RedHat.yaml

Je nach vorliegendem Betriebssystem sollen entweder die Variablen aus roles/apache/vars/Debian.yaml oder roles/apache/vars/RedHat.yaml eingebunden werden:


---
#roles/apache/vars/Debian.yaml
apache_server_root: /etc/apache2
apache_service_name: apache2
apache_packages:
  - apache2
  - apache2-utils

--- #roles/apache/vars/RedHat.yaml
apache_server_root: /etc/httpd
apache_service_name: httpd
apache_packages:
  - httpd
  - httpd-devel
  - mod_ssl #Debian liefert das SSL Modul bereits in `apache2` mit aus

In roles/apache/tasks/main.yaml findet sich folgendes:


---
- name: Include OS-specific parameters
  include_vars: {{ ansible_os_family }}.yaml

- name: Install apache packages
  package:
    name: "{{ apache_packages }}"
    state: present
  notify: restart apache

- name: Activate mod_ssl if Debian
  file:
    src: "{{ apache_server_root }}/mods-available/mod_ssl"
    dest: "{{ apache_server_root }}/mods-enabled/mod_ssl"
    state: link
  notify: restart apache
  when: ansible_os_family == 'Debian'"

Der erste Task bindet mit dem Modul include_vars das für die aktuelle Distribution passende Variablenfile ein. Der zweite Task installiert dann die Pakete, die im jeweiligen File definiert wurden. Der letzte Task stellt eine Debian-Eigenheit dar:

Die Apache-Installation bringt standardmäßig diverse Module bereits mit. Um sie zu aktivieren kann entweder das CLI-Tool a2enmod benutzt werden, oder man erstellt "von Hand" File-Links von

/etc/apache2/mods-available/<mod_name> 

nach

/etc/apache2/mods-enabled/<mod_name>


Letzteres passiert auch im hier gezeigten Task, mit Hilfe des file-Moduls. Die when-Zeile stellt für einen Task sicher, dass er nur ausgeführt wird, wenn die nachfolgende Bedingung als true evaluiert wird. In diesem Fall ein Betriebssystem aus der Debian-Familie.

Nach Aktivierung eines Moduls muss noch der Server neu gestartet werden. Dazu wird an dieser Stelle das handler-Konzept benutzt. Handler sind tasks, die an beliebiger Stelle im Playbook getriggered werden können. Nach Abschluss des Playbooks werden dann alle Handler, die im Verlauf des Playbooks getriggered wurden, in Reihenfolge ihrer Definition abgearbeitet. Im Apache-Beispiel gibt es genau einen Handler-Task:


#/roles/apache/handlers/main.yaml
---
- name: restart apache
  service:
    name: "{{ apache_service_name }}"
    state: restarted

Das Triggern eines Handlers erfolgt im Kontext eines tasks mit dem Key notify und dem Task-Namen des Handlers. Bei genauerem Hinsehen fällt auf, dass auch der Task, der die Apache-Pakete installiert, selbigen Handler aufruft. Dies ist der Tatsache geschuldet, dass im RedHat-Szenario das Apache-SSL-Modul als eigenes Paket installiert wird. Im Debian-Fall wird der Handler damit zweimal getriggered. Dies bedeutet aber nicht, dass er auch zweimal ausgeführt wird – auch das mehrfache Triggern eines Handlers bedingt nur eine einmalige Ausführung.

Mit dem Tags-Konzept kann Ansible einzelne Tasks eines Playbooks ausüben und alle anderen überspringen. Das lässt sich gut anhand eines Beispiels erläutern: Eine Firma schreibt Java-Webanwendungen, die sie mittels Maven in war-Files paketiert und dann auf einem TomCat-Server deployed. Jede Nacht soll dabei die neueste Test-Version auf einem TomCat ausgebracht und getestet werden. Sowohl die initiale Installation des TomCats als auch
das nächtliche Deployen der neuesten Version sollen mit Ansible passieren. Dazu wurde folgendes Playbook geschrieben:


---
- name: Install Tomcat Server
  import_tasks: tomcat_setup.yaml
  tags:
    - inital_setup

- name: Deploy war file
   maven_artifact:
    group_id: com.company
    artifact_id: web-app
    version: latest
    extension: war
    repository_url: 'https://repo.company.com/maven'
    dest: /var/lib/tomcat7/webapps/web-app.war
  notify: restart_tomcat
  tags:
    - initial_setup
    - nightly_test

Der erste Task importiert mittels import_tasks eine yaml-Datei, die alle Tasks für die Installation und Konfiguration eines TomCat-Servers beinhaltet. Der zweite Task lädt die neueste Version der Webapp als war-file herunter, schiebt sie in den Webapp-Ordner des TomCat-Servers und triggered einen Handler, der den TomCat-Server durchstartet. Zusätzlich besitzen beide Tasks tags: Der erste nur initial_setup, der zweite initial_setup und nightly_test.

Durch diese Definition lässt sich folgendes bewerkstelligen:

  • ansible-playbook tomcat.yaml lässt das Playbook wie gewohnt laufen, alle Tasks werden ausgeführt.
  • ansible-playbook tomcat.yaml --tags <tag_name> führt alles Tasks aus, die mit dem entsprechenden Tag ausgestattet wurden.
  • ansible-playbook tomcat.yaml --tags initial_setup würde in diesem Fall auch beide Tasks ausführen. Hingegen kann mit ansible-playbook tomcat.yaml --tags nightly_test jede Nacht nur die neue Version der Webapplikation deployed werden, ohne dass Ansible auch das initiale Setup erneut auf Durchführung und Änderungen prüft.

Fazit

Der vorliegende Artikel soll und kann natürlich keine umfassende und vollständige Darstellung von Ansible sein: Zu jedem der hier genannten Punkte lässt sich noch vieles mehr sagen. Bei Interesse bietet sich zum einen die offizielle Ansible-Dokumentation an [3], zum anderen die Community: Sei es in Form von IRC-Channels (#ansible, #ansible-dev) oder aber die zahlreichen einschlägigen Meet-ups und Vorträge, die es mittlerweile gibt.

Die hier gezeigten Ideen und Konzepte wollen abschließend genug vermitteln, um einen ersten Einblick zu bekommen und den Einstieg zu wagen – es lohnt sich.

Dr. Bernhard Hopfenmüller auf den IT-Tagen 2019

Dr. Bernhard Hopfenmüller hält einen Vortrag auf den diesjährigen IT-Tagen – der Jahreskonferenz der Informatik Aktuell.

Apache Kafka – a system optimized for writing
(10.12.2019, 09:00 Uhr)

Autor

Dr. Bernhard Hopfenmüller

Dr. Bernhard Hopfenmüller ist promovierter Physiker und arbeitet im Linux-Open-Source-Umfeld. Seine Spezialgebiete sind Ansible und andere typische DevOps-Themen.
>> Weiterlesen
botMessage_toctoc_comments_9210