Ansible

Aus Laub-Home Wiki
Work in Progress:
Diese Seite wird erst noch aufgebaut. Bitte noch keine Änderungen vornehmen, ohne mit dem ursprünglichen Author zu sprechen!
Vielen Dank.
Das Logo des Ansible-Projektes

Ansible ist ein Orchestrierungs-Tool mit dessen Hilfe technische Prozessen im Infrastrukturbereich automatisiert werden können, dazu zählen u.a. Deployments, Systemprovisionierungen und Konfigurationsmanagement. Ursprünglich 2012 von Michael DeHaan veröffentlicht wurde die Technologie 2015 von Red Hat gekauft und seit dem von diesem und der Community weiterentwickelt. Der Kern von Ansible ist Open-Source, Red Hat bietet allerdings kommerzielle Zusatzpakete wie z.B. Ansible Tower und Enterprise Support an.

Dieser Artikel gibt eine Einführung in die Technologie.

Installation

TODO

Technische Konzepte

Der folgende Abschnitt gibt eine Einführung in die von Ansible festgelegten Konzepte inklusive kleiner Beispiele.

Ansible ist in Python geschrieben, ein Vorwissen der Programmiersprache ist für den normalen Gebrauch nicht notwendig. Für die Definitionen wird nahezu durchgehen die YAML-Syntax benutzt, hier ist es hilfreich, das Konzept von YAML vorab verstanden zu haben, ist aber nicht zwingend notwendig. YAML ist nicht kompliziert und wird bei der Einarbeitung in Ansible meist als "Beifang" mitgelernt.

Die Architektur sieht vor, dass eine beliebige Anzahl von Clients (Managed-Nodes) von einem einzigen Rechner aus (Control-Node) verwaltet werden können. Dabei muss der Control-Node nicht zwangsweise ein fester Server sein, sondern es kann sich hierbei auch um den lokalen Rechner handeln. Die Anweisungen und Inventories können auch einfach auf einen anderen Rechner übertragen und von diesem ausgeführt werden, d.h. es muss nicht nur einen Control-Server geben. Siehe dazu die Empfehlungen weiter unten bezüglich dem verteiltem Arbeiten mit Ansible.

Automatisierung im klassichen Ansatz hat meist bedeutet, dass der/die Administrator(en) über die Zeit eine Ansammlung von Skripten (z.B. Bash) erstellt und in eigen-Regie verwaltet haben. Diese Skripte waren oft eine Ansammlung von Befehlen, die auf dem jeweiligem System ausgeführt wurden. Ansible verfolgt einen etwas anderen Ansatz. In Ansible werden meist nicht Befehle, sondern Zustände definiert. Soll z.B. auf dem Ziel-System ein User erstell werden wurde im klassischen Ansatz ein "useradd"-Befehl in ein Skript gepackt. In Ansible wird definiert, dass der User mit den Attributen Name, Gruppe, Home-Verzeichniss, etc. auf dem Ziel-System existieren soll. Wird Ansible ausgeführt prüft dieses, ob der definierte Zusand existiert und wenn nein wird das System insofern angepasst, dass der gewünschte Zusand erreicht wird. Existiert der definierte User nicht, wird dieser z.B. erstellt. Exisitiert dieser, aber die ihm zugewiesenen Gruppen sind nicht korrekt, dann werden seine Gruppen entsprechend der Definition angepasst. Wie genau diese Anpassung erfolgt wird dabei komplett Ansible überlassen. Dies hat den Vorteil, dass der Admin sich meistens nicht mit Sonderfällen und Fehler-Verarbeitung kümmern muss und ist außerdem Ziel-Unabhängig aufgebaut, d.h. die Definitionen sind, OS-unabhängig. Aber Vorsicht! Dies ist leider nicht all-Umfassend in Ansible implementiert. Weitere Beispiele dazu werden weiter unten aufgeführt.

