Ansible für Einsteiger — RHEL und Ubuntu Server

Ein praxisorientiertes Tutorial für den Einstieg in die Automatisierung mit Ansible.

Dieses Tutorial richtet sich an Administratoren, die Linux-Server bereits von der Kommandozeile aus bedienen, aber noch nie mit einem Konfigurationsmanagement-Werkzeug gearbeitet haben. Alle Beispiele funktionieren gleichzeitig auf Red Hat Enterprise Linux 9 und Ubuntu Server 24.04 LTS — wer von Anfang an mit zwei Distributionen arbeitet, lernt automatisch sauberer zu automatisieren.

Zeitbedarf: etwa 6–8 Stunden inklusive der Übungen.


1. Einleitung

1.1 Was ist Ansible und wofür wird es eingesetzt

Ansible ist ein Werkzeug zur Automatisierung von IT-Infrastruktur. Man beschreibt in Textdateien den gewünschten Zustand eines Systems — welche Pakete installiert, welche Dienste gestartet und welche Konfigurationsdateien vorhanden sein sollen — und Ansible sorgt dafür, dass die Zielsysteme diesen Zustand erreichen.

Der entscheidende Perspektivwechsel für Einsteiger:

Klassisches Shell-Skript Ansible
Beschreibt Schritte („installiere Paket X“) Beschreibt Zustände („Paket X soll vorhanden sein“)
Zweiter Durchlauf kann Schaden anrichten Zweiter Durchlauf ändert nichts
Distributionsspezifisch Abstrahiert über Module
Fehlerbehandlung muss selbst gebaut werden Bricht bei Fehlern kontrolliert ab

Typische Einsatzgebiete sind die Grundkonfiguration frisch installierter Server, das Ausrollen von Anwendungen, Patch-Management, Compliance-Prüfungen und die Orchestrierung mehrstufiger Abläufe über mehrere Systeme hinweg.

1.2 Agentless-Prinzip: Push statt Pull

Auf den verwalteten Systemen muss keine Ansible-Software installiert werden. Ansible meldet sich per SSH an, überträgt ein kleines Python-Programm (das Modul), führt es aus, holt das Ergebnis ab und löscht das Programm wieder.

   Control Node                        Managed Nodes
   ┌────────────────┐                  ┌──────────────────┐
   │  ansible-core  │  ── SSH ──────▶  │  RHEL 9          │
   │  Playbooks     │                  │  python3, sshd   │
   │  Inventory     │                  └──────────────────┘
   │                │                  ┌──────────────────┐
   │                │  ── SSH ──────▶  │  Ubuntu 24.04    │
   └────────────────┘                  │  python3, sshd   │
                                       └──────────────────┘

Daraus folgen zwei praktische Konsequenzen:

1.3 Abgrenzung zu Puppet, Chef, Salt

Werkzeug Modell Agent nötig Sprache der Definitionen
Ansible Push nein YAML
Puppet Pull ja eigene DSL
Chef Pull ja Ruby
Salt beides meist ja YAML

Ansible ist für den Einstieg deshalb attraktiv, weil die Einstiegshürde niedrig ist: kein Server, kein Agent, keine neue Programmiersprache. Der Preis dafür ist geringere Geschwindigkeit bei sehr großen Umgebungen und das Fehlen einer permanenten Zustandsüberwachung.

1.4 Voraussetzungen für dieses Tutorial

Kenntnisse: Umgang mit der Bash, Grundverständnis von SSH, Paketverwaltung und systemd.

Technik: Drei Systeme — physisch, virtuell oder als Container. Für ein reines Lernsetup genügen jeweils 1 vCPU und 1 GB RAM. Alternativ funktioniert das gesamte Tutorial auch mit einer einzigen Maschine, auf der zwei VMs unter KVM, VirtualBox oder Vagrant laufen.

Verwende für die Übungen keine Produktivsysteme. Ansible führt Änderungen ohne Rückfrage aus, sobald ein Playbook startet.


2. Die Testumgebung aufbauen

2.1 Aufbau: ein Control Node, zwei Managed Nodes

Rolle Hostname Betriebssystem Beispiel-IP
Control Node ctrl beliebig (hier: Ubuntu 24.04) 192.168.56.10
Managed Node 1 rhel9 RHEL 9 192.168.56.11
Managed Node 2 ubuntu24 Ubuntu Server 24.04 192.168.56.12

Tipp: Wer keine RHEL-Subskription hat: Die Red Hat Developer Subscription ist für Einzelpersonen kostenlos und umfasst bis zu 16 Systeme. Alternativ funktionieren AlmaLinux 9 oder Rocky Linux 9 in diesem Tutorial identisch — beide melden sich als os_family = RedHat.

2.2 Netzwerk, Hostnamen und /etc/hosts

Ohne funktionierende DNS-Auflösung trägt man die Namen auf dem Control Node in /etc/hosts ein:

sudo tee -a /etc/hosts <<'EOF'
192.168.56.11   rhel9
192.168.56.12   ubuntu24
EOF

Erreichbarkeit prüfen:

ping -c1 rhel9
ping -c1 ubuntu24

2.3 Benutzer und sudo-Rechte auf den Zielsystemen

Ansible sollte sich nicht als root anmelden. Stattdessen legt man auf jedem Managed Node einen Automatisierungsbenutzer an, der per sudo ohne Passwortabfrage eskalieren darf.

Auf RHEL 9:

sudo useradd -m -s /bin/bash ansible
sudo passwd ansible
echo 'ansible ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/ansible
sudo chmod 0440 /etc/sudoers.d/ansible

Auf Ubuntu 24.04 identisch — Ubuntu verwendet ebenfalls /etc/sudoers.d/.

Achtung: NOPASSWD:ALL ist für eine Lernumgebung vertretbar. In produktiven Umgebungen schränkt man die erlaubten Kommandos ein oder hinterlegt das sudo-Passwort per –ask-become-pass bzw. über Ansible Vault.

2.4 SSH-Schlüssel erzeugen und verteilen

Auf dem Control Node als der Benutzer, der später Ansible ausführt:

ssh-keygen -t ed25519 -C "ansible control node"
ssh-copy-id ansible@rhel9
ssh-copy-id ansible@ubuntu24

Test — die Anmeldung muss ohne Passwortabfrage gelingen:

ssh ansible@rhel9 'hostname; id'
ssh ansible@ubuntu24 'hostname; id'

Warum das wichtig ist: Ansible startet je Task eine SSH-Verbindung. Eine interaktive Passwortabfrage würde jeden Playbook-Lauf blockieren. Der Schlüsselaustausch ist die eigentliche Grundvoraussetzung — funktioniert er nicht, funktioniert nichts danach.

Übung 2.1: Melde dich einmal manuell per SSH an jedem Managed Node an und führe sudo whoami aus. Die Ausgabe muss root lauten, ohne dass ein Passwort abgefragt wird.


3. Installation von Ansible

Ansible wird ausschließlich auf dem Control Node installiert.

3.1 Installation auf RHEL

