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.
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.
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:
| 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.
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.
| 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.
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
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.
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.
Ansible wird ausschließlich auf dem Control Node installiert.
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
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
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 |
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:
ansible.cfg tatsächlich greift. Steht hier None, wurde keine gefunden.
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.
Ansible sucht seine Konfiguration in dieser Reihenfolge und nimmt die erste gefundene Datei — es wird nicht zusammengeführt:
ANSIBLE_CONFIG./ansible.cfg im aktuellen Arbeitsverzeichnis~/.ansible.cfg/etc/ansible/ansible.cfgFür dieses Tutorial legen wir ein Projektverzeichnis an:
mkdir -p ~/ansible-tutorial cd ~/ansible-tutorial
[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“.
Das Inventory beantwortet die Frage: Welche Systeme gibt es und wie sind sie gruppiert?
INI-Format (kompakt, gut für kleine Umgebungen):
[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):
--- 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
all.children verschachtelt werden.ungrouped enthält Hosts ohne eigene Gruppe.
Eine bewährte Konvention: Gruppen nach Funktion (webserver, datenbank) und zusätzlich nach Umgebung (prod, test) bilden. Ein Host ist dann in beiden.
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 |
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.
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 |
# 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.
# 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.
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).
YAML kennt drei Bausteine: Skalare (einzelne Werte), Listen und Dictionaries (Schlüssel-Wert-Paare).
--- # 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:
name: wert, nicht name:wert.# 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.
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.
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?)
--- - 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.
# 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.
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 |
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.
--- - 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:
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
--- webserver_paket: httpd webserver_dienst: httpd webserver_konfig: /etc/httpd/conf.d/tutorial.conf firewall_werkzeug: firewalld
--- 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.
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
- 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.
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:
roles/x/defaults/main.yml)group_vars/allgroup_vars/<gruppe>host_vars/<host>vars:)–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.
# 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.
# 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.
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
notify muss exakt dem Handler-Namen entsprechen.- ansible.builtin.meta: flush_handlers.–force-handlers ändert das.# 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.
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.
| 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.
# {{ 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.
| Syntax | Bedeutung |
|---|---|
ansible2 | Ausdruck wird ausgegeben |
{% … %} | Anweisung (if, for, set) — keine Ausgabe |
{# … #} | Kommentar, erscheint nicht in der Ausgabe |
{%- … -%} | Anweisung, entfernt umgebende Leerzeilen |
{{ 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.
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.
| 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 |
--- webserver_docroot: /var/www/html webserver_port: 80 erlaubte_netze: - 192.168.56.0/24
--- webserver_paket: httpd webserver_dienst: httpd webserver_konfig: /etc/httpd/conf.d/tutorial.conf webserver_benutzer: apache
--- webserver_paket: apache2 webserver_dienst: apache2 webserver_konfig: /etc/apache2/conf-available/tutorial.conf webserver_benutzer: www-data
--- - 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 }}"
<!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>
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
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
Das Playbook aus Kapitel 11 ist bereits an der Grenze der Übersichtlichkeit. Rollen lösen drei Probleme: Wiederverwendbarkeit, Lesbarkeit und Teilbarkeit im Team.
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 | — |
Der elegante Standardweg, um in einer Rolle zwischen RHEL und Ubuntu zu unterscheiden:
roles/webserver/vars/ ├── RedHat.yml └── Debian.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.
--- - 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 |
# 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
--- 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.
# 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...
echo "MeinVaultPasswort" > ~/.vault_pass chmod 600 ~/.vault_pass
[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
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.
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
Ansible-Code ist Code und gehört in ein Repository. Der praktische Nutzen:
name: — es erscheint in der Ausgabe und ist die Dokumentation zur Laufzeit.webserver_port statt port.ansible.builtin.copy statt copy.true/false.| 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 |
# 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
Sobald mehrere Personen automatisieren, reicht die Kommandozeile nicht mehr. AWX (Open Source) bzw. die Red Hat Ansible Automation Platform (unterstützt) ergänzen:
Statische Inventory-Dateien werden in Cloud-Umgebungen schnell unbrauchbar. Dynamische Inventory-Plugins fragen die Quelle direkt ab:
--- 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.
rhel-system-roles bzw. linux-system-roles, unter anderem für SSH-Härtung, Zeitsynchronisation und Firewallansible-doc <modul> liefert offline eine vollständige Modulbeschreibung mit Beispielen — oft schneller als die Websuche| 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 |
# --- 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
| 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/ |
Ü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:
{{ 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.