Warum Roles statt flacher Playbooks?
Wer Ansible ein paar Monate produktiv einsetzt, kennt das Problem: Das Repository wächst, Playbooks werden länger, Tasks wiederholen sich in verschiedenen Kontexten. Was als saubere site.yml begann, ist irgendwann eine 800-Zeilen-Datei, die niemand mehr anfassen möchte.
Ansible Roles sind die Antwort darauf. Sie erzwingen eine klare Trennung von Verantwortlichkeiten, machen Code wiederverwendbar und erlauben es, Konfiguration von Logik zu trennen. Für Production-Umgebungen sind Roles keine Option – sie sind Pflicht.
Die Vorteile im Überblick:
- Wiederverwendbarkeit: Eine Role kann in mehreren Playbooks und Projekten verwendet werden
- Testbarkeit: Isolierte Roles lassen sich mit Molecule unabhängig testen
- Versionierbarkeit: Roles können separat getaggt und über Ansible Galaxy verteilt werden
- Teamarbeit: Klare Strukturen machen es einfacher, Verantwortung aufzuteilen
- Übersicht: Das Haupt-Playbook bleibt kurz und deklarativ
Die richtige Verzeichnisstruktur
Ansible erwartet eine bestimmte Verzeichnisstruktur für Roles. ansible-galaxy role init myrole generiert das Grundgerüst automatisch. Hier ist die vollständige Struktur mit allen relevanten Verzeichnissen:
roles/
└── nginx/
├── defaults/
│ └── main.yml # Überschreibbare Standardwerte
├── vars/
│ └── main.yml # Feste, interne Variablen
├── tasks/
│ ├── main.yml # Einstiegspunkt, importiert Sub-Tasks
│ ├── install.yml # Pakete installieren
│ ├── configure.yml # Konfiguration anwenden
│ └── tls.yml # TLS-spezifische Tasks
├── handlers/
│ └── main.yml # Notify-Handler (z.B. service restart)
├── templates/
│ └── nginx.conf.j2 # Jinja2-Templates
├── files/
│ └── favicon.ico # Statische Dateien
├── meta/
│ └── main.yml # Abhängigkeiten, Galaxy-Metadaten
└── README.md # Dokumentation
Ein typisches Haupt-Playbook sieht dann so aus – kurz, lesbar, deklarativ:
# site.yml
---
- name: Webserver konfigurieren
hosts: webservers
become: true
roles:
- role: common
- role: nginx
vars:
nginx_worker_processes: 4
- role: certbot
defaults vs. vars: Der häufigste Fehler
Einer der meistverbreiteten Fehler bei Ansible Roles ist der Unterschied zwischen defaults/main.yml und vars/main.yml. Diese Unterscheidung hat direkte Auswirkungen auf die Flexibilität und Wartbarkeit deiner Roles.
Faustregel: Alles, was der Nutzer der Role überschreiben soll, kommt indefaults/. Alles, was intern fest ist und nicht geändert werden soll, kommt invars/.
Die Ansible-Variablen-Präzedenz (von niedrig nach hoch) erklärt warum:
# defaults/main.yml – niedrigste Priorität, gut überschreibbar
---
nginx_port: 80
nginx_server_name: "{{ inventory_hostname }}"
nginx_worker_processes: "auto"
nginx_keepalive_timeout: 65
nginx_gzip_enabled: true
nginx_log_format: combined
# vars/main.yml – höhere Priorität, interne Konstanten
---
nginx_package: nginx
nginx_service: nginx
nginx_config_dir: /etc/nginx
nginx_sites_available: "{{ nginx_config_dir }}/sites-available"
nginx_sites_enabled: "{{ nginx_config_dir }}/sites-enabled"
Ein konkretes Beispiel: Jemand, der deine Role nutzt, kann einfach nginx_port: 8080 in seinem Inventory oder Playbook setzen und damit den Default überschreiben. Der interne Pfad nginx_config_dir bleibt dagegen fest – das ist so gewollt.
Brauchst du Unterstützung bei der Umsetzung?
30-Min Call — kostenlos, unverbindlich, konkret.
Tasks sauber strukturieren: main.yml als Dispatcher
Eine häufige Falle ist, alle Tasks in eine einzige tasks/main.yml zu packen. Ab einer gewissen Größe wird das unübersichtlich. Besser: main.yml als Dispatcher verwenden, der Sub-Task-Dateien importiert:
# tasks/main.yml
---
- name: Pakete installieren
ansible.builtin.import_tasks: install.yml
tags: [nginx, install]
- name: Nginx konfigurieren
ansible.builtin.import_tasks: configure.yml
tags: [nginx, configure]
- name: TLS einrichten
ansible.builtin.import_tasks: tls.yml
tags: [nginx, tls]
when: nginx_tls_enabled | default(false)
Wichtig: import_tasks ist statisch (wird zur Parse-Zeit aufgelöst), include_tasks ist dynamisch (wird zur Runtime aufgelöst). Für Production empfehle ich import_tasks als Standard – es ist vorhersehbarer und funktioniert zuverlässiger mit Tags.
Eine typische tasks/configure.yml mit sauberer Struktur:
# tasks/configure.yml
---
- name: Nginx-Hauptkonfiguration deployen
ansible.builtin.template:
src: nginx.conf.j2
dest: "{{ nginx_config_dir }}/nginx.conf"
owner: root
group: root
mode: "0644"
validate: "/usr/sbin/nginx -t -c %s"
notify: nginx reload
- name: Default-Site deaktivieren
ansible.builtin.file:
path: "{{ nginx_sites_enabled }}/default"
state: absent
notify: nginx reload
- name: Virtual Hosts einrichten
ansible.builtin.template:
src: vhost.conf.j2
dest: "{{ nginx_sites_available }}/{{ item.server_name }}.conf"
owner: root
group: root
mode: "0644"
loop: "{{ nginx_vhosts }}"
notify: nginx reload
- name: Virtual Hosts aktivieren
ansible.builtin.file:
src: "{{ nginx_sites_available }}/{{ item.server_name }}.conf"
dest: "{{ nginx_sites_enabled }}/{{ item.server_name }}.conf"
state: link
loop: "{{ nginx_vhosts }}"
notify: nginx reload
Handlers richtig nutzen
Handlers sind eine der elegantesten Features in Ansible – und gleichzeitig eine häufige Quelle von Bugs, wenn sie falsch eingesetzt werden. Das Grundprinzip: Ein Handler wird nur ausgeführt, wenn er durch notify ausgelöst wurde und wenn sich die auslösende Task tatsächlich geändert hat.
# handlers/main.yml
---
- name: nginx reload
ansible.builtin.service:
name: "{{ nginx_service }}"
state: reloaded
listen: nginx reload
- name: nginx restart
ansible.builtin.service:
name: "{{ nginx_service }}"
state: restarted
listen: nginx restart
- name: nginx start
ansible.builtin.service:
name: "{{ nginx_service }}"
state: started
enabled: true
listen: nginx start
Drei wichtige Punkte zu Handlers:
- Handlers laufen einmal am Ende des Plays, nicht sofort beim Notify. Wenn du sofortiges Handeln brauchst, nutze
meta: flush_handlers - reload vs. restart: Für Nginx-Konfigurationsänderungen reicht
reload– der Prozess bleibt am Laufen und akzeptiert neue Verbindungen während des Reloads - listen statt name: Mit
listenkönnen mehrere Handler auf denselben Notify-Namen reagieren – das macht das System flexibler
Ein häufiger Fehler: Nginx-Service direkt in Tasks mit state: restarted neu starten, anstatt Handlers zu verwenden. Das führt bei mehreren Konfigurationsänderungen zu unnötigen Neustarts.
Tags und Selektivität: Gezielt deployen
In Production-Umgebungen will man selten das gesamte Playbook laufen lassen. Tags ermöglichen es, gezielt einzelne Teile auszuführen – etwa nur die Konfiguration neu deployen, ohne Pakete zu reinstallieren:
# Nur Konfiguration anwenden
ansible-playbook site.yml --tags configure
# Alles außer Installation
ansible-playbook site.yml --skip-tags install
# Nur TLS-bezogene Tasks
ansible-playbook site.yml --tags tls
# Dry-Run für einen bestimmten Host
ansible-playbook site.yml --limit webserver01 --check --diff
Eine bewährte Tag-Taxonomie für Production-Playbooks:
# Empfohlene Tag-Kategorien
# Aktion-Tags (was passiert)
tags: [install] # Pakete installieren
tags: [configure] # Konfiguration anwenden
tags: [service] # Services verwalten
tags: [deploy] # Applikation deployen
# Rollen-Tags (welche Rolle)
tags: [nginx]
tags: [postgresql]
tags: [common]
# Umgebungs-Tags
tags: [always] # Läuft immer, auch mit --tags
tags: [never] # Läuft nie, außer explizit aufgerufen
Role-Abhängigkeiten via meta
Manche Roles setzen andere voraus. Statt das im Playbook manuell zu verwalten, lassen sich Abhängigkeiten in meta/main.yml deklarieren:
# meta/main.yml der nginx-Role
---
galaxy_info:
role_name: nginx
author: emre_hayta
description: Nginx Webserver mit TLS und Virtual Hosts
license: MIT
min_ansible_version: "2.14"
platforms:
- name: Ubuntu
versions: ["22.04", "24.04"]
- name: Debian
versions: ["11", "12"]
galaxy_tags:
- nginx
- webserver
- tls
dependencies:
- role: common # Immer common zuerst
- role: firewall # Firewall muss vor nginx konfiguriert sein
vars:
firewall_allowed_ports:
- 80
- 443
Ansible führt Abhängigkeiten automatisch vor der eigentlichen Role aus. Bei Kreisabhängigkeiten bricht Ansible mit einem klaren Fehler ab.
Testing mit Molecule: Roles verifizieren bevor sie prod erreichen
Eine Role ohne Tests ist eine Zeitbombe. Molecule ist das Standard-Testtool für Ansible Roles – es startet Container oder VMs, spielt die Role ein, und prüft das Ergebnis mit Testinfra oder Ansible-eigenen Assert-Tasks.
# Molecule installieren
pip install molecule molecule-docker
# Test-Szenario initialisieren
cd roles/nginx
molecule init scenario --driver-name docker
# Tests ausführen
molecule test # Vollständiger Zyklus: create, converge, verify, destroy
molecule converge # Nur Role anwenden (für iterative Entwicklung)
molecule verify # Nur Verifikation
molecule idempotency # Idempotenz prüfen
Eine einfache Molecule-Konfiguration für die Nginx-Role:
# molecule/default/molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu-2404
image: geerlingguy/docker-ubuntu2404-ansible:latest
pre_build_image: true
- name: debian-12
image: geerlingguy/docker-debian12-ansible:latest
pre_build_image: true
provisioner:
name: ansible
playbooks:
converge: converge.yml
verifier:
name: ansible
# molecule/default/verify.yml
---
- name: Nginx-Installation verifizieren
hosts: all
tasks:
- name: Nginx-Service läuft
ansible.builtin.service_facts:
- name: Prüfen ob nginx aktiv
ansible.builtin.assert:
that:
- ansible_facts.services['nginx.service'].state == 'running'
fail_msg: "Nginx ist nicht aktiv!"
- name: Port 80 ist erreichbar
ansible.builtin.wait_for:
port: 80
timeout: 10
- name: HTTP-Response prüfen
ansible.builtin.uri:
url: "http://localhost"
status_code: 200
Molecule ist auch ideal für CI/CD: In einer GitHub Actions Pipeline kann automatisch bei jedem Push geprüft werden, ob die Role auf allen Zielplattformen korrekt funktioniert – bevor irgendjemand sie auf Production anwendet.
Ansible Galaxy und Wiederverwendung
Für Standard-Aufgaben wie Nginx, PostgreSQL oder Docker muss man das Rad nicht neu erfinden. Ansible Galaxy bietet tausende geprüfte Roles. Bewährte Community-Maintainer sind etwa geerlingguy, whose Roles faktisch Industriestandard sind.
# requirements.yml – externe Abhängigkeiten deklarieren
---
roles:
- name: geerlingguy.docker
version: "7.1.0" # Immer pinnen!
- name: geerlingguy.postgresql
version: "3.5.0"
collections:
- name: community.general
version: ">=8.0.0"
- name: ansible.posix
version: ">=1.5.0"
# Abhängigkeiten installieren
ansible-galaxy install -r requirements.yml
ansible-galaxy collection install -r requirements.yml
# In roles/ Verzeichnis installieren (für Repo-Einbindung)
ansible-galaxy install -r requirements.yml -p roles/
Wichtig: Versions-Pins sind in Production Pflicht. Ohne Pin kann ein ansible-galaxy install morgen eine breaking change bringen. Halte die requirements.yml unter Versionskontrolle und update Versionen bewusst und getestet.
Für eigene interne Roles, die in mehreren Projekten verwendet werden, empfiehlt sich ein privates Git-Repository als Quelle:
# requirements.yml mit privaten Roles aus Git
---
roles:
- name: internal_nginx
src: git+ssh://git@git.example.com/ansible-roles/nginx.git
version: v2.3.1
scm: git
Fazit: Struktur ist keine Bürokratie
Ansible Roles sind auf den ersten Blick mehr Aufwand als einfache Playbooks. Die Verzeichnisstruktur erzeugt Overhead, defaults vs. vars muss man verstehen, Molecule will eingerichtet sein.
Aber dieser Invest zahlt sich schnell aus: Wer Roles konsequent einsetzt, hat ein Repository, das nach sechs Monaten noch lesbar ist, in dem neue Teammitglieder sich schnell zurechtfinden, und in dem Production-Änderungen reproduzierbar und testbar sind.
Die wichtigsten Regeln zusammengefasst:
- Defaults für überschreibbare Werte, vars für interne Konstanten
- main.yml als Dispatcher – Sub-Tasks in eigene Dateien auslagern
- Handlers statt direkter Service-Restarts in Tasks
- import_tasks statt include_tasks als Standard in Production
- Tags konsistent vergeben – nach Aktion und Rolle
- Molecule-Tests für jede Role, die in Production geht
- Versions-Pins für alle externen Abhängigkeiten
Wer diese Grundsätze befolgt, baut Ansible-Infrastruktur, die nicht nach sechs Monaten zum Wegwerfprodukt wird.
Ansible-Implementierung für dein Team?
Von der Playbook-Struktur bis zum vollständig automatisierten Infrastructure-as-Code-Setup – gemeinsam bauen wir etwas Wartbares.
Kostenloses Erstgespräch buchen