Seit RHEL 8 liegt ansible-core direkt im AppStream-Repository:

sudo dnf install -y ansible-core

Damit erhält man die Ansible-Engine plus die Collection ansible.builtin. Weitere Collections installiert man bei Bedarf über Galaxy (siehe Kapitel 12.5) oder über Pakete wie rhel-system-roles.

Wer stattdessen das große Community-Paket möchte, aktiviert EPEL:

sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
sudo dnf install -y ansible

3.2 Installation auf Ubuntu

Aus den Ubuntu-Repositories:

sudo apt update
sudo apt install -y ansible

Das Paket ansible entspricht hier dem Community-Bundle inklusive vieler Collections. Wer nur die Engine will, installiert ansible-core.

Ubuntu-Pakete hinken der aktuellen Version oft hinterher. Für eine neuere Version bietet das Ansible-Projekt eine PPA an:

sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansible

3.3 Installation via pip in einer venv

Die flexibelste Variante, besonders wenn mehrere Ansible-Versionen parallel benötigt werden:

python3 -m venv ~/ansible-venv
source ~/ansible-venv/bin/activate
pip install --upgrade pip
pip install ansible ansible-lint

Deaktivieren mit deactivate. Die venv muss vor jeder Nutzung wieder aktiviert werden.

Methode Vorteil Nachteil
Distributionspaket integriert, gepatcht oft ältere Version
PPA / EPEL aktueller zusätzliche Fremdquelle
pip in venv volle Versionskontrolle Pflege liegt beim Benutzer

3.4 Versionen prüfen und verstehen

ansible --version

Beispielausgabe:

ansible [core 2.16.3]
  config file = /home/andre/ansible/ansible.cfg
  configured module search path = ['/home/andre/.ansible/plugins/modules', ...]
  ansible python module location = /usr/lib/python3/dist-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.12.3
  jinja version = 3.1.2

Drei Zeilen sind für Einsteiger wichtig:

Begriffsklärung: ansible-core ist die Engine. ansible ist ein Bundle aus der Engine plus rund 85 Collections. Beide melden dieselbe core-Version. Prüfen lässt sich das Bundle mit ansible-community –version.


4. Grundkonfiguration

4.1 Die Datei ansible.cfg und ihre Suchreihenfolge

Ansible sucht seine Konfiguration in dieser Reihenfolge und nimmt die erste gefundene Datei — es wird nicht zusammengeführt:

  1. Umgebungsvariable ANSIBLE_CONFIG
  2. ./ansible.cfg im aktuellen Arbeitsverzeichnis
  3. ~/.ansible.cfg
  4. /etc/ansible/ansible.cfg

Für dieses Tutorial legen wir ein Projektverzeichnis an:

mkdir -p ~/ansible-tutorial
cd ~/ansible-tutorial
ansible.cfg
[defaults]
inventory        = ./inventory.yml
remote_user      = ansible
host_key_checking = False
interpreter_python = auto_silent
stdout_callback  = yaml
retry_files_enabled = False
 
[privilege_escalation]
become        = False
become_method = sudo
become_user   = root

Bedeutung der Einträge:

Option Wirkung
inventory Standard-Inventory, spart -i bei jedem Aufruf
remote_user Benutzer für die SSH-Anmeldung
host_key_checking False vermeidet Rückfragen bei neuen Hosts — nur in Testumgebungen!
interpreter_python unterdrückt Warnungen zur Interpreter-Erkennung
stdout_callback = yaml macht die Ausgabe deutlich besser lesbar

Achtung: Ansible ignoriert eine ansible.cfg im aktuellen Verzeichnis, wenn dieses für alle schreibbar ist (chmod 777). Das ist eine Sicherheitsmaßnahme und eine häufige Ursache für „meine Konfiguration wird nicht gelesen“.

4.2 Das Inventory: INI- und YAML-Format

Das Inventory beantwortet die Frage: Welche Systeme gibt es und wie sind sie gruppiert?

INI-Format (kompakt, gut für kleine Umgebungen):

inventory.ini
[rhel]
rhel9 ansible_host=192.168.56.11
 
[ubuntu]
ubuntu24 ansible_host=192.168.56.12
 
[webserver:children]
rhel
ubuntu

YAML-Format (strukturierter, für alles ab mittlerer Größe empfohlen):

inventory.yml
---
all:
  children:
    webserver:
      children:
        rhel:
          hosts:
            rhel9:
              ansible_host: 192.168.56.11
        ubuntu:
          hosts:
            ubuntu24:
              ansible_host: 192.168.56.12

Das Ergebnis prüfen — dieser Befehl ist bei Inventory-Problemen das wichtigste Diagnosewerkzeug:

ansible-inventory --list -y
ansible-inventory --graph

4.3 Gruppen, Kindgruppen und die Gruppe all

Eine bewährte Konvention: Gruppen nach Funktion (webserver, datenbank) und zusätzlich nach Umgebung (prod, test) bilden. Ein Host ist dann in beiden.

4.4 Erste Verbindung testen

ansible all -m ansible.builtin.ping

Erwartete Ausgabe:

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

Hinweis: ping hat nichts mit ICMP zu tun. Das Modul meldet sich per SSH an, startet Python auf dem Ziel und lässt sich pong zurückgeben. Es testet also die komplette Kette: SSH, Authentifizierung und Python — genau das, was Ansible braucht.

Typische Fehler an dieser Stelle:

Meldung Ursache
Permission denied (publickey,password) SSH-Schlüssel nicht verteilt oder falscher remote_user
Failed to connect … No route to host Netzwerk/Firewall
/usr/bin/python3: not found kein Python auf dem Ziel
Missing sudo password become ohne NOPASSWD-Regel

4.5 Privilege Escalation mit become

Ohne Eskalation:

ansible all -m ansible.builtin.command -a "id -un"
# -> ansible

Mit Eskalation:

ansible all -m ansible.builtin.command -a "id -un" --become
# -> root

become lässt sich global in ansible.cfg, pro Play oder pro einzelnem Task setzen. Empfehlung für Einsteiger: global auf False lassen und dort aktivieren, wo es gebraucht wird. Das macht sichtbar, welche Tasks tatsächlich Root-Rechte benötigen.

Übung 4.1: Erweitere das Inventory um eine Gruppe test, die beide Hosts enthält, und prüfe das Ergebnis mit ansible-inventory –graph.


5. Ad-hoc-Befehle

5.1 Aufbau eines Ad-hoc-Kommandos

ansible <muster> -m <modul> -a "<argumente>" [optionen]
Bestandteil Bedeutung
<muster> Host, Gruppe oder Muster (all, rhel, web*, rhel:!rhel9)
-m Modul (Standard ist command)
-a Argumente für das Modul
-b Kurzform für –become

5.2 Nützliche Module für den Einstieg

# Uptime aller Hosts
ansible all -m ansible.builtin.command -a "uptime"
 
# Freier Speicherplatz
ansible all -m ansible.builtin.shell -a "df -h / | tail -1"
 
# Datei verteilen
ansible all -m ansible.builtin.copy \
  -a "src=/etc/motd dest=/tmp/motd.kopie mode=0644"
 
