Suche
Ansible - so geht's

Ansible Tutorial: Multi Tier Deployment mit Ansible

Daniel Stender

© Shutterstock /  Billion Photos

Ansible von Red Hat bietet automatisierte Konfiguration sowie Orchestrierung von Maschinenlandschaften. Das leichtgewichtige und flexible Werkzeug wird immer häufiger in DevOps-Toolchains und beim Cloud Computing eingesetzt. Dieser Artikel zeigt, wie man ein komplexes Multi-Tier-Setup damit auf Server bekommen.

Ansible bietet als Maschinenkonfigurations- und Provisionierungsframework automatisiertes und reproduzierbares Management von IT-Landschaften. Das relativ junge aber bereits recht ausgereifte Werkzeug (aktuell ist Version 2.6) gehört der Anwendungsklasse Infrastructure-as-Code (IaC) an, und steht in Konkurrenz zu teilweise schon seit längerem etablierten Lösungen wie Puppet, Chef und Salt/Saltstack.

Die Software wurde 2015 von Red Hat aufgekauft und wird zurzeit als Bestandteil des von diesem Anbieter angebotenen umfassenden Open Source Softwarestacks für die Anwendungsbereitstellung im Markt positioniert. Die auch außerhalb des kommerziellen Angebots verfügbaren CLI-basierten Werkzeuge werden dabei als Ansible Engine bezeichnet. Der Provisioner bietet ein klar strukturiertes und übersichtliches Komponentenmodell, das aus Maschinen-Inventar, Modulen, Playbooks und Rollen besteht, verfolgt bewusst einen leichtgewichtigen Ansatz und kann bereits nach relativ kurzer Einarbeitungszeit erfolgreich eingesetzt werden.

Ansible dient hauptsächlich der Steuerung von Linux, BSD- sowie Unix-Maschinen und kommt ohne Agenten auf den Zielsystemen aus. Es benötigt keinen zentralen Server und muss nur auf dem Kontrollknoten beziehungsweise dem Arbeitsrechner des Anwenders installiert und aktuell gehalten werden. Für Operationen auf einer Maschine wird ein SSH-Zugang und ein dort installierter Python-Interpreter (per Default 2.7) vorausgesetzt, Ansible setzt also auf einer höheren Ebene an als „Bare Metal“-Systeme ohne installiertes Betriebssystem, die dies nicht zur Verfügung stellen können.

Ansible-Projekte dienen dazu, eine oder mehrere Maschinen von einem bestimmten Zustand (meistens wird von der Basis-Installation eines Betriebssystems ausgegangen) in einen anderen bestimmten Zustand zu bringen (etwa als Web- oder Datenbankserver zu fungieren). Die Software verfolgt dabei einen imperativen Ansatz (wie wird etwas erreicht). Die Anhänger dieser Methode verweisen auf deren Vorteile, wozu etwa einfacheres Debugging und die volle Kontrolle gehören. Vom Anwender wird allerdings verlangt, den Ausgangszustand einer Maschine wie ein Administrator im Detail genau zu kennen und die dafür benötigten Prozeduren („plays“) mit den dafür erforderlichen Einzelschritten („tasks“) in richtiger Reihenfolge selbst in Ansible umzusetzen. Das kann schnell knifflig werden.

Ansible

Für das Skripten von Prozeduren in Ablaufplänen („playbook“) und für andere Elemente bedient sich Ansible des anwenderfreundlichen YAML-Formats und bietet als „Killer-Feature“ die eingebundene, mächtige Template-Sprache Jinja2, die als eigenständiges Projekt von der Pocoo-Gruppe entwickelt und auch in anderen Python-Projekten eingesetzt wird. Für das Umsetzen von verschiedenen administrativen Operationen auf Zielsystemen steht in Ansible ein umfangreicher Werkzeugkasten von Hunderten mitgelieferten Modulen zu Verfügung. Ein tiefgehendes Wissen über Ansible ist zu einem maßgeblichen Teil das Wissen darüber, welche Module es für welche Zwecke gibt und wie diese sich im Detail verhalten.

