Fullmoon System

Ansible-Playbooks in der Praxis: Inventory, Idempotenz, Vault und rollende Bereitstellung

EdwardMoon

Ansible-Playbooks verwalten Pakete, Konfigurationsdateien und Dienstzustände mehrerer Linux-Server deklarativ. Das verringert ausgelassene manuelle Schritte und Unterschiede zwischen Umgebungen. Wer nach einem erfolgreichen ping sofort alle Server bearbeitet, command und shell ungeprüft einsetzt oder Passwörter in YAML ablegt, kann mit der Automatisierung jedoch auch Ausfälle und Datenoffenlegung schneller verbreiten.

Dieser Leitfaden ist weder an eine bestimmte Distribution noch an eine alte Ansible-Version gebunden. Er verbindet eine isolierte Python-Umgebung, YAML-Inventory, ansible.builtin-FQCN, Idempotenz, Handler, check und diff, Vault, serial sowie block und rescue zu einem überprüfbaren Betriebsablauf. Vor der Anwendung die offizielle Dokumentation der eingesetzten ansible-core- und Collection-Versionen prüfen.

Ansible-Automatisierung: Inventory, Steuerknoten, SSH, Servergruppen, Prüfung und Wiederherstellung
Inventory und deklarative Tasks auf dem Steuerknoten prüfen, über SSH schrittweise auf die Server der jeweiligen Umgebung anwenden und Ergebnisse kontrollieren

Wichtige Bestandteile eines Ansible-Playbooks

Bestandteil Aufgabe Vorgabe für den Betrieb
Inventory Zielhosts, Gruppen und Verbindungsvariablen dev, stage und prod trennen und doppelte Variablendefinitionen minimieren
Play Zielgruppe und Ausführungsrichtlinien hosts, become, serial und Fehlerstrategie ausdrücklich festlegen
Task Ein einzelner Modulaufruf Namen, FQCN und einen ausdrücklichen Zielzustand verwenden
Module Konkrete Aktionen für Pakete, Dateien, Dienste und weitere Ressourcen Wenn möglich spezialisierte Module statt command oder shell verwenden
Handler Folgeaktion, die nur nach einer Änderung ausgeführt wird Für geprüfte Dienstneustarts nach Konfigurationsänderungen verwenden
Role Wiederverwendbares Bündel aus tasks, handlers, templates und defaults Kleine Verantwortungsbereiche bilden und Schnittstellenvariablen dokumentieren
Collection Verteilungseinheit für Module, Plugins und Rollen Getestete Versionen festlegen und Änderungen prüfen
Im Ansible-Statusbericht bedeutet ok, dass der gewünschte Zustand bereits besteht, changed eine tatsächliche Änderung und failed einen Fehler. Für den Nachweis der Idempotenz reicht Erfolg allein nicht: Bei erneuter Ausführung muss changed=0 gelten, und es dürfen nur die erwarteten Handler laufen.

Ausführungsumgebung vorbereiten

Auf dem Steuerknoten eine venv pro Projekt erstellen, statt Pakete direkt mit dem System-Python zu vermischen. ansible-core, Python, Konfigurationsdateien und Collection-Pfade dokumentieren, damit sich die Automatisierung reproduzieren lässt.

Ansible in einer Python-venv installieren

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install ansible-core ansible-lint

ansible --version
ansible-playbook --version
ansible-lint --version
python -m pip freeze > requirements-lock.txt

Für produktive Projekte getestete ansible-core- und Bibliotheksversionen über eine Lock-Datei oder ein freigegebenes Paket-Repository festlegen. Wer stets ungeprüft die neueste Version installiert, verliert bei geänderten Collection-Abhängigkeiten oder Python-Anforderungen die Reproduzierbarkeit.

Projektverzeichnis

install -d inventories/dev/group_vars/all inventories/prod/group_vars/all
install -d roles/web/{tasks,handlers,templates,defaults}
install -d playbooks/templates

find . -maxdepth 3 -type d | sort

Sichere Grundeinstellungen in ansible.cfg

[defaults]
inventory = inventories/dev/hosts.yml
roles_path = roles
host_key_checking = True
retry_files_enabled = False
interpreter_python = auto_silent
forks = 10
timeout = 15

[privilege_escalation]
become = False
become_ask_pass = True

Beispiele mit deaktiviertem host_key_checking verhindern die Erkennung von Man-in-the-Middle-Angriffen. Den SSH-Hostschlüsselfingerabdruck über einen vertrauenswürdigen Weg prüfen und in known_hosts aufnehmen. become nur für die Plays oder Tasks aktivieren, die es benötigen; Passwörter nicht in Konfigurationsdateien speichern.

Ein YAML-Inventory erstellen

Gruppennamen sollten Rollen und Ausfallbereiche beschreiben; ansible_host enthält die tatsächliche Verbindungsadresse. Separate Inventory-Quellen für Entwicklung und Produktion verringern das Risiko einer versehentlichen prod-Auswahl stärker als eine reine Trennung über Tags in derselben Datei.

inventories/dev/hosts.yml

all:
  children:
    web:
      hosts:
        dev-web-01:
          ansible_host: 192.0.2.11
        dev-web-02:
          ansible_host: 192.0.2.12
    database:
      hosts:
        dev-db-01:
          ansible_host: 192.0.2.21
  vars:
    ansible_user: automation
    ansible_become: true

Inventory-Syntax und Zielhosts prüfen

ansible-inventory -i inventories/dev/hosts.yml --graph
ansible-inventory -i inventories/dev/hosts.yml --list | jq .

ansible web   -i inventories/dev/hosts.yml   -m ansible.builtin.ping   --limit dev-web-01
ansible.builtin.ping führt keinen ICMP-Ping aus, sondern prüft die Ansible-Verbindung und die Ausführung von Python auf dem Ziel. Ein Erfolg bestätigt SSH, Python und die erforderlichen Rechte, aber nicht die Bereitschaft von Paketquellen, Speicherplatz oder Dienstabhängigkeiten.

Das erste Playbook mit Handlern

Das folgende Play beschreibt Paketinstallation, Konfigurationsvorlage und Dienstzustand. Jeder Task erhält einen verständlichen Namen und einen FQCN. Der Handler wird nur benachrichtigt, wenn template tatsächlich eine Änderung vornimmt. So entfallen unnötige Neustarts.

playbooks/web.yml

---
- name: Configure web servers
  hosts: web
  become: true
  gather_facts: true

  tasks:
    - name: Ensure Nginx is installed
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Render Nginx virtual host
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
        validate: '/usr/sbin/nginx -t -c %s'
      notify: Restart Nginx

    - name: Ensure Nginx is enabled and running
      ansible.builtin.service:
        name: nginx
        enabled: true
        state: started

  handlers:
    - name: Restart Nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

Dieses Beispiel verwaltet die vollständige /etc/nginx/nginx.conf eines neuen Übungshosts. Damit nginx -t -c %s die temporäre Datei als eigenständige Hauptkonfiguration prüfen kann, enthält die Vorlage events und http. Die Hauptkonfiguration eines vorhandenen produktiven Nginx damit nicht ungeprüft überschreiben, da sonst andere virtuelle Hosts entfallen können. Eine produktive Rolle, die nur einzelne vhosts verteilt, benötigt eine gesonderte Prüfung der zusammengesetzten Gesamtkonfiguration.

Beispiel für eine Jinja-Vorlage

Den folgenden Inhalt unter playbooks/templates/nginx.conf.j2 speichern. Die Variablen wie gezeigt in inventories/dev/group_vars/all/app.yml ablegen. Für app_port den Port der tatsächlich laufenden Anwendung einsetzen.

app_server_name: app.example.internal
app_port: 8080
events {
    worker_connections 1024;
}

http {
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    server {
        listen 80;
        server_name {{ app_server_name }};

        location /healthz {
            access_log off;
            return 200 "ok\n";
        }
        location / {
            proxy_pass http://127.0.0.1:{{ app_port }};
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

Vorabprüfung mit syntax, check und diff

Nach der Syntaxprüfung den Check-Modus auf einem einzelnen Host des Entwicklungs-Inventory ausführen und den Diff prüfen. Der Check-Modus simuliert Änderungen, wird aber nicht von allen Modulen unterstützt. Plays, deren Bedingungen Ergebnisse vorheriger Tasks verwenden, können sich dabei anders darstellen als bei einer tatsächlichen Ausführung.

ansible-playbook   -i inventories/dev/hosts.yml   playbooks/web.yml   --syntax-check

ansible-playbook   -i inventories/dev/hosts.yml   playbooks/web.yml   --check --diff   --limit dev-web-01

Ein Diff zeigt vorherige und neue Konfigurationswerte und kann Geheimnisse offenlegen. Für sensible Template-Tasks diff: false und no_log: true prüfen; außerdem Zugriff und Aufbewahrung von CI-Logs begrenzen. Nach erfolgreicher Prüfung mit beibehaltenem –limit tatsächlich ausführen und den Umfang anschließend gruppenweise erweitern.

ansible-lint playbooks/web.yml

ansible-playbook   -i inventories/dev/hosts.yml   playbooks/web.yml   --limit dev-web-01

ansible-playbook   -i inventories/dev/hosts.yml   playbooks/web.yml   --check --limit dev-web-01

Idempotente Playbooks schreiben

Unzuverlässiger Ansatz Empfohlener Ansatz Begründung
shell: echo >> file lineinfile·blockinfile·template Verhindert Duplikate bei erneuter Ausführung
shell: yum install package·dnf Liest den Istzustand und führt nur erforderliche Änderungen aus
Bei jedem Lauf mit command initialisieren creates, removes oder ein spezialisiertes Modul verwenden Abschlussbedingung ausdrücklich festlegen
Dienst bei jedem Lauf neu starten Handler notify Nur bei geänderter Konfiguration neu starten
Alle Fehler ignorieren failed_when·block/rescue Erwartete Fehler von tatsächlichen Ausfällen unterscheiden

Abschlussbedingungen für command festlegen

- name: Initialize application database once
  ansible.builtin.command:
    argv:
      - /usr/local/bin/myapp-init
      - --database
      - /var/lib/myapp/app.db
    creates: /var/lib/myapp/.initialized
  register: init_result
  # Das command-Modul meldet anhand von creates selbst skipped oder changed.

creates verhindert den command-Aufruf, wenn der angegebene Pfad bereits existiert. Sicherstellen, dass das Initialisierungsprogramm den Marker nach erfolgreichem Abschluss tatsächlich erzeugt. Ein pauschales changed_when: false verbirgt Änderungen und verhindert Handler-Auslösungen. Die Bedingung anhand der Bedeutung von Exitcode und Ausgabe formulieren.

Geheimnisse mit Ansible Vault schützen

Ansible Vault verschlüsselt Variablen und Dateien und reduziert damit Klartextgeheimnisse im Repository. Wie die offizielle Dokumentation betont, schützt Vault jedoch nur gespeicherte Daten. Während der Ausführung entschlüsselte Werte können in Modulargumenten, Diffs, debug-Ausgaben oder Fehlermeldungen erscheinen. Deshalb zusätzlich no_log, eingeschränkte Diffs und kontrollierten Logzugriff einsetzen.

Zeichenfolgen über die Terminaleingabe verschlüsseln

read -rsp 'Secret value: ' SECRET_VALUE
echo
printf '%s' "$SECRET_VALUE"   | ansible-vault encrypt_string       --vault-id prod@prompt       --stdin-name 'db_password'   > inventories/prod/group_vars/all/vault.yml
unset SECRET_VALUE

chmod 0600 inventories/prod/group_vars/all/vault.yml

Mit einer Vault-ID ausführen

ansible-playbook   -i inventories/prod/hosts.yml   playbooks/web.yml   --vault-id prod@prompt   --check --limit prod-web-01
# Mit !vault verschlüsselte YAML-Variablen werden beim Playbook-Aufruf mit --vault-id entschlüsselt.

Ausgabe sensibler Tasks unterdrücken

- name: Render application secret configuration
  ansible.builtin.template:
    src: app-secret.conf.j2
    dest: /etc/myapp/secret.conf
    owner: root
    group: root
    mode: '0600'
  no_log: true
  diff: false
no_log unterdrückt Ausgaben des betreffenden Tasks, bildet aber keine Sicherheitsgrenze gegen Schadcode, separate debug-Tasks oder Logs auf dem Zielsystem. Nach Möglichkeit einen externen Secret Manager und kurzlebige Zugangsdaten verwenden. Auch Vault-Passwortdateien gehören nicht in Git.

Rollende Bereitstellung mit Ansible

Nach der Bereitstellung zuerst mit meta: flush_handlers die benachrichtigten Neustarts ausführen und danach den Healthcheck durchführen. Für ein Upgrade entweder die freigegebene Version im Paketnamen angeben oder im Änderungsfenster myapp_package_state ausdrücklich auf latest setzen. present aktualisiert ein bereits installiertes Paket nicht automatisch.

Mit serial die Stapelgröße begrenzen, statt alle Produktionsserver gleichzeitig zu verändern. max_fail_percentage bricht ab, wenn der Fehleranteil im aktuellen Stapel den Wert überschreitet. Soll bereits ein Fehler unter zwei Hosts zum Abbruch führen, die strikte Überschreitung berücksichtigen und beispielsweise 49 statt 50 setzen.

---
- name: Roll out application safely
  hosts: web
  become: true
  serial: 2
  max_fail_percentage: 49

  pre_tasks:
    - name: Confirm target batch
      ansible.builtin.debug:
        msg: "Deploying to {{ ansible_play_batch }}"

  tasks:
    - name: Deploy application package
      ansible.builtin.package:
        name: myapp
        state: "{{ myapp_package_state | default('present') }}"
      notify: Restart MyApp

    - name: Restart changed services before checking health
      ansible.builtin.meta: flush_handlers

    - name: Verify local health endpoint
      ansible.builtin.uri:
        url: http://127.0.0.1:8080/healthz
        status_code: 200
        return_content: false
      register: health
      retries: 10
      delay: 3
      until: health.status == 200

  handlers:
    - name: Restart MyApp
      ansible.builtin.service:
        name: myapp
        state: restarted

Dieses Minimalbeispiel erklärt die Struktur. Hinter einem Loadbalancer jeden Host des Stapels zunächst aus dem Verkehr nehmen und bestehende Verbindungen auslaufen lassen. Danach bereitstellen, den Healthcheck ausführen und den Host wieder aufnehmen. Auch bei delegate_to und API-Modulen Zertifikatsprüfung, Fehlerbehandlung und sichere Wiederholbarkeit kontrollieren.

Fehlerbehandlung und Wiederherstellung

block wendet gemeinsame Angaben wie become und when auf zusammengehörige Tasks an; rescue und always beschreiben den Fehlerablauf. Bei erfolgreichem rescue gilt der ursprüngliche Fehler als behoben, sodass das Play fortgesetzt werden kann. Syntaxfehler und nicht erreichbare Hosts werden von rescue nicht behandelt. Für Verbindungsfehler sind daher eigene Richtlinien und Überwachung erforderlich.

- name: Back up current configuration
  ansible.builtin.copy:
    src: /etc/myapp/myapp.conf
    dest: /var/backups/myapp.conf.pre-ansible
    remote_src: true
    owner: root
    group: root
    mode: '0600'

- name: Update service configuration with recovery
  block:
    - name: Render candidate configuration
      ansible.builtin.template:
        src: myapp.conf.j2
        dest: /etc/myapp/myapp.conf
        owner: root
        group: root
        mode: '0640'
        validate: '/usr/local/bin/myapp --check-config %s'
      notify: Restart MyApp

  rescue:
    - name: Restore previous configuration
      ansible.builtin.copy:
        src: /var/backups/myapp.conf.pre-ansible
        dest: /etc/myapp/myapp.conf
        remote_src: true
        owner: root
        group: root
        mode: '0640'

    - name: Stop this host after recovery
      ansible.builtin.fail:
        msg: Configuration deployment failed and was restored

  always:
    - name: Record completion state
      ansible.builtin.debug:
        msg: "Configuration block finished for {{ inventory_hostname }}"

Nicht davon ausgehen, dass eine einzelne Wiederherstellungsdatei zugleich Anwendung und Datenbankschema zurücksetzt. Eine eigene Betriebsanweisung erstellen, die Prüfungen vor und nach der Bereitstellung, mögliche Paket-Rollbacks, Datenänderungen und den Ausführungszeitpunkt der Handler berücksichtigt.

Qualitätsprüfungen und Ausführungsreihenfolge

  1. Versionen von ansible-core, Python und Collections sowie den vorgesehenen Git-Commit dokumentieren.
  2. Ziele, Variablen und Gruppen mit inventory –graph und –list prüfen.
  3. ansible-lint und –syntax-check erfolgreich durchlaufen lassen.
  4. –check –diff auf einem einzelnen Entwicklungshost ausführen.
  5. Sensible Diffs und den Geltungsbereich von no_log prüfen.
  6. Nach der tatsächlichen Ausführung auf einem Host den Healthcheck und changed=0 bei einem erneuten Lauf prüfen.
  7. Abbruch- und Wiederherstellungsverfahren in stage mit kleinen serial-Stapeln testen.
  8. prod im freigegebenen Änderungsfenster bereitstellen und recap, Anwendungsmetriken sowie Logs aufbewahren.
ansible-config dump --only-changed
ansible-inventory -i inventories/prod/hosts.yml --graph
ansible-lint playbooks roles
ansible-playbook -i inventories/prod/hosts.yml playbooks/web.yml --syntax-check

ansible-playbook   -i inventories/prod/hosts.yml   playbooks/web.yml   --check --diff   --limit prod-web-01   --vault-id prod@prompt

Checkliste für den Betrieb

  • Inventory-Quellen und Schreibrechte für dev, stage und prod sind getrennt.
  • SSH-Hostschlüsselprüfung bleibt aktiv; Automatisierungskonto und sudo-Rechte sind auf das Nötige begrenzt.
  • Jeder Task hat einen Namen und FQCN; command und shell besitzen Abschluss- und Änderungsbedingungen.
  • Konfigurationsänderungen werden mit validate und Handlern abgesichert; erneute Läufe ergeben changed=0.
  • Vault, no_log, eingeschränkte Diffs und kontrollierter Logzugriff werden gemeinsam eingesetzt.
  • Der Umfang wächst schrittweise über syntax, lint, check, diff, Einzelhost und kleine serial-Stapel.
  • Für von block/rescue nicht behandelte Erreichbarkeits- und Syntaxfehler sowie Daten-Rollbacks bestehen Verfahren.
  • Commit, Inventory, limit, ausführende Person, recap und nachträgliche Prüfungen werden im Änderungsprotokoll festgehalten.

Offizielle Dokumentation und weiterführende Artikel

Zusammenfassung

Sichere Ansible-Automatisierung bildet Zielzustände, Änderungsbedingungen, Fehlergrenzen und die Prüfreihenfolge in Code ab. Inventories nach Umgebung trennen, Idempotenz mit spezialisierten FQCN-Modulen und Handlern herstellen und den Schutzumfang von Vault beachten. Erst nach erfolgreichen Syntax-, Lint-, Check- und Diff-Prüfungen, Einzelhosttests, serial-Stapeln und Wiederherstellungstests den Einsatz in der Produktion ausweiten.