# Dienststatus abfragen
ansible all -m ansible.builtin.service_facts
 
# Alle Fakten eines Hosts anzeigen
ansible rhel9 -m ansible.builtin.setup

command oder shell? command führt das Programm direkt aus — ohne Shell, also ohne Pipes, Umleitungen und Variablenexpansion. shell startet eine Shell und kann all das. Faustregel: immer command, außer man braucht wirklich Shell-Funktionen. command ist sicherer, weil keine Sonderzeichen interpretiert werden.

5.3 Paketverwaltung: dnf, apt und package

# Nur RHEL
ansible rhel -m ansible.builtin.dnf -a "name=tree state=present" -b
 
# Nur Ubuntu
ansible ubuntu -m ansible.builtin.apt -a "name=tree state=present update_cache=yes" -b
 
# Beide gleichzeitig - das generische Modul wählt selbst
ansible all -m ansible.builtin.package -a "name=tree state=present" -b

Das package-Modul ist bequem, hat aber eine Grenze: Es abstrahiert nur den Paketmanager, nicht die Paketnamen. httpd heißt unter Ubuntu apache2, und daran ändert package nichts. Wie man das löst, zeigt Kapitel 11.

5.4 Grenzen von Ad-hoc-Befehlen

Ad-hoc-Befehle sind ideal für einmalige Abfragen und schnelle Eingriffe. Sie stoßen dort an ihre Grenzen, wo:

Für all das gibt es Playbooks.

Übung 5.1: Ermittle mit einem einzigen Ad-hoc-Befehl die Kernel-Version aller Hosts. Übung 5.2: Installiere das Paket htop auf beiden Systemen und deinstalliere es anschließend wieder (state=absent).


6. YAML-Grundlagen

6.1 Einrückung, Listen, Dictionaries

YAML kennt drei Bausteine: Skalare (einzelne Werte), Listen und Dictionaries (Schlüssel-Wert-Paare).

beispiel.yml
---
# Skalar
name: webserver
 
# Liste
pakete:
  - httpd
  - mod_ssl
  - php
 
# Dictionary
benutzer:
  name: andre
  shell: /bin/bash
  uid: 1001
 
# Liste von Dictionaries - die häufigste Struktur in Ansible
benutzerliste:
  - name: andre
    uid: 1001
  - name: martina
    uid: 1002

Die drei eisernen Regeln:

  1. Eingerückt wird ausschließlich mit Leerzeichen, niemals mit Tabulatoren.
  2. Die Einrückungstiefe muss innerhalb einer Ebene konsistent sein (üblich: 2 Leerzeichen).
  3. Nach einem Doppelpunkt folgt ein Leerzeichen: name: wert, nicht name:wert.

6.2 Strings, Booleans und typische Stolperfallen

# Booleans - YAML kennt viele Schreibweisen
aktiv: true          # empfohlen
aktiv: yes           # gleichbedeutend, aber uneinheitlich
 
# Norwegen-Problem: NO wird als Boolean false gelesen!
land: NO             # -> false
land: "NO"           # -> Zeichenkette "NO"
 
# Doppelpunkt im Wert erzwingt Anführungszeichen
meldung: "Fehler: Datei nicht gefunden"
 
# Führende Null bleibt nur als String erhalten
modus: 0644          # kann als Zahl interpretiert werden
modus: "0644"        # sicher
 
# Mehrzeilig
skript: |            # Zeilenumbrüche bleiben erhalten
  #!/bin/bash
  echo "Hallo"

beschreibung: >      # Zeilenumbrüche werden zu Leerzeichen
  Dieser Text wird
  zu einer Zeile.

Achtung: Der Dateimodus in Ansible sollte immer in Anführungszeichen stehen: mode: „0644“. Ohne Anführungszeichen kann YAML den Wert als Dezimalzahl lesen, was zu falschen Berechtigungen führt.

6.3 Editor-Unterstützung und Linting

pip install ansible-lint yamllint
 
yamllint playbook.yml
ansible-lint playbook.yml

Zusätzlich prüft Ansible selbst die Syntax, ohne etwas auszuführen:

ansible-playbook playbook.yml --syntax-check

Für vim empfiehlt sich set expandtab shiftwidth=2 softtabstop=2 in der ~/.vimrc, für VS Code die Erweiterung Ansible von Red Hat.


7. Das erste Playbook

7.1 Aufbau: Plays, Tasks, Module

Ein Playbook ist eine Liste von Plays. Jedes Play ordnet einer Hostgruppe eine Liste von Tasks zu. Jeder Task ruft genau ein Modul auf.

Playbook
 └─ Play        (welche Hosts?)
     └─ Task    (was soll passieren?)
         └─ Modul  (womit?)

7.2 Ein Paket installieren und einen Dienst starten

01-erstes-playbook.yml
---
- name: Grundkonfiguration der Testsysteme
  hosts: all
  become: true

  tasks:
    - name: Paket chrony installieren
      ansible.builtin.package:
        name: chrony
        state: present

    - name: Dienst chronyd starten und aktivieren
      ansible.builtin.service:
        name: "{{ 'chronyd' if ansible_facts['os_family'] == 'RedHat' else 'chrony' }}"
        state: started
        enabled: true

Hinweis: Schon im ersten Playbook zeigt sich der Distributionsunterschied: Der Dienst heißt unter RHEL chronyd, unter Ubuntu schlicht chrony. Kapitel 8 zeigt, wie man das sauberer löst als mit einem Inline-Ausdruck.

7.3 Playbook ausführen, --check und --diff

# Syntax prüfen
ansible-playbook 01-erstes-playbook.yml --syntax-check
 
# Probelauf ohne Änderungen (Dry Run)
ansible-playbook 01-erstes-playbook.yml --check --diff
 
# Tatsächlich ausführen
ansible-playbook 01-erstes-playbook.yml

Tipp: –check ist die wichtigste Sicherheitsleine für Einsteiger. Ansible meldet, was es ändern würde, ohne es zu tun. Einschränkung: Die Module command und shell werden im Check-Modus standardmäßig übersprungen, und Tasks, die auf dem Ergebnis eines übersprungenen Tasks aufbauen, können dann fehlschlagen. Ein sauberer –check-Lauf ist also ein gutes Zeichen, aber keine Garantie.

7.4 Ausgabe lesen

PLAY [Grundkonfiguration der Testsysteme] **************************

TASK [Gathering Facts] ********************************************
ok: [rhel9]
ok: [ubuntu24]

TASK [Paket chrony installieren] **********************************
changed: [ubuntu24]
ok: [rhel9]

PLAY RECAP ********************************************************
rhel9      : ok=3  changed=0  unreachable=0  failed=0  skipped=0
ubuntu24   : ok=3  changed=1  unreachable=0  failed=0  skipped=0
Status Bedeutung Farbe
ok Zustand war bereits korrekt grün
changed Ansible hat etwas verändert gelb
failed Task fehlgeschlagen, Host scheidet aus rot
skipped Bedingung nicht erfüllt blau
unreachable Host nicht erreichbar rot