Einige Module bilden einen überschaubaren Basissatz für grundlegende administrative Handgriffe auf Zielsystemen, mit denen man dort etwa Dateien hochladen oder aus dem Netz einspielen, Pakete installieren, User und Verzeichnisse anlegen, Zeilen an Konfigurationsdateien anhängen oder darin verändern sowie Services neu starten kann. Manche Standardmodule wie zum Beispiel ping eignen sich vor allem dazu, mit dem mitgelieferten CLI-Tool ansible ad hoc und ohne Playbook ausgelöst zu werden. Darüber hinaus werden viele spezielle Module bereitgehalten mit denen man zum Beispiel mit MySQL-Servern umgehen kann, etwa um User und Datenbanken einzurichten; die für die Anwendung von einigen Modulen auf dem Zielsystem zusätzlich benötigten Python-Bibliotheken (wie bspw. für MySQL) kann man dann einfach als ersten Schritt in demselben Playbook installieren.

DevOpsCon Whitepaper 2018

Free: BRAND NEW DevOps Whitepaper 2018

Learn about Containers,Continuous Delivery, DevOps Culture, Cloud Platforms & Security with articles by experts like Michiel Rook, Christoph Engelbert, Scott Sanders and many more.

Es gibt teilweise alternative Module für einen bestimmten Zweck und manchmal auch alternative Wege, wie bestimmte Dinge erreicht werden können. Zudem lassen sich Module auch durchaus kreativ einsetzen, wie zum Beispiel das Modul subversion. Dieses kann man so entfremden, dass es auf Zielmaschinen einzelne Verzeichnisse direkt aus Github-Projekten einspielt. Das mitgelieferte CLI-Tool ansible-doc bietet auf dem Arbeitsrechner einen schnellen Zugriff auf die Dokumentationen von allen mitgelieferten Modulen, die Linux Man-Pages ähnelt. Eine große Rolle in den aktuelleren Versionen von Ansible spielen die dort mittlerweile voll etablierten, mit Maschinenprovisionierung nur im mittelbaren Zusammenhang stehenden Infrastruktur- und Cloud-Computing-Module, die sich das prozedurale Prinzip von Ansible für eigene Zwecke zunutze machen, zum Beispiel für das Hochziehen der Cloud-Infrastruktur und der Fernsteuerung der Netzwerk-Hardware.

Rollen in Ansible fungieren im Sinne von „Rolle, die eine Maschine einnimmt“ als eine Organisationsstruktur für die Bestandteile von Projekten. Ein komplexes Ansible-Projekt kann mehrere Rollen beinhalten, und eine einzelne Rolle bietet eine Reihe von fest definierten Verzeichnissen (Listing 1): tasks/ ist für die Playbooks, vars/ und defaults/ für die Definition von Variablen, sowie handlers/ für die Definition von Schaltern nehmen YAML-Konfigurationsdateien auf, und in den Verzeichnissen files/ und templates/ können Sie beliebige Dateien und Templates für das Aufspielen beim Ansible-Lauf bereit legen. Das Skelett für eine neue Rolle können Sie mit dem mitgelieferten CLI-Tool ansbile-galaxy erzeugen, wobei die nicht benötigten generierten Verzeichnisse mit enthaltenen Vorlagen (immer main.yml) einfach auch wieder gelöscht werden können; meta/ zum Beispiel ist hauptsächlich dafür gedacht, die Rolle über das offizielle Repositorium von Ansible – der Galaxy – zu vertrieben.

Der Anwender kann in Ansible-Projekten beliebige eigene Variablen definieren und zusammen mit den eingebauten Variablen, die alle mit ansible_ beginnen, bei Bedarf auswerten. Eine besondere Rolle spielen dabei die Fakten („facts“). Das sind umfangreiche Informationen, die Ansible bei Ausführung eines Playbooks von allen eingebundenen Maschinen sammelt und die man sich mit dem Modul setup für Entwicklungszwecke vollständig ausgeben lassen kann. Ansible recherchiert beim Lauf zum Beispiel also auch die IP-Adressen und Hostnamen von allen eingebundenen Maschinen und der Anwender kann sich diese bei komplexen Setups, bei denen Knoten einander erreichen können müssen, in Templates für Konfigurationsdateien evaluieren.

Beispiel

