Worum es in diesem Teil wirklich geht

Der erste Ansible-Beitrag hat Installation, Inventory und ein erstes Playbook eingeführt. Dieser Teil macht daraus eine belastbare Betriebsstruktur: ein Repository, das Hosts, Gruppen, Variablen, Rollen, Secrets, Tests und Ausführung so trennt, dass Änderungen reviewbar und wiederholbar bleiben.

Das Ziel ist nicht ein besonders großes Beispiel, sondern ein Muster, das in einem Lab beginnt und später in Test, Produktion, AWX oder Red Hat Ansible Automation Platform weiterleben kann. Ein Techniker soll nach diesem Beitrag wissen, wo welche Datei liegt, warum sie dort liegt und wie man vor produktiven Änderungen prüft, was passieren würde.

Zielbild: Repository als Betriebsvertrag

Ein brauchbares Ansible-Repository ist mehr als eine Sammlung von Playbooks. Es ist der Vertrag zwischen Architektur, Betrieb und Review: Welche Systeme gibt es? Welche Gruppenlogik gilt? Welche Werte sind global, welche umgebungsspezifisch, welche hostbezogen? Welche Rolle setzt welchen Zustand? Welche Secrets sind verschlüsselt? Und welche Befehle gelten als technische Abnahme?

Control Node, CI oder AWX
|
| liest ansible.cfg
v
Inventory pro Umgebung -> group_vars / host_vars / vault
|
| ruft Playbooks auf
v
Rollen mit Defaults, Tasks, Handlern und Templates
|
| setzen Zustände idempotent
v
Managed Nodes: SLES, Rocky, Alma, Debian, Ubuntu, Netzwerkdienste
  • Inventories beantworten: Auf welche Systeme wirkt ein Lauf?
  • Variablen beantworten: Mit welchen Werten wird ein Zustand berechnet?
  • Rollen beantworten: Welche wiederverwendbare Betriebslogik wird angewendet?
  • Vault beantwortet: Welche sensiblen Werte dürfen im Repository liegen und trotzdem nicht im Klartext stehen?
  • Check Mode, Diff Mode und Idempotenzlauf beantworten: Ist die Änderung nachvollziehbar und wiederholbar?

YAML oder YAML: was ist der Unterschied?

Technisch beschreiben .yaml und .yaml dasselbe Dateiformat. .yaml ist historisch aus Umgebungen entstanden, in denen Dateiendungen kurz gehalten wurden. Heute ist .yaml meist die klarere Schreibweise, weil sie den Formatnamen vollständig ausdrückt. Ansible akzeptiert beide Varianten.

Für Teams ist nicht entscheidend, welche Endung „richtiger“ ist, sondern dass sie im Repository einheitlich verwendet wird. Wir schreiben in Beispielen bewusst .yaml beziehungsweise in Codeblöcken language: yaml, auch wenn manche Ansible-Defaults und viele bestehende Rollen weiterhin main.yaml, site.yaml oder requirements.yaml heißen. In bestehenden Ansible-Konventionen bleibt .yaml völlig legitim; neue ForgeOne-Beispiele bevorzugen .yaml, außer ein etabliertes Tool erwartet konkret .yaml.

Repository-Struktur mit klaren Grenzen

Die Struktur sollte Umgebungen, Playbooks, Rollen, Collections und Secrets trennen. Dadurch können Rollen unverändert gegen Lab und Produktion laufen, während Hostlisten, Gruppenwerte und Vault-Dateien je Umgebung getrennt bleiben.

ansible-platform/
├── ansible.cfg
├── inventories/
│ ├── lab/
│ │ ├── hosts.yaml
│ │ ├── group_vars/
│ │ │ ├── all.yaml
│ │ │ ├── linux.yaml
│ │ │ └── vault.yaml
│ │ └── host_vars/
│ │ └── sles16-client01.example.test.yaml
│ └── prod/
│ ├── hosts.yaml
│ ├── group_vars/
│ └── host_vars/
├── playbooks/
│ ├── baseline.yaml
│ └── patch-window.yaml
├── roles/
│ └── linux_baseline/
│ ├── defaults/main.yaml
│ ├── tasks/main.yaml
│ ├── handlers/main.yaml
│ └── templates/issue.j2
└── collections/
└── requirements.yaml