7.5 Idempotenz — das zentrale Konzept

Führe das Playbook ein zweites Mal aus. Das Ergebnis muss lauten:

rhel9      : ok=3  changed=0
ubuntu24   : ok=3  changed=0

Genau das ist Idempotenz: Ein Playbook beschreibt einen Zielzustand. Ist dieser erreicht, ändert ein erneuter Lauf nichts.

Merksatz: Ein Playbook, das bei jedem Lauf changed meldet, ist mit hoher Wahrscheinlichkeit fehlerhaft. Häufigste Ursache sind command- oder shell-Tasks, die keine Zustandsprüfung durchführen. Wie man das repariert, zeigt Kapitel 9.4 mit changed_when und creates.

Übung 7.1: Schreibe ein Playbook, das die Pakete vim, curl und git installiert. Führe es zweimal aus und prüfe, dass der zweite Lauf changed=0 meldet.


8. Variablen und Fakten

8.1 Variablen definieren

---
- name: Variablen-Demonstration
  hosts: all
  vars:
    paketname: tree
    zielverzeichnis: /opt/daten

  vars_files:
    - vars/allgemein.yml

  tasks:
    - name: Variable ausgeben
      ansible.builtin.debug:
        msg: "Paket {{ paketname }} nach {{ zielverzeichnis }}"

Von der Kommandozeile:

ansible-playbook playbook.yml -e "paketname=htop"
ansible-playbook playbook.yml -e "@vars/extra.yml"

Hinweis: Variablen werden in doppelten geschweiften Klammern referenziert. Steht eine Variable am Anfang eines Wertes, sind Anführungszeichen zwingend, sonst hält YAML die Klammer für den Beginn eines Dictionaries:

name: „paketname — richtig name: paketname — Syntaxfehler

8.2 group_vars und host_vars

Die sauberste Methode: Variablen werden nach Gruppe bzw. Host in eigene Dateien ausgelagert. Ansible lädt diese Verzeichnisse automatisch, ohne dass man sie einbinden muss.

~/ansible-tutorial/
├── ansible.cfg
├── inventory.yml
├── group_vars/
│   ├── all.yml
│   ├── rhel.yml
│   └── ubuntu.yml
└── host_vars/
    └── rhel9.yml
group_vars/rhel.yml
---
webserver_paket:  httpd
webserver_dienst: httpd
webserver_konfig: /etc/httpd/conf.d/tutorial.conf
firewall_werkzeug: firewalld
group_vars/ubuntu.yml
---
webserver_paket:  apache2
webserver_dienst: apache2
webserver_konfig: /etc/apache2/conf-available/tutorial.conf
firewall_werkzeug: ufw

Damit wird aus dem unübersichtlichen Inline-Ausdruck aus Kapitel 7.2 ein lesbarer Task:

- name: Webserver installieren
  ansible.builtin.package:
    name: "{{ webserver_paket }}"
    state: present

Das ist das didaktische Herzstück dieses Tutorials. Nicht der Task kennt den Unterschied zwischen RHEL und Ubuntu, sondern die Variablendatei. Der Task selbst bleibt distributionsneutral und damit lesbar. Diese Trennung von Logik und Daten ist der wichtigste Qualitätssprung beim Übergang vom Skript zur Automatisierung.

8.3 Fakten sammeln und auswerten

Zu Beginn jedes Plays sammelt Ansible automatisch Informationen über die Zielsysteme — die Fakten.

ansible rhel9 -m ansible.builtin.setup
ansible rhel9 -m ansible.builtin.setup -a "filter=ansible_distribution*"

Wichtige Fakten für den Einstieg:

Faktum RHEL 9 Ubuntu 24.04
ansible_facts['os_family'] RedHat Debian
ansible_facts['distribution'] RedHat Ubuntu
ansible_facts['distribution_major_version'] 9 24
ansible_facts['pkg_mgr'] dnf apt
ansible_facts['service_mgr'] systemd systemd
ansible_facts['hostname]'' rhel9 ubuntu24
ansible_facts['default_ipv4']['address'] 192.168.56.11 192.168.56.12

Das Sammeln kostet Zeit. Bei Plays, die keine Fakten brauchen, lässt es sich abschalten:

- name: Schnelles Play ohne Fakten
  hosts: all
  gather_facts: false

8.4 Distributionsunterschiede über Fakten abfangen

- name: Nur auf RHEL-Systemen
  ansible.builtin.dnf:
    name: policycoreutils-python-utils
    state: present
  when: ansible_facts['os_family'] == 'RedHat'

- name: Nur auf Debian-Systemen
  ansible.builtin.apt:
    name: ufw
    state: present
    update_cache: true
  when: ansible_facts['os_family'] == 'Debian'

os_family statt distribution verwenden. os_family liefert RedHat für RHEL, CentOS Stream, Rocky, AlmaLinux und Fedora — das Playbook funktioniert damit auf allen. distribution unterscheidet diese Systeme und macht die Bedingung unnötig eng.

8.5 Vorrangregeln der Variablen

Ist dieselbe Variable an mehreren Stellen definiert, gewinnt die speziellere. Ansible kennt 22 Ebenen; für den Einstieg genügt diese Reihenfolge von schwach nach stark:

  1. Rollen-Defaults (roles/x/defaults/main.yml)
  2. Inventory-Variablen
  3. group_vars/all
  4. group_vars/<gruppe>
  5. host_vars/<host>
  6. Play-Variablen (vars:)
  7. Task-Variablen
  8. –extra-vars auf der Kommandozeile — gewinnt immer

Praxisregel: Werte, die überschreibbar sein sollen, gehören in defaults. Werte, die nicht überschrieben werden dürfen, gehören in vars. Und wenn eine Variable sich unerklärlich verhält, hilft ansible-playbook playbook.yml -e „…“ -vvv oder ein debug-Task an der richtigen Stelle.

Übung 8.1: Lege group_vars/all.yml mit einer Variablen projekt_name an und gib sie in einem Playbook per debug aus. Überschreibe sie anschließend per -e und beobachte das Ergebnis.


9. Bedingungen, Schleifen und Handler

9.1 when — Tasks bedingt ausführen

# Einfache Bedingung
- name: EPEL nur auf RHEL
  ansible.builtin.dnf:
    name: epel-release
    state: present
  when: ansible_facts['os_family'] == 'RedHat'
 
# UND-Verknüpfung: Liste bedeutet AND
- name: Nur auf RHEL 9
  ansible.builtin.debug:
    msg: "RHEL 9 erkannt"
  when:
    - ansible_facts['os_family'] == 'RedHat'
    - ansible_facts['distribution_major_version'] == '9'
 
# ODER-Verknüpfung
  when: ansible_facts['os_family'] == 'RedHat' or ansible_facts['os_family'] == 'Debian'
 
# Variable prüfen
  when: webserver_paket is defined
  when: meine_liste | length > 0

In when stehen Variablen ohne geschweifte Klammern: when: os_family == „RedHat“, nicht when: „os_family == …“. Der Ausdruck wird bereits als Jinja2-Bedingung ausgewertet.

9.2 loop — Wiederholungen

# Einfache Liste
- name: Mehrere Pakete installieren
  ansible.builtin.package:
    name: "{{ item }}"
    state: present
  loop:
    - vim
    - curl
    - git
 
# Besser: das Modul kann die Liste selbst verarbeiten
- name: Mehrere Pakete in einer Transaktion
  ansible.builtin.package:
    name:
      - vim
      - curl
      - git
    state: present
 
# Liste von Dictionaries
- name: Benutzer anlegen
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ item.gruppe }}"
    state: present
  loop:
    - { name: andre,   gruppe: wheel }
    - { name: martina, gruppe: users }
  loop_control:
    label: "{{ item.name }}"