Als Beispiel für ein schon etwas komplexeres Ansible-Projekt mit mehreren Rollen soll hier ein mit Open Source-Software umgesetztes „klassisches“ Multi-Tier-Setup dienen, das aus drei Komponenten besteht. Auf einer Backend-Maschine läuft mit dem MySQL-Fork MariaDB ein relationaler Datenbankserver, auf welchem die bei vielen MySQL-Entwicklern bekannte Testdatenbank test_db installiert ist. Es handelt sich dabei um eine fingierte Personaldatenbank eines nicht-existierenden Großkonzerns mit sechs Tabellen, welche rund dreihunderttausend Personeneinträge und einige Millionen Gehaltsdaten enthält. Für die Benutzung dieser Datenbank ist auf Frontend-Maschinen ein mit dem Python-Webframework Flask [1] geschriebener Microservice installiert, der bei Aufruf diese Datenbank abfragt und ein JSON-Objekt zurückgibt. Dieses enthält den aktuellen Topverdiener jeweils einer der neun Firmenabteilungen, die in diesen Personaldaten mit aufgenomen sind. Als Grundlage für diese Webapplikation dient ein Apache-2-Webserver mit WSGI-Erweiterung, die Flask als Schnittstelle benötigt, um mit dem Server zu kommunizieren.

Auf einem weiteren Knoten ist der Load-Balancer HAProxy installiert, der zum Zwecke der Ausfallsicherheit und der Lastaufteilung Anfragen aus dem Netz auf eine beliebig hohe Anzahl von reduplizierten Frontend-Knoten mit derselben Flask-Applikation verteilt, die alle parallel auf dasselbe Backend mit der Datenbank zugreifen. Bei HAProxy handelt es sich um eine potente Softwarelösung auf Enterprise-Niveau, die von vielen namhaften Anbietern wie zum Beispiel Twitter und Github eingesetzt wird. Die mit Flask aufgesetzte Applikation macht auch über die gegenstandslosen Personendaten hinaus nur einen eingeschränkten Sinn, weil sich die Daten gar nicht verändern und die Abfragen immer dieselben Ergebnisse zurückgeben. Trotzdem handelt es sich um ein Gesamtsetup, dass in Abwandlungen häufiger in der Praxis vorkommt. Und Ansible eignet sich dafür, dieses gesamte Gebilde mit einem Knopfdruck vollständig automatisiert neu hochzuziehen, und dabei die benötigten Komponenten auf Maschinen („hosts“) zu deployen. Die Playbooks sind für Debian-9-Basisinstallationen geschrieben und sie enthalten einige angepasste Details wie zum Beispiel die verwendeten Paketnamen.

├── group_vars
│   └── all.yml
├── hosts
├── roles
│   ├── flask
│   │   ├── files
│   │   │   ├── querier.conf
│   │   │   └── querier.wsgi
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       └── querier.py.j2
│   ├── haproxy
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   └── main.yml
│   │   ├── templates
│   │   │   └── haproxy.cfg.j2
│   │   └── vars
│   │       └── main.yml
│   └── mariadb
│       ├── handlers
│       │   └── main.yml
│       └── tasks
│           └── main.yml
└── site.yml

Backend

Für die Installation des MariaDB-Servers benötigt man lediglich ein Playbook (Listing 2). Auf Dateien, die auf dem Kontrollknoten bereit gelegt wurden kann verzichtet werden, denn die Beispieldatenbank lässt sich direkt aus dem Netz einspielen. Das Playbook installiert zunächst mit dem Modul apt für den Paketmanager von Debian das Paket mariadb-server aus dem offiziellen Archiv. Es werden für spätere Operationen noch zwei weitere Pakete auf diesem Host benötigt, nämlich python-mysqldb für die MySQL-Module von Ansible und unzip, um die Beispieldatenbank auszupacken. Es spricht nichts dagegen, alle drei Pakete in einem Schritt zu installieren. Dafür gibt es in Ansible die Variable item, die man in Verbindung mit with_items beliebig einsetzen kann, um eigene Iteratoren zu konstruieren, wie hier im Beispiel. Variablen werden bei Ansible beziehungsweise Jinja2 immer mit doppelt geschwungenen Klammern expandiert und die Schreibweise in den Beispielen (es gibt noch eine andere) verlangt, diese Konstrukte in Anführungsstriche zu setzen. Der Datenbankserver ist nach der Installation bereits aktiviert, das Playbook legt dann in zwei weiteren Schritten mit den Modulen mysql_db und mysql_user eine neue, leere Datenbank und einen Benutzer dafür an. Das in der benutzerdefinierten Variable employees_password voreingestellte Kennwort wird auch für die Flask-Applikation benötigt. Deshalb bietet es sich an, dass man es nicht in beiden Rollen parallel in vars/ definiert, sondern zentral auf übergeordneter Ebene in group_vars/ (Listing 13).