Wichtig ist die Richtung der Abhängigkeit: Playbooks rufen Rollen auf. Rollen kennen Defaults und Templates. Inventories liefern Zielsysteme und Werte. Secrets liegen verschlüsselt. Rollen sollten möglichst nicht wissen, ob sie gerade in Lab oder Produktion laufen; diese Entscheidung kommt aus Inventory und Variablen.

ansible.cfg: Verhalten projektweit fixieren

Ohne projektlokale ansible.cfg hängt ein Lauf zu stark von Benutzerprofil, Systemkonfiguration und Shell-Umgebung ab. Für Teamarbeit und CI sollte die relevante Konfiguration im Repository liegen.

ini
[defaults]
inventory = inventories/lab/hosts.yaml
roles_path = roles
collections_path = .ansible/collections
interpreter_python = auto_silent
host_key_checking = True
retry_files_enabled = False
stdout_callback = yaml
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
timeout = 30
[privilege_escalation]
become = True
become_method = sudo
bash
ansible-config view
ansible-config dump --only-changed

ansible-config view zeigt, welche Konfiguration Ansible tatsächlich lädt. ansible-config dump --only-changed macht sichtbar, welche Werte vom Standard abweichen. Das ist nützlich, wenn ein Lauf lokal anders reagiert als im CI-Runner oder in AWX.

Inventory nach Umgebung, Distribution und Funktion

Ein Inventory sollte nicht nur Servernamen sammeln. Es beschreibt, welche Systeme zu welcher Umgebung, Distribution und Betriebsfunktion gehören. Ein Host kann in mehreren Gruppen liegen: zum Beispiel linux, suse und monitoring_targets.

yaml
all:
children:
linux:
children:
suse:
hosts:
sles16-client01.example.test:
ansible_host: 172.31.52.21
rhel_family:
hosts:
rocky10-client01.example.test:
ansible_host: 172.31.52.31
monitoring_targets:
hosts:
sles16-client01.example.test:
rocky10-client01.example.test:
vars:
ansible_user: labadmin
ansible_python_interpreter: /usr/bin/python3

Die Gruppen suse und rhel_family beschreiben technische Unterschiede. monitoring_targets beschreibt eine Betriebsfunktion. Dadurch kann dieselbe Rolle später gezielt nur gegen SLES-Systeme, gegen alle Linux-Systeme oder gegen alle Monitoring-Ziele laufen.

Variablenmodell: wenige Orte, klare Regeln

Ansible kennt sehr viele Variablenquellen. In der Praxis sollte man bewusst wenige verwenden: role defaults für überschreibbare Standardwerte, group_vars/all für globale Konventionen, group_vars/<gruppe> für Gruppenlogik, host_vars für echte Ausnahmen und Vault für Secrets.

yaml
# inventories/lab/group_vars/all.yaml
timezone: Europe/Vienna
baseline_owner: platform-team
# inventories/lab/group_vars/linux.yaml
linux_baseline_packages:
- chrony
- vim
- curl
linux_baseline_services:
- chronyd
# inventories/lab/host_vars/sles16-client01.example.test.yaml
linux_baseline_packages:
- chrony
- vim
- curl
- supportutils

Host-Variablen sollten Ausnahmen bleiben. Wenn viele Hosts dieselbe Ausnahme brauchen, ist meistens eine neue Gruppe sinnvoller. Das hält Reviews lesbar und verhindert, dass Konfiguration in vielen host_vars-Dateien versteckt wird.

Rollen statt wachsender Einzelplaybooks

Ein Playbook sollte orchestrieren, nicht alle Details enthalten. Rollen kapseln wiederverwendbare Betriebslogik. Das Playbook entscheidet dann nur, welche Rolle auf welche Zielgruppe angewendet wird.

yaml
# playbooks/baseline.yaml
---
- name: Linux baseline ausrollen
hosts: linux
become: true
gather_facts: true
roles:
- role: linux_baseline
yaml
# roles/linux_baseline/tasks/main.yaml
---
- name: Basispakete installieren
ansible.builtin.package:
name: "{{ linux_baseline_packages }}"
state: present
- name: Baseline-Dienste aktivieren
ansible.builtin.service:
name: "{{ item }}"
enabled: true
state: started
loop: "{{ linux_baseline_services }}"
- name: Betriebsnotiz schreiben
ansible.builtin.template:
src: issue.j2
dest: /etc/issue.d/forgeone-baseline.issue
owner: root
group: root
mode: '0644'
notify: Baseline-Hinweis geändert
yaml
# roles/linux_baseline/handlers/main.yaml
---
- name: Baseline-Hinweis geändert
ansible.builtin.debug:
msg: "Baseline-Hinweis wurde auf {{ inventory_hostname }} aktualisiert."