Tipp: Bei Paketmodulen ist die zweite Variante deutlich schneller: Statt zehn einzelner dnf-Aufrufe gibt es eine einzige Transaktion. loop_control.label sorgt dafür, dass die Ausgabe nicht das ganze Dictionary zeigt, sondern nur den Namen.

9.3 Handler und notify

Ein Handler ist ein Task, der nur ausgeführt wird, wenn ein anderer Task etwas geändert hat — und zwar erst am Ende des Plays, und selbst dann nur einmal.

---
- name: Webserver konfigurieren
  hosts: webserver
  become: true

  tasks:
    - name: Konfigurationsdatei ausbringen
      ansible.builtin.template:
        src: tutorial.conf.j2
        dest: "{{ webserver_konfig }}"
        owner: root
        group: root
        mode: "0644"
      notify: Webserver neu laden

  handlers:
    - name: Webserver neu laden
      ansible.builtin.service:
        name: "{{ webserver_dienst }}"
        state: reloaded

9.4 Fehlerbehandlung

# Fehler ignorieren
- name: Optionaler Dienst
  ansible.builtin.service:
    name: irgendwas
    state: started
  ignore_errors: true
 
# Eigene Fehlerdefinition
- name: Prüfung ausführen
  ansible.builtin.command: /usr/local/bin/pruefung.sh
  register: ergebnis
  failed_when: "'FEHLER' in ergebnis.stdout"
 
# Idempotenz für command herstellen
- name: Reine Abfrage - meldet nie "changed"
  ansible.builtin.command: systemctl is-enabled sshd
  register: ssh_status
  changed_when: false
  failed_when: false
 
# Nur ausführen, wenn die Zieldatei fehlt
- name: Archiv einmalig entpacken
  ansible.builtin.command: tar xzf /tmp/app.tar.gz -C /opt
  args:
    creates: /opt/app/bin/start.sh
 
# Block mit Fehlerbehandlung
- name: Installation mit Aufräumen
  block:
    - name: Anwendung installieren
      ansible.builtin.package:
        name: meine-app
        state: present
  rescue:
    - name: Fehlerbehandlung
      ansible.builtin.debug:
        msg: "Installation fehlgeschlagen"
  always:
    - name: Temporäre Dateien entfernen
      ansible.builtin.file:
        path: /tmp/installation
        state: absent

changed_when: false bei reinen Abfragen ist die häufigste Korrektur, die Einsteiger-Playbooks brauchen. Ohne sie meldet jeder command-Task bei jedem Lauf changed — und die Idempotenz-Prüfung aus Kapitel 7.5 wird wertlos.

9.5 Tags zur selektiven Ausführung

tasks:
  - name: Pakete installieren
    ansible.builtin.package:
      name: "{{ webserver_paket }}"
      state: present
    tags:
      - installation
      - pakete

  - name: Konfiguration ausbringen
    ansible.builtin.template:
      src: tutorial.conf.j2
      dest: "{{ webserver_konfig }}"
      mode: "0644"
    tags:
      - konfiguration
ansible-playbook site.yml --tags konfiguration
ansible-playbook site.yml --skip-tags installation
ansible-playbook site.yml --list-tags

Übung 9.1: Erweitere das Playbook aus Übung 7.1 so, dass vim nur auf RHEL und neovim nur auf Ubuntu installiert wird.


10. Templates mit Jinja2

10.1 Das template-Modul im Vergleich zu copy

Modul Einsatz
copy Datei wird unverändert übertragen
template Datei wird vor der Übertragung durch Jinja2 verarbeitet

Templates liegen konventionell im Verzeichnis templates/ und tragen die Endung .j2.

10.2 Variablen, Bedingungen und Schleifen im Template

templates/tutorial.conf.j2
# {{ ansible_managed }}
# Erzeugt fuer {{ inventory_hostname }} am {{ ansible_date_time.date }}

ServerName {{ ansible_facts['fqdn'] }}
DocumentRoot {{ webserver_docroot | default('/var/www/html') }}

{% if ansible_facts['os_family'] == 'RedHat' %}
# RHEL: Module werden ueber conf.modules.d geladen
ErrorLog logs/tutorial_error.log
{% else %}
# Debian/Ubuntu: absolute Pfade
ErrorLog ${APACHE_LOG_DIR}/tutorial_error.log
{% endif %}

{% for netz in erlaubte_netze %}
# Freigabe fuer {{ netz }}
Require ip {{ netz }}
{% endfor %}

Zugehöriger Task:

- name: Konfiguration aus Template erzeugen
  ansible.builtin.template:
    src: tutorial.conf.j2
    dest: "{{ webserver_konfig }}"
    owner: root
    group: root
    mode: "0644"
    validate: "apachectl -t -f %s"
  notify: Webserver neu laden

Tipp: ansible_managed erzeugt einen Kommentarhinweis, dass die Datei von Ansible verwaltet wird. Das erspart Kollegen die Frage, warum ihre manuelle Änderung beim nächsten Lauf verschwunden ist. validate prüft die erzeugte Datei, bevor sie an ihren Platz kopiert wird — eine fehlerhafte Konfiguration erreicht den Server damit gar nicht erst.

10.3 Kontrollstrukturen im Überblick