- name: MariaDB und benötigte Pakete installieren
  apt:
    name: "{{ item }}"
    state: latest
    update_cache: yes
  with_items:
    - mariadb-server
    - python-mysqldb
    - unzip

- name: Datenbank "employees" anlegen
  mysql_db:
    name: employees
    state: present

- name: SQL-Benutzer "employees" anlegen
  mysql_user:
    name: employees
    host: "%"
    password: "{{ employees_password }}"
    priv: "employees.*:ALL"
    state: present

- name: check ob test_db bereits importiert ist
  stat:
    path: /var/lib/mysql/employees/employees.frm
  register: testdb_imported

- name: test_db von Github einspielen
  unarchive:
    src: https://github.com/datacharmer/test_db/archive/master.zip
    dest: /tmp
    remote_src: yes
  when: testdb_imported.stat.exists == false

- name: Pfade in Importskript anpassen
  replace:
    path: /tmp/test_db-master/employees.sql
    regexp: "source "
    replace: "source /tmp/test_db-master/"
  when: testdb_imported.stat.exists == false

- name: test_db importieren
  mysql_db:
    name: all
    state: import
    target: /tmp/test_db-master/employees.sql
  when: testdb_imported.stat.exists == false

- name: MariaDB für Fernzugriff freischalten
  lineinfile:
    dest: /etc/mysql/mariadb.conf.d/50-server.cnf
    regexp: "^bind-address(.+)127.0.0.1"
    line: "bind-address = 0.0.0.0"
    backrefs: yes
  notify: restart mariadb
Ende

Die nächsten Schritte dienen der Installation der Testdatenbank, aber zunächst legt man am besten einen Prüfmechanismus an, um zu verhindern, dass dies bei jedem neuen Ansible-Lauf erneut geschieht, denn das erzeugt unnötigen Overhead. Dafür eignet sich das Modul stat, mit dem sich prüfen lässt, ob die Datei employees.frm bereits existiert (was der Fall ist, wenn die Datenbank bereits installiert worden ist), und die Rückgabe des Moduls lässt sich mit register in einer Variable aufnehmen (hier in testdb_imported). Im nächsten Schritt wird mit dem Modul unarchive die Datenbank als ZIP-Datei aus GitHub in /tmp eingespielt und dieses dann ausgepackt. Das geschieht aber nur (when), wenn der Rückgabewert von testdb_imported.stat.exists negativ ist. Das replace-Modul passt dann einige Pfade in dem Importskript aus dem ZIP-Archiv an und auch hier sowie beim nächsten Schritt legt das Playbook mit when dieselbe Bedingung für die Ausführung fest. Der nächste Schritt setzt wieder das Modul mysql_db ein, um die ausgepackte Personaldatenbank mittels des mitgelieferten Importskripts im MariaDB-Server zu installieren.

Um den Datenbankserver für Zugriffe aus dem Netz freizuschalten, ist es nötig, eine Zeile in einer Konfigurationsdatei unter /etc/myslq zu ändern, was sich mit dem Modul lineinfile bewerkstelligen lässt. Die Option backrefs für dieses Modul verhindert, dass dieselbe Zeile bei erneuten Durchlauf dieses Playbooks noch einmal geschrieben wird: Sie wird nämlich sonst immer wieder angehangen, falls der regexp-Ausdruck in dieser Datei nicht (mehr) gefunden wird. Dieser Schritt aktiviert mit notify bei Bedarf (falls das Modul als Ergebnis changed zurückgibt) den Schalter („handler“) restart mariadb, der in Listing 3 für das Modul service definiert ist. Der Handler sorgt dafür, dass der MariaDB-Server seine Konfigurationsdateien neu einliest, dabei wird mit enabled gleichzeitig festgelegt, dass der zugehörige Systemd-Service nach einem Reboot der Zielmaschine wieder aktiv sein soll (das Modul stellt bei Bedarf die Service-Unit um). Durch den Einsatz von Handlern anstatt von festen Schritten kann man bei dem Modul service etwa verhindern, dass Dienste bei einem erneuten Durchlauf des Playbooks immer wieder neu geladen oder neu gestartet werden, was ohne weitere Änderungen an der Konfiguration auch nicht nötig ist. Die imperative Methode von Ansible verlangt, dass man genau mitdenkt, wenn man Prozeduren schreibt und auch immer den wiederholten Durchlauf eines Playbooks beachtet.