Im Hintergrund generiert Ansible Python Skripte, pusht diese über SSH auf die Clients und führt diese dann aus, daher sind die einzigen Anforderungen an die Clients, dass Python installiert und der Login per SSH aktiv sein muss, einen dedizierten Ansible-Client gibt es nicht (agent-less). Das hat den Vorteil, dass die Wartung von Ansible selbst nahezu kein Aufwand nötigt ist. Da die meisten Linux-Systeme bereits mit Python ausgeliefert und über SSH verwaltet werden sind fast alle Linux-Systeme unterstützt und können auch nachträglich von Ansible verwaltet werden.

Inventar

Welche Systeme ein Control-Node verwaltet wird über das sogenannte Inventar (Inventory) definiert. Dabei handelt es sich um eine einfache Text-Datei, in welcher alle Systeme eingetragen werden. Für diese Datei können 2 Formate genutzt werden: Das INI- und YAML-Format. Welches eingesetzt wird ist eine persönliche Entscheidung, das INI-Format ist etwas einfacher zu lesen, YAML ist aber struktierierter und auch mit dem Rest von Ansible konsistent.

Die folgenden Beispiele für Inventare werden in beiden Formaten aufgezeigt, beide stellen dabei die gleiche Struktur dar. Als erstes ein einfaches Inventar, welches 5 Systeme enhält:

# INI-Format
web.example.com
mail.example.com
chat.example.com
video.example.com
10.0.10.4
# YAML-Format
all:
  hosts:
    web.example.com:
    mail.example.com:
    chat.example.com:
    video.example.com:
    10.0.10.4:

Standardmäßig versucht Ansible, den im Inventory angegebenen Namen für eine SSH-Verbindung zu nutzten, d.h. es können sowohl DNS-Namen als auch IP-Adressen genutzt werden. Dies ist allerdings nicht zwingend, da die angegeben Hosts im Inventar eigentlich Alias sind. Die tatsächliche IP/der tatsächlichen Hostname kann optional über Variablen zum jeweiligen Host-Eintrag unabhängig gesetzt werden. Das hat den Vorteil, dass kurze, eindeutige Namen innerhalb Ansibles für den Verweis auf Systeme genutzt werden kann, wenn z.B. kein DNS zur Verfügung steht oder die Namen der Systeme sehr lang und/oder unleserlich sind. Als User für die SSH-Verbindung wird der aktuelle (Linux-)User genutzt. Zusätzlich kann ein Host-Eintrag auch um weitere Variablen erweitert werden, um spezifische Gegebenheiten der Umgebung zu berücksichtigen, z.B. ein alternativer SSH-Port. Es folgt ein Beispiel mit dem Inventar von oben, allerdings mit optionalen Variablen erweitert:

# INI-Format
webserver ansible_host=web.example.com     # Der Host 'webserver' ist eigentlich
                                           # 'web.example.com'
mail.example.com
chat.example.com ansible_port=20202        # Die SSH-Verbindung erfolgt über
                                           # Port 20202 anstatt 22
video.example.com ansible_user=ansible     # Benutze den SSH-User "ansible" für
                                           # Die Verbindung anstatt den
                                           # aktuellen User.
test.example.com ansible_host=10.0.10.4 ansible_port=2022 ansible_user=testuser
# YAML-Format
all:
  hosts:
    webserver:
      ansible_host: 'web.example.com'
    mail.example.com:
    chat.example.com:
      ansible_port: 20202
    video.example.com:
      ansible_user: 'ansible'
    test.example.com:
      ansible_host: '10.0.10.4'
      ansible_port: 2022
      ansible_user: 'testuser'

Wie zu sehen ist werden im INI-Format Variablen hinter den Host als "<key>=<value>" Paare geschrieben und durch Leerzeichen getrennt. Es können beliebig viele Variablen gentutzt werden. Bei YAML wird eine Variable als zusätzliches Element unterhalb des Hosts definiert. Eine Liste der vom Inventar supporteten Variablen kann hier eingesehen werden. Das Inventar kann einzelne System auch in Gruppen einteilen. Diese Gruppen können dann als ganzes in Ansible angesprochen werden. Dies hat den Vorteil, dass ein neuer Host einfach einer bestehenden Gruppe zugeordnet und dann natlos in bestehende Strukturen eingebunden werden kann. Hier ein Beispiel:

# INI-Format
chat.example.com
video.example.com

[webserver]
web01.example.com
web02.example.com
web03.example.com
web04.example.com

[mailserver]
mail01.example.com
mail02.example.com

[dbserver]
dba.example.com
dbb.example.com
dbc.example.com


# YAML-Format
all:
  hosts:
    chat.example.com:
    video.example.com:
  children:
    webserver:
      hosts:
        web01.example.com:
        web02.example.com:
        web03.example.com:
        web04.example.com:
    mailserver:
      hosts:
        mail01.example.com:
        mail02.example.com:
    dbserver:
      hosts:
        dba.example.com:
        dbb.example.com:
        dbc.example.com:

Gruppen können dabei auch verschachtelt werden. Das gleiche Beispiel hier nochmal mit einer zusätzlichen Gruppe "frontend", welche die extern verfügbaren Systeme enthält:

# INI-Format
chat.example.com
video.example.com

[webserver]
web01.example.com
web02.example.com
web03.example.com
web04.example.com

[mailserver]
mail01.example.com
mail02.example.com

[dbserver]
dba.example.com
dbb.example.com
dbc.example.com

[frontend:children]
webserver
mailserver
# YAML-Format
all:
  hosts:
    chat.example.com:
    video.example.com:
  children:
    frontend:
      children:
        webserver:
          hosts:
            web01.example.com:
            web02.example.com:
            web03.example.com:
            web04.example.com:
        mailserver:
          hosts:
            mail01.example.com:
            mail02.example.com:
    dbserver:
      hosts:
        dba.example.com:
        dbb.example.com:
        dbc.example.com:

An dieser Stelle zeigt sich auch ein Vorteil der YAML-Syntax gegenüber INI: In der YAML-Syntax kann eine Gruppe definiert werden, die sowohl Gruppen als auch normale Hosts enthält. In INI führt eine solche Definition leider zu einem Syntax-Fehler:

# INI-Format
# Nicht funktionsfähig, die Definition in der Gruppe "frontend" wird einen
# Syntax-Fehler werfen!

[webserver]
web01.example.com
web02.example.com
web03.example.com
web04.example.com

[mailserver]
mail01.example.com
mail02.example.com

[dbserver]
dba.example.com
dbb.example.com
dbc.example.com

[frontend:children]
chat.example.com      # Das hier wird einen Syntax-Fehler werfen!
video.example.com     # Das hier wird einen Syntax-Fehler werfen!
webserver
mailserver
# YAML-Format
# Im Gegensatz zum INI-Format wird hier kein Syntax-Fehler geworfen!

all:
  children:
    frontend:
      hosts:                    # Diese Definition funktioniert!
        chat.example.com:       # Diese Definition funktioniert!
        video.example.com:      # Diese Definition funktioniert!
      children:
        webserver:
          hosts:
            web01.example.com:
            web02.example.com:
            web03.example.com:
            web04.example.com:
        mailserver:
          hosts:
            mail01.example.com:
            mail02.example.com:
    dbserver:
      hosts:
        dba.example.com:
        dbb.example.com:
        dbc.example.com:

Als letzten Punkt soll genannt werden, dass in einem Inventar Hosts auch auch in Ranges definiert werden können. Das ist besonders von Vorteil, wenn eine großzahl von Systemen mit relativ gleichem Namen existieren und das Inventar nicht zu "aufgebläht" werden soll. In unserem Beispiel von oben haben wir in unseren Gruppen Systeme mit durchlaufenden Nummern bzw Buchstaben deklariert, also z.B. web01, web02 etc.. Die Syntax dafür ist "[<von>:<bis>]" und kann für Zahlen und Buchstaben benutzt werden. Hier nochmal das obere Beispiel, welches mit Ranges definiert wurde:

# INI-Format
chat.example.com
video.example.com

[webserver]
web[01:04].example.com

[mailserver]
mail[01:02].example.com

[dbserver]
db[a:c].example.com