Syntax Bedeutung
ansible2 Ausdruck wird ausgegeben
{% … %} Anweisung (if, for, set) — keine Ausgabe
{# … #} Kommentar, erscheint nicht in der Ausgabe
{%- … -%} Anweisung, entfernt umgebende Leerzeilen

10.4 Filter und Defaults

{{ name | upper }}                      # Großbuchstaben
{{ pfad | basename }}                   # Dateiname aus Pfad
{{ wert | default('Standardwert') }}    # Ersatzwert
{{ wert | default('x', true) }}         # auch bei leerem String
{{ liste | join(', ') }}                # Liste zu Zeichenkette
{{ liste | length }}                    # Anzahl Elemente
{{ zahl | int }}                        # Typumwandlung
{{ daten | to_nice_json }}              # formatiertes JSON
{{ text | regex_replace('^a', 'b') }}   # Ersetzung
{{ pfad | dirname }}                    # Verzeichnisanteil

Hinweis: Der Filter default() ist das wichtigste Werkzeug für robuste Templates. Er verhindert, dass ein Playbook mit undefined variable abbricht, nur weil eine optionale Variable nicht gesetzt wurde.

Übung 10.1: Erstelle ein Template für /etc/motd, das Hostname, IP-Adresse, Distribution und Anzahl der CPU-Kerne ausgibt, und bringe es auf beiden Systemen aus.


11. Praxisbeispiel: Webserver auf RHEL und Ubuntu

Dieses Kapitel führt alles Bisherige zusammen. Ziel ist ein einziges Playbook, das auf beiden Distributionen einen funktionierenden Apache-Webserver mit geöffneter Firewall hinterlässt.

11.1 Die Unterschiede im Überblick

Aspekt RHEL 9 Ubuntu 24.04
Paket httpd apache2
Dienst httpd apache2
Konfigurationsverzeichnis /etc/httpd/conf.d/ /etc/apache2/conf-available/
Hauptkonfiguration /etc/httpd/conf/httpd.conf /etc/apache2/apache2.conf
DocumentRoot /var/www/html /var/www/html
Benutzer des Dienstes apache www-data
Firewall firewalld ufw
Aktivierung von Konfigurationen automatisch per conf.d a2enconf nötig
SELinux aktiv (enforcing) AppArmor, für Apache unkritisch

11.2 Variablendateien

group_vars/all.yml
---
webserver_docroot: /var/www/html
webserver_port: 80
erlaubte_netze:
  - 192.168.56.0/24
group_vars/rhel.yml
---
webserver_paket:   httpd
webserver_dienst:  httpd
webserver_konfig:  /etc/httpd/conf.d/tutorial.conf
webserver_benutzer: apache
group_vars/ubuntu.yml
---
webserver_paket:   apache2
webserver_dienst:  apache2
webserver_konfig:  /etc/apache2/conf-available/tutorial.conf
webserver_benutzer: www-data

11.3 Das vollständige Playbook

11-webserver.yml
---
- name: Apache-Webserver auf RHEL und Ubuntu bereitstellen
  hosts: webserver
  become: true

  tasks:

    # ---------- Installation ----------

    - name: Paketcache unter Debian aktualisieren
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600
      when: ansible_facts['os_family'] == 'Debian'

    - name: Webserver-Paket installieren
      ansible.builtin.package:
        name: "{{ webserver_paket }}"
        state: present
 
    # ---------- Inhalt und Konfiguration ----------

    - name: Startseite erzeugen
      ansible.builtin.template:
        src: index.html.j2
        dest: "{{ webserver_docroot }}/index.html"
        owner: root
        group: root
        mode: "0644"

    - name: Konfigurationsdatei ausbringen
      ansible.builtin.template:
        src: tutorial.conf.j2
        dest: "{{ webserver_konfig }}"
        owner: root
        group: root
        mode: "0644"
      notify: Webserver neu laden

    - name: Konfiguration unter Ubuntu aktivieren
      ansible.builtin.command: a2enconf tutorial
      args:
        creates: /etc/apache2/conf-enabled/tutorial.conf
      when: ansible_facts['os_family'] == 'Debian'
      notify: Webserver neu laden
 
    # ---------- SELinux (nur RHEL) ----------

    - name: SELinux-Werkzeuge installieren
      ansible.builtin.dnf:
        name: policycoreutils-python-utils
        state: present
      when: ansible_facts['os_family'] == 'RedHat'

    - name: SELinux-Kontext des DocumentRoot sicherstellen
      community.general.sefcontext:
        target: "{{ webserver_docroot }}(/.*)?"
        setype: httpd_sys_content_t
        state: present
      when:
        - ansible_facts['os_family'] == 'RedHat'
        - ansible_facts['selinux']['status'] == 'enabled'
      notify: SELinux-Kontext anwenden
 
    # ---------- Firewall ----------

    - name: Firewalld installieren und starten
      when: ansible_facts['os_family'] == 'RedHat'
      block:
        - name: firewalld sicherstellen
          ansible.builtin.dnf:
            name: firewalld
            state: present

        - name: firewalld starten
          ansible.builtin.service:
            name: firewalld
            state: started
            enabled: true

        - name: HTTP in firewalld freigeben
          ansible.posix.firewalld:
            service: http
            permanent: true
            immediate: true
            state: enabled

    - name: HTTP in ufw freigeben
      community.general.ufw:
        rule: allow
        port: "{{ webserver_port }}"
        proto: tcp
      when: ansible_facts['os_family'] == 'Debian'
 
    # ---------- Dienst ----------

    - name: Webserver starten und aktivieren
      ansible.builtin.service:
        name: "{{ webserver_dienst }}"
        state: started
        enabled: true
 
    # ---------- Verifikation ----------

    - name: Erreichbarkeit pruefen
      ansible.builtin.uri:
        url: "http://{{ ansible_facts['default_ipv4']['address'] }}/"
        status_code: 200
        return_content: true
      register: pruefung
      delegate_to: localhost
      become: false

    - name: Ergebnis anzeigen
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} antwortet mit Status {{ pruefung.status }}"

  handlers:

    - name: Webserver neu laden
      ansible.builtin.service:
        name: "{{ webserver_dienst }}"
        state: reloaded

    - name: SELinux-Kontext anwenden
      ansible.builtin.command: "restorecon -Rv {{ webserver_docroot }}"
templates/index.html.j2
<!DOCTYPE html>
<html lang="de">
<head><meta charset="utf-8"><title>{{ inventory_hostname }}</title></head>
<body>
  <h1>Willkommen auf {{ inventory_hostname }}</h1>
  <ul>
    <li>Distribution: {{ ansible_facts['distribution'] }}
        {{ ansible_facts['distribution_version'] }}</li>
    <li>Kernel: {{ ansible_facts['kernel'] }}</li>
    <li>CPU-Kerne: {{ ansible_facts['processor_vcpus'] }}</li>
    <li>Bereitgestellt am: {{ ansible_date_time.iso8601 }}</li>
  </ul>
</body>
</html>

11.4 Benötigte Collections

Die Module ansible.posix.firewalld, community.general.ufw und community.general.sefcontext sind nicht in ansible-core enthalten:

ansible-galaxy collection install ansible.posix community.general

11.5 Ausführen und prüfen

ansible-playbook 11-webserver.yml --check --diff
ansible-playbook 11-webserver.yml
ansible-playbook 11-webserver.yml      # zweiter Lauf: changed=0

Anschließend im Browser http://192.168.56.11/ und http://192.168.56.12/ aufrufen.

Wenn die Seite unter RHEL nicht erscheint, ist fast immer eine von drei Ursachen schuld: firewalld blockiert Port 80, SELinux verweigert den Zugriff auf den DocumentRoot, oder der Dienst läuft nicht. Diagnose auf dem Zielsystem:

sudo systemctl status httpd
sudo firewall-cmd --list-services
sudo ausearch -m AVC -ts recent

12. Rollen — Playbooks strukturieren

12.1 Wozu Rollen