---
- name: restart mariadb
  service:
    name: mariadb
    state: restarted
    enabled: true

Frontend

Das Playbook zum Aufsetzen des Frameworks (Listing 4) ist ein wenig kürzer, dafür muss man aber mehrere Dateien in dieser Rolle bereitlegen, damit Ansible sie aufspielen kann. Zunächst werden in einem Schritt wieder einige Pakete installiert: der Webserver apache2, die dazugehörige WSGI-Erweiterung, Flask, und wieder dieselbe Python-Bibliothek für MySQL, diesmal allerdings nicht für Ansible-Module, sondern für die Flask-Anwendung. Danach spielt man mit copy einen WSGI-Starter (Listing 5) auf der Zielmaschine ein. Dieses Modul sucht automatisch in files/ in dieser Rolle, sodass man bei src keinen Quellpfad angeben muss. Der Zielpfad wird vom Modul automatisch angelegt, falls er noch nicht vorhanden ist. Der WSGI-Prozess benötigt einen Pseudo-User auf dem Zielsystem, um nicht mit Root-Rechten laufen zu müssen. Das Playbook legt diesen dann mit dem Modul user an.

 
- name: Apache und benötigte Pakete installieren
  apt:
    name: "{{ item }}"
    state: latest
    update_cache: yes
  with_items:
    - apache2
    - libapache2-mod-wsgi
    - python-flask
    - python-mysqldb

- name: WSGI-Starter aufspielen
  copy:
    src: querier.wsgi
    dest: /var/www/querier/

- name: Pseudo-User für WSGI-Prozesss anlegen
  user:
    name: wsgi
    shell: /bin/false
    state: present

- name: Applikation aufspielen
  template:
    src: querier.py.j2
    dest: /var/www/querier/querier.py
    owner: wsgi
    mode: 0600
  notify: reload apache

- name: Konfiguration für virtuellen Host aufspielen
  copy:
    src: querier.conf
    dest: /etc/apache2/sites-available/
  notify: reload apache

- name: virtuellen Host enablen
  file:
    src: /etc/apache2/sites-available/querier.conf
    dest: /etc/apache2/sites-enabled/querier.conf
    state: link

- name: Default-Startseite disablen
  file:
    path: /etc/apache2/sites-enabled/000-default.conf
    state: absent
import sys
sys.path.insert(0, '/var/www/querier')
from querier import app as application

Als nächsten Schritt kann man die eigentliche Applikation querier.py (Listing 6) aufspielen. Das Python-Skript ist als Ansible Template (mit der Endung .j2) aufgesetzt und muss dementsprechend mit dem Modul template anstatt copy verarbeitet werden. Dafür liegt es in templates/ bereit. Beim Aufspielen auf dem Zielsystem werden im Template einige Ansible-Variablen aus den Fakten expandiert, so finden sich hinterher die IP-Adresse (ansible_eth0_ipv4.address) und der Hostname (ansible_hostname) des jeweiligen Zielsystems hier hard-gecoded wieder. Diese gibt die laufende Applikation dann mit dem JSON-Objekt und dem Ergebnis der Datenbankabfrage aus, wobei das Ganze der Kontrolle dient, woher die Rückgabe eigentlich stammt.

Mit hostvars gewinnt man auch noch die IP-Adresse des Datenbank-Backends aus dem Inventar (Listing 14), welche die Applikation für die Abfrage der Datenbank über das Netz benötigt. Wie beim Backend wird hier dann noch employees_password für das Zugriffs-Kennwort (Listing 13) evaluiert. Dieser Schritt ist mit dem Handler reload apache verknüpft (Listing 8), der ausgelöst wird, wenn man Änderungen am Template vornimmt und das Playbook dann wieder durchlaufen lässt, um die neue Fassung zu deployen. Im nächsten Schritt spielt das Playbook die Konfiguration für den virtuellen Host für Apache (Listing 7) ein, in dem die Flask-App laufen wird.