Das wirkt klein, ist aber der entscheidende Schritt: Ab jetzt kann dieselbe Rolle in mehreren Umgebungen laufen, mit unterschiedlichen Variablen versorgt werden und später durch Molecule, ansible-lint oder CI geprüft werden.

Vault: Secrets nicht verstecken, sondern kontrolliert verschlüsseln

Ansible Vault schützt sensible Werte im Repository vor Klartextablage. Es ersetzt keine Rechtevergabe und keine zentrale Secrets-Plattform, verhindert aber, dass Passwörter, Tokens oder private Parameter direkt im Git-Verlauf landen.

bash
ansible-vault create inventories/lab/group_vars/vault.yaml
ansible-vault encrypt_string \
--vault-id lab@prompt \
'BeispielPasswort' \
--name 'ansible_become_password'
yaml
# inventories/lab/group_vars/linux.yaml
ansible_become_password: "{{ vault_ansible_become_password }}"
# inventories/lab/group_vars/vault.yaml
vault_ansible_become_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;lab
396661303834...

Im Team sollte klar sein, welche Vault-IDs es gibt, wer die Passphrase besitzt, ob im CI ein Secret-Store verwendet wird und wie man Vault-Dateien rotiert. Unklarer Vault-Betrieb ist später schwerer zu debuggen als ein normales Playbook.

Check Mode, Diff Mode und technische Abnahme

Vor produktiven Änderungen sollte ein Lauf immer mindestens syntaktisch geprüft, simuliert, mit Diff betrachtet und danach auf Idempotenz getestet werden. Der zweite Lauf ist wichtig: Er zeigt, ob die Rolle wirklich Zustände beschreibt oder jedes Mal erneut Änderungen erzeugt.

bash
ansible-inventory -i inventories/lab/hosts.yaml --graph
ansible-inventory -i inventories/lab/hosts.yaml --list
ansible-playbook -i inventories/lab/hosts.yaml playbooks/baseline.yaml --syntax-check
ansible-playbook -i inventories/lab/hosts.yaml playbooks/baseline.yaml --check --diff
ansible-playbook -i inventories/lab/hosts.yaml playbooks/baseline.yaml --limit sles16-client01.example.test --diff
ansible-playbook -i inventories/lab/hosts.yaml playbooks/baseline.yaml --check --diff
Prüfschritt Erwartung
--------------------------- ---------------------------------------------
Inventory --graph Gruppen und Hosts sind wie geplant sichtbar
Inventory --list Variablen werden am Host korrekt aufgelöst
Syntax-Check Playbook ist syntaktisch gültig
Check Mode + Diff geplante Änderungen sind nachvollziehbar
Limit auf Einzelhost Änderung wird zuerst klein ausgerollt
Zweiter Check Mode changed=0 oder bewusst erklärte Abweichungen

Wenn Check Mode keine sinnvolle Aussage liefert, muss das dokumentiert werden. Nicht jedes Modul kann Check Mode vollständig simulieren. Gerade deshalb ist der limitierte erste Echtlauf auf einem einzelnen Host so wichtig.

Typische Fehler in dieser Phase

  • Variablen werden in Playbooks hart codiert, obwohl sie in group_vars oder defaults gehören.
  • host_vars werden als Müllhalde für Gruppenlogik verwendet.
  • Secrets liegen in normalen YAML-Dateien oder landen versehentlich im Git-Verlauf.
  • Rollen haben keine Defaults und funktionieren nur mit exakt einem Inventory.
  • Playbooks werden ohne --check, --diff und zweiten Idempotenzlauf produktiv ausgeführt.
  • Inventory-Gruppen mischen Umgebung, Distribution und Funktion unklar in einem Namen.

Wie es weitergeht

Der nächste sinnvolle Teil ist eine echte Linux-Baseline: Pakete, Dienste, SSH, Zeitbasis, Firewall und einfache Compliance-Prüfungen. Danach kann dieselbe Struktur für Updates, Reboots, Rollenqualität, CI und AWX weiterverwendet werden.