Das Playbook aus Kapitel 11 ist bereits an der Grenze der Übersichtlichkeit. Rollen lösen drei Probleme: Wiederverwendbarkeit, Lesbarkeit und Teilbarkeit im Team.

12.2 Verzeichnisstruktur einer Rolle

ansible-galaxy init roles/webserver
roles/webserver/
├── defaults/main.yml    # Standardwerte, niedrigste Priorität
├── vars/main.yml        # feste Werte, hohe Priorität
├── tasks/main.yml       # Einstiegspunkt der Rolle
├── handlers/main.yml    # Handler
├── templates/           # Jinja2-Vorlagen
├── files/               # statische Dateien für copy
├── meta/main.yml        # Metadaten und Abhängigkeiten
└── README.md
Verzeichnis Wird geladen Priorität
defaults/ automatisch niedrig — zum Überschreiben gedacht
vars/ automatisch hoch — nicht zum Überschreiben gedacht
tasks/ automatisch (main.yml)
templates/ über template-Modul, ohne Pfadangabe
files/ über copy-Modul, ohne Pfadangabe

12.3 Distributionsabhängige Variablen in Rollen

Der elegante Standardweg, um in einer Rolle zwischen RHEL und Ubuntu zu unterscheiden:

roles/webserver/vars/
├── RedHat.yml
└── Debian.yml
roles/webserver/tasks/main.yml
---
- name: Distributionsspezifische Variablen laden
  ansible.builtin.include_vars: "{{ ansible_facts['os_family'] }}.yml"

- name: Webserver-Paket installieren
  ansible.builtin.package:
    name: "{{ webserver_paket }}"
    state: present

- name: Konfiguration ausbringen
  ansible.builtin.template:
    src: tutorial.conf.j2
    dest: "{{ webserver_konfig }}"
    mode: "0644"
  notify: Webserver neu laden

- name: Firewall konfigurieren
  ansible.builtin.include_tasks: "firewall-{{ ansible_facts['os_family'] }}.yml"

- name: Dienst sicherstellen
  ansible.builtin.service:
    name: "{{ webserver_dienst }}"
    state: started
    enabled: true

Tipp: include_vars mit ansible_facts_os_family.yml ist ein Muster, das in praktisch jeder professionellen Rolle vorkommt. Neue Distributionen werden unterstützt, indem eine weitere Variablendatei hinzukommt — der Task-Code bleibt unverändert.

12.4 Rollen einbinden

site.yml
---
- name: Webserver bereitstellen
  hosts: webserver
  become: true
  roles:
    - role: basis
    - role: webserver
      vars:
        webserver_port: 8080

Alternativ dynamisch innerhalb der Task-Liste:

tasks:
  - name: Webserver-Rolle bedingt einbinden
    ansible.builtin.include_role:
      name: webserver
    when: inventory_hostname in groups['webserver']
Variante Zeitpunkt Besonderheit
roles: statisch, vor allen Tasks Tags vererben sich auf alle Tasks
import_role statisch, an der Stelle zur Übersetzungszeit aufgelöst
include_role dynamisch, zur Laufzeit when wirkt auf die ganze Rolle

12.5 Ansible Galaxy und Collections

# Suchen und installieren
ansible-galaxy collection install community.general
ansible-galaxy role install geerlingguy.apache
 
# Installiertes anzeigen
ansible-galaxy collection list
 
# Abhängigkeiten deklarieren
ansible-galaxy collection install -r requirements.yml
requirements.yml
---
collections:
  - name: ansible.posix
  - name: community.general

roles:
  - name: geerlingguy.apache
    version: "3.2.0"

Vollqualifizierte Modulnamen (FQCN) wie ansible.builtin.package statt nur package sind seit Ansible 2.10 die empfohlene Schreibweise. Sie machen eindeutig, aus welcher Collection ein Modul stammt — wichtig, seit mehrere Collections gleichnamige Module enthalten können.

Übung 12.1: Überführe das Playbook aus Kapitel 11 in eine Rolle webserver mit vars/RedHat.yml und vars/Debian.yml. Das aufrufende site.yml sollte danach nur noch wenige Zeilen umfassen.


13. Geheimnisse schützen mit Ansible Vault

13.1 Verschlüsselte Dateien und Variablen anlegen

# Neue verschlüsselte Datei anlegen
ansible-vault create group_vars/all/vault.yml
 
# Bestehende Datei verschlüsseln
ansible-vault encrypt secrets.yml
 
# Bearbeiten (entschlüsselt nur im Editor)
ansible-vault edit group_vars/all/vault.yml
 
# Anzeigen
ansible-vault view group_vars/all/vault.yml
 
# Passwort ändern
ansible-vault rekey group_vars/all/vault.yml

Einzelne Werte lassen sich auch inline verschlüsseln:

ansible-vault encrypt_string 'GeheimesPasswort' --name 'db_passwort'

Das Ergebnis wird direkt in eine normale YAML-Datei eingefügt:

---
db_benutzer: app
db_passwort: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          38396437653...

13.2 Passwortdateien und Vault-IDs

echo "MeinVaultPasswort" > ~/.vault_pass
chmod 600 ~/.vault_pass
ansible.cfg
[defaults]
vault_password_file = ~/.vault_pass

Mehrere Vaults für verschiedene Umgebungen:

ansible-vault encrypt --vault-id prod@~/.vault_prod secrets_prod.yml
ansible-playbook site.yml --vault-id prod@~/.vault_prod

13.3 Playbooks mit Vault ausführen

ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass

Die Vault-Passwortdatei gehört niemals ins Git-Repository.

Ein passender .gitignore-Eintrag:

.vault_pass
*.retry

Eine bewährte Konvention ist die Trennung in eine unverschlüsselte vars.yml mit sprechenden Variablennamen und eine verschlüsselte vault.yml, die die eigentlichen Werte unter dem Präfix vault_ enthält. So bleibt im Klartext sichtbar, welche Geheimnisse existieren, ohne ihren Inhalt preiszugeben.


14. Gute Praxis für den Einstieg

14.1 Verzeichnisstruktur eines Projekts

ansible-projekt/
├── ansible.cfg
├── requirements.yml
├── .gitignore
├── inventories/
│   ├── test/
│   │   ├── hosts.yml
│   │   ├── group_vars/
│   │   └── host_vars/
│   └── prod/
│       ├── hosts.yml
│       └── group_vars/
├── roles/
│   ├── basis/
│   └── webserver/
├── playbooks/
│   ├── site.yml
│   └── webserver.yml
└── templates/
ansible-playbook -i inventories/test playbooks/site.yml

14.2 Versionierung mit Git

Ansible-Code ist Code und gehört in ein Repository. Der praktische Nutzen:

14.3 Namenskonventionen und Lesbarkeit

14.4 Häufige Anfängerfehler