[frontend:children]
webserver
mailserver
# YAML-Format
all:
  children:
    frontend:
      hosts:
        chat.example.com:
        video.example.com:
      children:
        webserver:
          hosts:
            web[01:04].example.com:
        mailserver:
          hosts:
            mail[01:02].example.com:
    dbserver:
      hosts:
        db[a:c].example.com:

Module

Module sind so etwas wie Ansible-native Befehl, sie werden mit bestimmten Variablen aufgerufen und das Modul führt dann den jeweiligen Befehl aus, meistens auf dem Ziel-System. Die meisten Module verfolgen dabei das bereits oben erwähnte Konzept der definierten Zustände. Jetzt könnte man vielleicht denken, dass es sich hierbei um das gleiche wie klassische Shell-Befehle handelt und es kommt der Sache schon nahe. Allerding dienen Module auch als eine Art Abstraktionsschicht, d.h. das Modul zum erstellen eines Users wird immer gleich definiert, Ansible kümmert sich darum, dass der User auf den verschiedenen Systemen gemäß der Definition existiert unabhängig von darunter liegenden OS.

Im Rahmen von Playbooks (näheres dazu siehe unten Playbooks) werden Module mit YAML deklariert, hier ein Beispiel:

- name: "Create a new user"
  user:
    name: "test"                     # Name des neuen Users
    group: "testgroup"               # Primäre Gruppe des neuen Users
    shell: "/bin/bash"               # Die Login-Shell des Users
    password: "<PASSWORD HASH>"      # Der Password-Hash des Users
    state: present                   # Status des Users, hier "vorhanden"
    system: no                       # Der neue User ist kein System-Account

Gehen wir das Ganze mal durch:

Ganz am Anfang steht "name", dass ist eine optionale Bezeichnung, die der Aktion gegeben werden kann, hier sollte immer etwas kurzes und sprechendes stehen. Wer das weg lässt macht sich schnell unbeliebt :-)

Der nächste Punkt sagt, dass die Aktion das Modul "user" aufrufen soll, die eingerückten weiteren Punkte sind, YAML typisch, eine Ansammlung von Key-Value-Paaren, die "user" unterstellt sind und den zu erstellenden User beschreiben. Ein Modul kann eine beliebige Zahl dieser Variablen haben, meistens werden aber nur die wenigsten zwingend benötigt. Das "user"-Modul hat z.B. über 30 verfügbare Variablen, die meisten haben aber gut gewählte Standard-Werte. Ein Überblick der vom Ansible-Projekt mitgelieferten Module (Core Modules) sowie der verfügbaren Variablen und Beispielen kann in der Dokumentation des Projektes eingesehen werden.

Es können auch eigene Module geschrieben und in Ansible ingetriert werden, die bereits mitgelieferten reichten jedoch in über 99% der Fälle locker aus. Module werden primär in Python geschrieben um eine durchgehende Kompatibilität zwischen den unterschiedlichen Systemen zu gewährleisten, allerdings kann theoretisch fast jede Programmiersprache verwendet werden. Der dazu gehörende Interpreter (z.B. Perl) muss dann aber auf dem Ziel-System verfügbar sein.

Wird ein Modul ausgeführt gibt es danach eine von 3 möglichen Status-Meldungen zurück:

Mögliche Status-Meldungen eines Ansible Moduls
Status Beschreibung
OK Das Modul wurde erfolgreich ausgeführt, es mussten keine Veränderungen am Ziel durchgeführt werden um den definierten Status zu erreichen
Changed Das Modul wurde erfolgreich ausgeführt, das Ziel-System wurde angepasst um den definierten Status zu erreichen
Failed Das Modul ist in einen kritischen Fehler gelaufen.

Gibt ein Modul den Status Failed zurück endet sofort die komplette Ausführung von Ansible für das jeweilige Ziel, auch wenn es noch nachfolgende Aktionen gibt, die theoretisch erfolgreich durchgeführt werden könnten. Zudem kann ein Modul noch zusätzlich informationen zurück geben wie z.B. der Status-Code eines dazugehörenden Kommandos oder eine Fehlermeldung.

Ad-Hoc Befehle

TODO

Playbooks

TODO

Rollen

TODO

Verteiltes Arbeiten

TODO

Quellen