from flask import Flask
import json
import MySQLdb as mysqldb

app = Flask(__name__)

ipv4 = '{{ ansible_eth0.ipv4.address }}'
hostname = '{{ ansible_hostname }}'

mydb = mysqldb.connect(user = 'employees',
    host = '{{ hostvars[groups.datenbank.0].ansible_default_ipv4.address }}',
    passwd = '{{ employees_password }}',
    db = 'employees')

@app.route("/")
def topearners(abteilung):
    cursor = mydb.cursor()

    command = cursor.execute("""SELECT e.last_name, e.first_name, d.dept_no,
        max(s.salary) as max_sal FROM employees e
        JOIN salaries s ON e.emp_no = s.emp_no AND s.to_date > now()
        JOIN dept_emp d ON e.emp_no = d.emp_no
        WHERE d.dept_no = 'd%s'
        GROUP BY e.emp_no ORDER BY max_sal desc limit 1;""" % abteilung)

    results = cursor.fetchall()
    daten = (results[0])
    (nachname, vorname, abteilung, gehalt) = daten
    resultsx = (abteilung, vorname, nachname, gehalt, ipv4, hostname)
    return json.dumps(resultsx)
<VirtualHost *:80>
 WSGIDaemonProcess querier user=wsgi group=wsgi threads=5
 WSGIScriptAlias / /var/www/querier/querier.wsgi
 <Directory /var/www/querier>
  WSGIProcessGroup querier
  WSGIApplicationGroup %{GLOBAL}
  Order allow,deny
  Allow from all
 </Directory>
</VirtualHost>
---
- name: reload apache
  service:
    name: apache2
    state: reloaded
    enabled: yes

Zwei weitere Schritte aktivieren dann den virtuellen Host mit der bei Apache üblichen Methode. Zunächst muss dafür mit dem Modul file ein Softlink unterhalb von /etc/apache2 angelegt werden. Anschließend löscht man dann mit demselben Modul den Softlink für die Default-Startseite von Apache. Da an diesen Schritten keine Änderungen in der Zukunft zu erwarten sind (außer es wird irgendwann bei Apache anders geregelt), ist es überflüssig, sie mit einem Handler zu verknüpfen, der immer wieder bei Bedarf wirksam wird. Das stellt aber kein Problem beim ersten Durchlauf dar, weil ein bei vorherigen Schritten getriggerter Handler wie reload apache in Ansible immer erst am Ende des Playbooks ausgelöst wird. Damit werden diese Änderungen an der Default-Konfiguration von Apache ebenfalls wirksam. Der Schalter reloaded beim Service-Modul startet den Dienst übrigens auch immer, falls das noch nicht geschehen ist. Dies ist etwa bei der Installation des Pakets apache2 nicht der Fall.

Load Balancer

Für die Installation des Load-Balancers benötigt man, in Verbindung mit einem Handler (Listing 9), lediglich zwei Schritte im Playbook (Listing 10), denn die entscheidende Dinge spielen sich hier im Template für die Konfigurationsdatei für HAProxy (Listing 11) ab. Unter backend (also im Backend vom Load Balancer) müssen die zu berücksichtigenden Knoten eingebunden werden. Dies wird durchgeführt, indem man mit einer for-Schleife, die Jinja2 zur Verfügung stellt, über die Knoten in der Gruppe applikation im Inventar (Listing 14) iteriert und dabei mit hostvars deren IP-Adressen und Hostnamen aus den Fakten in diese Konfigurationsdatei schreiben lässt.

- name: reload haproxy
  service:
    name: haproxy
    state: reloaded
    enabled: yes
- name: haproxy-Paket installieren
  apt:
    name: haproxy
    state: latest
    update_cache: yes

- name: Konfiguration einspielen
  template:
    src: haproxy.cfg.j2
    dest: /etc/haproxy/haproxy.cfg
    backup: yes
  notify: reload haproxy
global
  daemon
  maxconn 256