Fehler Symptom Lösung
Tabulatoren in YAML found character \t nur Leerzeichen verwenden
Fehlende Anführungszeichen bei am Zeilenanfang Syntaxfehler name: „var
in when Warnung, unerwartetes Verhalten Klammern weglassen
shell statt command unnötiges Sicherheitsrisiko command bevorzugen
command ohne changed_when immer changed changed_when: false
mode: 0644 ohne Anführungszeichen falsche Berechtigungen mode: „0644“
become vergessen Permission denied become: true im Play
Handler-Name stimmt nicht Handler läuft nie Namen exakt abgleichen
Produktivsystem ohne –check ungewollte Änderungen erst Dry Run
Passwörter im Klartext Sicherheitsvorfall Ansible Vault

14.5 Fehlersuche

# Ausgabestufen: mehr v = mehr Details
ansible-playbook site.yml -v      # Ergebnisse der Tasks
ansible-playbook site.yml -vvv    # inkl. SSH-Kommandos
ansible-playbook site.yml -vvvv   # inkl. Verbindungsdebug
 
# Nur bestimmte Hosts
ansible-playbook site.yml --limit rhel9
 
# Ab einem bestimmten Task starten
ansible-playbook site.yml --start-at-task "Konfiguration ausbringen"
 
# Schrittweise mit Bestätigung
ansible-playbook site.yml --step
 
# Was wird ausgeführt?
ansible-playbook site.yml --list-tasks
ansible-playbook site.yml --list-hosts

Das nützlichste Diagnosewerkzeug im Playbook selbst:

- name: Zwischenergebnis anzeigen
  ansible.builtin.debug:
    var: ergebnis

- name: Alle Fakten eines Hosts anzeigen
  ansible.builtin.debug:
    var: ansible_facts

15. Ausblick

15.1 Ansible Automation Platform / AWX

Sobald mehrere Personen automatisieren, reicht die Kommandozeile nicht mehr. AWX (Open Source) bzw. die Red Hat Ansible Automation Platform (unterstützt) ergänzen:

15.2 Dynamische Inventories

Statische Inventory-Dateien werden in Cloud-Umgebungen schnell unbrauchbar. Dynamische Inventory-Plugins fragen die Quelle direkt ab:

inventory_azure.yml
---
plugin: azure.azcollection.azure_rm
include_vm_resource_groups:
  - produktion
keyed_groups:
  - key: tags.rolle
    prefix: rolle

Verfügbar unter anderem für Azure, AWS EC2, VMware, OpenStack, Proxmox und Red Hat Satellite.

15.3 Weiterführende Themen


Anhang

A. Modul-Übersicht für den Einstieg

Modul Zweck
ansible.builtin.package Pakete distributionsneutral verwalten
ansible.builtin.dnf / .apt Pakete distributionsspezifisch verwalten
ansible.builtin.service / .systemd Dienste steuern
ansible.builtin.copy Datei unverändert übertragen
ansible.builtin.template Datei aus Jinja2-Vorlage erzeugen
ansible.builtin.file Dateien, Verzeichnisse, Links, Rechte
ansible.builtin.lineinfile einzelne Zeile in Datei sicherstellen
ansible.builtin.blockinfile Textblock in Datei verwalten
ansible.builtin.user / .group Benutzer und Gruppen
ansible.builtin.command / .shell Befehle ausführen
ansible.builtin.debug Werte ausgeben
ansible.builtin.setup Fakten sammeln
ansible.builtin.uri HTTP-Aufrufe und Prüfungen
ansible.builtin.git Repositories auschecken
ansible.builtin.cron Cronjobs verwalten
ansible.builtin.mount Dateisysteme einhängen
ansible.posix.firewalld firewalld (RHEL)
community.general.ufw ufw (Ubuntu)
ansible.posix.seboolean SELinux-Booleans

B. Befehls-Spickzettel

# --- Prüfen ---
ansible --version
ansible-inventory --graph
ansible all -m ansible.builtin.ping
ansible-playbook site.yml --syntax-check
ansible-playbook site.yml --list-tasks
 
# --- Ausführen ---
ansible-playbook site.yml
ansible-playbook site.yml --check --diff
ansible-playbook site.yml --limit rhel9
ansible-playbook site.yml --tags konfiguration
ansible-playbook site.yml -e "variable=wert"
ansible-playbook site.yml --start-at-task "Name"
ansible-playbook site.yml -b -K          # sudo mit Passwortabfrage
 
# --- Ad-hoc ---
ansible all -m ansible.builtin.command -a "uptime"
ansible all -m ansible.builtin.setup -a "filter=ansible_distribution*"
 
# --- Dokumentation ---
ansible-doc -l
ansible-doc ansible.builtin.package
ansible-doc -s ansible.builtin.service   # nur Beispiel-Snippet
 
# --- Vault ---
ansible-vault create   secrets.yml
ansible-vault edit     secrets.yml
ansible-vault encrypt_string 'wert' --name 'variable'
 
# --- Galaxy ---
ansible-galaxy collection install community.general
ansible-galaxy init roles/meine-rolle
ansible-galaxy collection list

C. Gegenüberstellung RHEL ↔ Ubuntu

Bereich RHEL 9 Ubuntu 24.04
Paketmanager dnf apt
Ansible-Faktum os_family RedHat Debian
Webserver-Paket httpd apache2
Webserver-Dienst httpd apache2
Webserver-Benutzer apache www-data
Webserver-Konfiguration /etc/httpd/ /etc/apache2/
NTP-Dienst chronyd chrony
SSH-Dienst sshd ssh
MariaDB-Paket mariadb-server mariadb-server
Firewall firewalld ufw
Mandatory Access Control SELinux AppArmor
Repositorydefinition /etc/yum.repos.d/ /etc/apt/sources.list.d/
Netzwerkkonfiguration NetworkManager netplan
Paketcache aktualisieren automatisch update_cache: true nötig
Logdateien Apache /var/log/httpd/ /var/log/apache2/

D. Lösungshinweise zu den Übungen

Übung 5.1 — Kernel-Version aller Hosts:

ansible all -m ansible.builtin.setup -a "filter=ansible_kernel"

Übung 7.1 — Idempotente Paketinstallation:

---
- name: Werkzeuge installieren
  hosts: all
  become: true
  tasks:
    - name: Basiswerkzeuge sicherstellen
      ansible.builtin.package:
        name:
          - vim
          - curl
          - git
        state: present

Übung 9.1 — Distributionsabhängiger Editor:

- name: vim auf RHEL
  ansible.builtin.dnf:
    name: vim-enhanced
    state: present
  when: ansible_facts['os_family'] == 'RedHat'

- name: neovim auf Ubuntu
  ansible.builtin.apt:
    name: neovim
    state: present
    update_cache: true
  when: ansible_facts['os_family'] == 'Debian'

Übung 10.1 — motd-Template:

templates/motd.j2
{{ ansible_managed }}
------------------------------------------------------------
 Host        : {{ ansible_facts['fqdn'] }}
 IP-Adresse  : {{ ansible_facts['default_ipv4']['address'] }}
 System      : {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}
 Kernel      : {{ ansible_facts['kernel'] }}
 CPU-Kerne   : {{ ansible_facts['processor_vcpus'] }}
 Speicher    : {{ ansible_facts['memtotal_mb'] }} MB
------------------------------------------------------------

Ende des Tutorials.