Ansible / IaC Praxisleitfaden

Ansible Roles: Saubere Struktur für Production-Playbooks

Wie du Ansible Roles richtig aufbaust, Defaults von Vars trennst, Handlers sauber verwendest und deine Playbooks mit Molecule testest – damit Production-Deployments reproducible und wartbar bleiben.

E
Emre Hayta
21. März 2026 10 Min. Lesezeit
Ansible Roles IaC DevOps
Inhaltsverzeichnis

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:

bash
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:

yaml
# 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 in defaults/. Alles, was intern fest ist und nicht geändert werden soll, kommt in vars/.

Die Ansible-Variablen-Präzedenz (von niedrig nach hoch) erklärt warum:

yaml
# 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.

Termin buchen →

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:

yaml
# 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:

yaml
# 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.

yaml
# 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 listen kö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:

bash
# 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:

yaml
# 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:

yaml
# 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.

bash
# 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:

yaml
# 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
yaml
# 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.

yaml
# 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"
bash
# 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:

yaml
# 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