{% if statspage %}
listen stats
  bind 0.0.0.0:9000
  mode http
  stats enable
  stats uri /haproxy?stats
  stats refresh 15s
{% endif %}

defaults
  mode http
  timeout connect 10s
  timeout client 1m
  timeout server 1m

frontend http
  bind {{ balancer_listen_address }}:{{ balancer_listen_port|default('80') }}
  default_backend querier
  
backend querier
{% for host in groups['applikation'] %}
  server {{ hostvars[host].ansible_hostname }} {{ hostvars[host].ansible_default_ipv4.address }}:80 check
{% endfor %}
Ende

Die Programmierelemente von Jinja2 wie if und for stehen immer innerhalb von einfachen geschwungenen Klammern mit Prozentzeichen, wie weiter oben in dieser Datei bei if statspage: Dieser Block für das HAProxy-Dashboard wird nur ausgegeben, falls statspage auf true gestellt ist, wie im Beispiel das Masterplaybook beim Aufruf der Rolle mitgibt (Listing 15). Unter frontend (also im Frontend vom Load Balancer) werden zwei weitere selbst definierte Variablen ausgewertet. Diese sind nur für diese Rolle gültig, weshalb sie am besten unter vars/ (Listing 12) definiert werden. Für den Port, auf dem HAProxy auf Anfragen wartet, ist hier mit einem Variablenfilter (default) die Zahl 80 voreingestellt. Möchte man das ändern, definiert man einfach balancer_list_port entsprechend um (Listing 12).

---
balancer_listen_address: 0.0.0.0
#balancer_listen_port: 80
employees_password: fbfad90d99d0b4
Ende

Deployment

Um diese Rollen anzuwenden, benötigt man noch zwei weitere Bestandteile im Ansible-Projekt. Einerseits ein Inventar bzw. eine Inventar-Datei (mit beliebigem Namen, häufig aber hosts oder inventory) und ein Masterplaybook (häufig site.yml). Ansible-Inventare werden im einfachen INI-Format geschrieben, nehmen IP-Adressen oder DNS-Hostnamen auf und können darin das dem Projekt zugehörige Maschineninventar beliebig gruppieren (Listing 14).

[datenbank]
167.99.242.69

[applikation]
167.99.242.84
167.99.242.179
167.99.242.237

[load-balancer]
167.99.250.42

[all:vars]
ansible_user=root
ansible_ssh_private_key_file=~/.ssh/id_digitalocean
Ende
---
- hosts: datenbank
  roles:
    - mariadb

- hosts: applikation
  roles:
    - flask

- hosts: load-balancer
  roles:
    - {role: haproxy, statspage: true}

Die datenbank-Gruppe enthält nur eine Maschine. Mehrere sind möglich und werden homogen bestückt, wobei die Applikation aber nur den ersten Knoten (groups.datenbank.0 in Listing 6) berücksichtigt. Unter applikationen kann man beliebig skalieren und über die drei Server im Beispiel hinausgehen wenn eine extreme Anfragelast zu erwarten ist, denn das Template für HAProxy iteriert wie gesagt über alle hier eingetragenen Knoten. Bei Bedarf kann man auch später einfach weitere Maschinen hinzufügen. Hierzu muss Ansible erneut gestartet werden, um die neu hinzugekommenen Knoten zu bestücken und den Load Balancer im Hinblick darauf zu aktualisieren.

Weitere load-balancer-Knoten sind natürlich auch möglich und funktionieren genauso, aber alternative Zugänge soll der Einsatz von HAProxy ja gerade überwinden. Ausgefallene Frontend-Knoten werden von den verbleibenden automatisch aufgefangen. Möchte man den Ausfall der Datenbank oder des Load-Balancers allerdings kompensieren, muss ein noch komplexeres Setup mit eingebautem Monitoring vorhanden sein. Das für diesen Artikel angewandte Beispiel bedient sich virtueller Server von DigitalOcean (Abbildung 1). Der für den Zugang darauf benötigte Username (ansible_user) und Pfad zum privaten SSH-Schlüssel auf dem Arbeitsrechner (ansible_ssh_private_key_file) lassen sich direkt im Inventar einstellen.

Ansible Tutorial 1

Das Masterplaybook (Listing 15) verknüpft die Gruppen aus dem Inventar (hosts) mit den Rollen im Projekt und legt die Reihenfolge fest, in welcher das Deployment stattfinden soll. Dafür wird einfach ansible-playbook -i hosts site.yml ausgelöst und Ansible arbeitet dann das gesamte Projekt durch. Beim Durchlauf wird eine Log-Datei ausgegeben, die die einzelnen Schritte aufführt und anzeigt, ob Änderungen stattgefunden haben (was beim ersten Durchlauf überall der Fall ist). Bereits nach einigen Minuten ist das gesamte Setup fertig installiert. Dann muss einfach der Load Balancer angesprochen werden, dafür stehen die neun Abteilungen des fingierten Konzerns als Endpunkte (001-009) zur Verfügung:

$ curl 167.99.250.42/001
["d001", "Akemi", "Warwick", 145128, "167.99.242.179", "frontend2"]
$ curl 167.99.250.42/002
["d002", "Lunjin", "Swick", 142395, "167.99.242.237", "frontend3"]
$ curl 167.99.250.42/003
["d003", "Yinlin", "Flowers", 141953, "167.99.242.84", "frontend1"]

Die zurückgegebenen JSON-Objekte enthalten zu Kontrollzwecken immer zuerst noch einmal die abgefragte Abteilung, den Vor- und Nachnamen des jeweiligen Topverdieners darin, das aktuelle Jahresgehalt dieser Person sowie jeweils die IPv4-Adresse und den Hostnamen des Frontends, von dem die Rückgabe stammt. Ruft man das Dashboard von HAProxy auf (Abbildung 2), kann man die Arbeit des Load Balancers verfolgen. Es dauert immer erst ein paar Sekunden, bis das Ergebnis einer Anfrage eintrifft, denn es müssen auf dem Backend jedes Mal immerhin rund 160 MB an Daten durchgearbeitet werden. Wenn die Anfragen in die Breite gehen sollen, dann sollte man für den MariaDB-Server also besser potente Hardware zur Verfügung stellen und zusätzlich tiefergehende Tuningmaßnahmen vornehmen.

Ansible Tutorial 2

Fazit

Bei Ansible handelt es sich um ein einschlägiges Werkzeug, um Applikationen auf Servern zu deployen. Nicht nur Single-Node- sondern auch Multi-Tier-Setups können damit umgesetzt werden. Ansible entfaltet dabei seine volle Stärke und bietet mit den Rollen ein probates Mittel an, um komplexe Projekte zu strukturieren. Das Beispiel hat gezeigt, wie man diesen Provisionierer anwendet, um ein Setup mit drei untereinander kommunizierenden Komponenten auf fünf Knoten zu deployen. Außerdem zeigte es, wie Playbooks und die eingebauten Module eingesetzt werden, um prozedural angeordnete Schritte auf den Zielmaschinen auszuführen.

Das Beispielsetup ist nicht produktionsreif und für potentielle Angreifer eher eine Herausforderung auf Grundschulniveau: der Datenbankserver zum Beispiel ist nicht abgesichert (es ist kein Root-Kennwort gesetzt und anonymer Zugang ist möglich), die Frontends sind individuell ansprechbar, die internen Verbindungen sind ungesichert (man würde dafür auch eher auf ein privates Netzwerk setzen), der Load Balancer ist nicht über HTTPS ansprechbar usw. Das Hardening des MariaDB-Servers würde man entsprechend dem dabei mitgelieferten Skript mysql_secure_installation auch mit Ansible-Werkzeugen umsetzen. Es sollte hier aber natürlich nicht das Setup selbst im Vordergrund stehen, sondern wie man so ein Konstrukt mit Ansible automatisiert installiert bekommt und Ansatzpunkte für eine ausführlichere Beschäftigung mit diesem Werkzeug bieten. Aber Vorsicht, wie viele andere DevOps-Tools hat Ansible ein gewisses Suchtpotential.

Verwandte Themen:

Geschrieben von
Daniel Stender
Daniel Stender
Daniel Stender (Homepage: http://www.danielstender.com) ist zertifizierter Linux-Admin und Debian Developer (stender@debian.org). Er arbeitet als freier DevOps Engineer und sendet Ihnen bei Interesse gerne sein aktuelles CV.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: