SSD Nodes Learn
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-07-24

Ansible Tutorial: Erster Playbook auf VPS

Ansible mit pipx auf Ubuntu 24.04 installieren. Erstellen Sie ein Playbook zur VPS-Härtung und lösen Sie Permission denied sowie sudo Fehler effektiv.

Was Sie aufbauen

Eine Steuerungsmaschine mit installiertem Ansible und ein oder mehrere neue Ubuntu 24.04 VPSes, die nur das Standard-Image enthalten. Am Ende besitzen Sie eine Inventory-Datei mit den Servernamen, einen Ad-hoc-Ping zur Überprüfung der End-to-End-Authentifizierung und ein Playbook, das die gesamte Checkliste für neue VPSes als Code ausführt: ein Deploy-User mit Ihrem SSH-Key, ein gehärteter sshd, fail2ban, unattended upgrades und eine Firewall, die OpenSSH erlaubt, bevor sie alles andere blockiert. Wenden Sie dies auf einen oder zwanzig Server an. Führen Sie es zweimal aus; der zweite Durchlauf ändert nichts – das ist der entscheidende Vorteil.

Nach fünfzehn Jahren der Bereitstellung von VPSes kenne ich das typische Muster: Jeder konfiguriert die ersten fünf Server manuell, verliert dann aber ein ganzes Wochenende beim sechsten Server, weil sich niemand mehr an die Schritte der ersten fünf erinnert. Diese Anleitung vertieft die Informationen aus Verwalten mehrerer Linux-Server — lesen Sie diese, sobald Sie dabei ertappt werden, wie Sie denselben apt install in drei verschiedene Terminals tippen.

Was Ansible tatsächlich ist, in einem Absatz

Ansible ist agentenlos. Es muss kein Daemon auf den verwalteten Servern installiert werden: Die Control-Machine verbindet sich über Standard-SSH, kopiert ein kleines Python-Modul auf das Zielsystem, führt es aus, liest die ausgegebene JSON-Ausgabe und löscht das Modul wieder. Das Zielsystem benötigt lediglich python3, welches in jedem Standard-Ubuntu-Image enthalten ist. Das entscheidende Wort ist idempotent. Das bedeutet schlicht: Eine Aufgabe beschreibt einen Zustand, keine Aktion. state: present für ein Paket bedeutet „stelle sicher, dass dieses installiert ist“ und nicht „führe den Installer aus“. Wenn der Zustand bereits erreicht ist, ändert Ansible nichts und meldet ok anstatt changed. Diese Eigenschaft ist das Kernmerkmal des Produkts – sie ermöglicht das sichere erneute Ausführen eines Playbooks. Sichere Wiederholungen sind es, die ein Shell-Skript in Infrastructure as Code verwandeln.

Voraussetzungen und bekannte Probleme

  • Eine Steuerungsmaschine: Ihr Laptop oder ein kleiner VPS. Ich gehe von Ubuntu 24.04 aus; macOS funktioniert identisch, sobald pipx über Homebrew installiert wurde.
  • Ein oder mehrere Ziel-VPS, auf denen Ubuntu 24.04 auf KVM läuft und die als root erreichbar sind. Es wird keine Software auf ihnen installiert.
  • SSH-Key-Authentifizierung für jedes Ziel. Ansible nutzt genau die Authentifizierung, die Ihr ssh-Befehl verwendet — wenn ssh root@host nach einem Passwort fragt, schlägt Ansible fehl.
  • Unter Ubuntu 24.04 schlägt pip install ansible mit error: externally-managed-environment fehl. Das ist eine bewusste Richtlinie der Distribution und kein Fehler. Verwenden Sie pipx.
  • YAML-Leerzeichen sind Teil der Syntax. Eine falsche Einrückung führt zu mapping values are not allowed in this context, und ein Tabulator-Zeichen an beliebiger Stelle führt zum Abbruch.
  • Halten Sie auf jedem Ziel eine aktive SSH-Sitzung offen, während das Playbook sshd härtet. Jede Wiederherstellung, bei der ich einem Kunden helfen konnte, betraf einen Ausschluss, der durch das Schließen der letzten Sitzung „um sauber zu testen“ verursacht wurde.

Schritt 1: Ansible mit pipx auf der Control-Machine installieren, nicht mit pip

Der klassische Instinkt ist pip3 install ansible. Auf einem komplett neuen 24.04-Image schlägt dies bereits einen Schritt zu früh fehl — Command 'pip3' not found, but can be installed with: sudo apt install python3-pip — und die Installation von pip führt nur zu weiteren Problemen:

pip3 install ansible
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

Ubuntu 24.04 markiert das System-Python als extern verwaltet (PEP 668), sodass pip nicht mit apt um dieselben Dateien konkurrieren kann. Nutzen Sie nicht --break-system-packages; der Flag ist ehrlich benannt. Die saubere Lösung ist pipx. Dies erstellt ein isoliertes virtualenv für Ansible und fügt die Binaries Ihrem PATH hinzu:

sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansible

Öffnen Sie nach pipx ensurepath eine neue Shell, damit die PATH-Änderung wirksam wird. --include-deps ist keine Dekoration: Das ansible-Paket enthält keine eigenen Console-Scripts — ansible, ansible-playbook und der Rest sind Entry-Points der ansible-core-Abhängigkeit — daher verweigert pipx die Installation ohne den Flag mit No apps associated with package ansible or its dependencies. Installieren Sie das ansible-Paket und nicht das nackte ansible-core — das vollständige Paket enthält die Community-Collections, und dieses Playbook verwendet Module aus zwei davon (ansible.posix und community.general).

ansible --version

Das korrekte Ergebnis beginnt mit einer Zeile wie ansible [core 2.19.x] und nennt das verwendete Python; jede aktuelle Core-Release ist für diese Zwecke geeignet. ansible: command not found bedeutet hingegen, dass ~/.local/bin noch nicht in Ihrem PATH liegt — nutzen Sie eine neue Shell oder source ~/.bashrc.

Dies ist die vollständige Installation. Die Zielsysteme erhalten keine Installation.

Schritt 2: SSH-Schlüsselzugriff auf jedes Zielsystem

ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20

Überprüfen Sie dies anschließend einmal pro Host:

ssh root@10.0.0.10 true && echo ok

Dieser eine Befehl erfüllt zwei Aufgaben: Er bestätigt, dass die Schlüsselauthentifizierung ohne Passwort funktioniert, und er speichert den Host-Key in known_hosts. Führen Sie dies jetzt aus, da Ansible einen nicht registrierten Host-Key als interaktive Abfrage während des Durchlaufs anzeigt, was wie ein Systemstillstand wirkt.

Schritt 3: das Inventory — zuerst INI, dann YAML bei steigender Größe

Das Inventory ist eine Textdatei, die alle Maschinen auflistet, auf denen Ansible Aktionen ausführt. Erstellen Sie inventory.ini in einem neuen Projektverzeichnis:

[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20

[vps:vars]
ansible_user=root

web1 ist ein von Ihnen gewählter Alias – dieser erscheint in der Ausgabe und ist das Ziel für --limit web1. ansible_host ist die tatsächliche Adresse. [vps] ist eine Gruppe, und [vps:vars] setzt Variablen für jeden Host in dieser Gruppe; ansible_user ist der Benutzer, mit dem Ansible sich anmeldet. Daneben steht ein ansible.cfg, damit Sie -i nicht mehr manuell eingeben müssen:

[defaults]
inventory = inventory.ini

Ansible liest ansible.cfg aus dem aktuellen Verzeichnis. Das gleiche Inventory im YAML-Format – speichern Sie es als inventory.yml und verweisen Sie ansible.cfg stattdessen auf diesen Namen – ist die bevorzugte Variante, sobald jeder Host mehrere Variablen besitzt:

vps:
  hosts:
    web1:
      ansible_host: 10.0.0.10
    web2:
      ansible_host: 10.0.0.20
  vars:
    ansible_user: root

Beide Formate sind gleichwertig. INI ist bei zwei Servern leichter zu überblicken; YAML skaliert besser bei zwanzig Servern. Wählen Sie eines aus und konzentrieren Sie sich auf die Arbeit.

Schritt 4: Ad-hoc-Befehle — das grüne Signal zur Bestätigung

ansible all -m ping

Dies ist kein ICMP. Das ping-Modul ist eine vollständige Testdurchführung: SSH-Login, Modulkopie, Python-Ausführung auf dem Zielsystem und Bereinigung. Das korrekte Ergebnis ist grün, ein Block pro Host:

web1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Grünes SUCCESS bedeutet, dass die Authentifizierung, der Python-Interpreter und der Transport funktionieren — das Playbook wird ebenfalls funktionieren. Rotes UNREACHABLE! bedeutet, dass der Transport fehlgeschlagen ist, bevor ein Modul ausgeführt wurde; die genaue Fehlermeldung und die Lösung finden Sie im Abschnitt zu den Fehlerzuständen unten. Zwei weitere Ad-hoc-Befehle sind wichtig:

ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --become

Ad-hoc-Befehle dienen für einmalige Ausführungen und Prüfungen. Alles, was Sie zweimal ausführen würden, gehört in ein Playbook.

Schritt 5: das erste Playbook — die new-VPS-Checkliste als Code

Dies umfasst alle Schritte, die Sie in den ersten zehn Minuten auf einem neuen Server manuell ausführen würden. Speichern Sie die Datei als site.yml:

---
- name: Baseline a fresh Ubuntu VPS
  hosts: vps
  become: true

  vars:
    deploy_user: deploy
    deploy_pubkey: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
    baseline_packages:
      - fail2ban
      - unattended-upgrades
      - ufw
    baseline_services:
      - fail2ban
      - unattended-upgrades

  tasks:
    - name: Create the deploy user
      ansible.builtin.user:
        name: "{{ deploy_user }}"
        groups: sudo
        append: true
        shell: /bin/bash

    - name: Install the deploy user's SSH key
      ansible.posix.authorized_key:
        user: "{{ deploy_user }}"
        key: "{{ deploy_pubkey }}"

    - name: Passwordless sudo for the deploy user
      ansible.builtin.copy:
        dest: /etc/sudoers.d/deploy
        content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
        mode: "0440"
        validate: /usr/sbin/visudo -cf %s

    - name: Install baseline packages
      ansible.builtin.apt:
        name: "{{ baseline_packages }}"
        state: present
        update_cache: true

    - name: Enable and start baseline services
      ansible.builtin.service:
        name: "{{ item }}"
        state: started
        enabled: true
      loop: "{{ baseline_services }}"

    - name: Harden sshd with a drop-in
      ansible.builtin.copy:
        dest: /etc/ssh/sshd_config.d/00-hardening.conf
        content: |
          PasswordAuthentication no
          KbdInteractiveAuthentication no
          PermitRootLogin prohibit-password
          X11Forwarding no
        mode: "0644"
        validate: /usr/sbin/sshd -t -f %s
      notify: Restart ssh

    - name: Allow OpenSSH through ufw
      community.general.ufw:
        rule: allow
        name: OpenSSH

    - name: Enable ufw with default deny
      community.general.ufw:
        state: enabled
        policy: deny

  handlers:
    - name: Restart ssh
      ansible.builtin.service:
        name: ssh
        state: restarted

Diese Zeilen sollten verstanden statt nur kopiert werden:

Variablen befinden sich unter vars: und werden mit "{{ deploy_user }}" referenziert — setzen Sie den gesamten Ausdruck in Anführungszeichen, wenn ein Wert mit einer geschweiften Klammer beginnt, da der YAML-Parser sonst einen Lesefehler begeht. Der lookup('file', ...) liest Ihren öffentlichen Schlüssel zur Laufzeit von der Control-Maschine aus, sodass das Playbook keine Keys enthält.

Die Schleife (Loop). loop: "{{ baseline_services }}" führt die Service-Aufgabe einmal pro Element aus; die Ausgabe zeigt jedes Element in einer eigenen Zeile an. Beachten Sie, dass die apt-Aufgabe die gesamte Paketliste in einem Durchgang verarbeitet — eine einzige apt-Transaktion ist schneller und ist das bevorzugte Muster für Pakete; Schleifen sind für Module gedacht, die tatsächlich jeweils nur ein Element bearbeiten.

Der Handler ist das zentrale Konzept. notify: Restart ssh bedeutet nicht „ssh jetzt neu starten“. Es stellt den Handler in eine Warteschlange, der einmal am Ende des Plays ausgeführt wird, und nur dann, wenn die benachrichtigende Aufgabe tatsächlich changed meldet. Führen Sie das Playbook morgen erneut aus: Die Drop-in-Datei ist bereits korrekt, die Copy-Aufgabe meldet ok und der sshd-Dienst wird nicht neu gestartet. Die validate:-Zeile dient als Sicherheitsmechanismus — sshd prüft die Datei, bevor die alte ersetzt wird. Ein Tippfehler führt so zum Abbruch der Aufgabe, anstatt den Daemon zu beschädigen.

PermitRootLogin prohibit-password statt no — bewusst gewählt. Dieses Playbook meldet sich mit einem Key als root an. prohibit-password deaktiviert Root-Logins per Passwort, lässt Ihren Zugriff jedoch bestehen. Sobald der Deploy-User verifiziert ist (ssh deploy@10.0.0.10 sudo true — die einfache Adresse, da web1 nur ein Ansible-Alias ist), wechseln Sie ansible_user=deploy in der Inventory und verschärfen die Einstellungen in einem späteren Durchgang auf no. Härten Sie das System in einer Reihenfolge, die Sie nicht selbst aussperrt.

Das Präfix 00- ist wichtig. Bei den meisten Schlüsselwörtern verwendet sshd das erste gefundene Vorkommen. Das Ubuntu-Konfigurationsfile sshd_config enthält sshd_config.d/*.conf in lexikalischer Reihenfolge vor dem eigentlichen Inhalt. Ubuntu 24.04 Cloud-Images enthalten bereits eine 60-cloudimg-settings.conf in diesem Verzeichnis. Anbieter, die Passwort-Logins über cloud-init aktivieren, fügen eine 50-cloud-init.conf mit PasswordAuthentication yes hinzu; durch die Benennung unserer Datei als 00-hardening.conf wird diese zuerst sortiert und überschreibt beide anderen.

Die Reihenfolge der Aufgaben ist die Sicherheitsgarantie. Allow OpenSSH wird vor Enable ufw mit einer Deny-Policy ausgeführt — Ansible arbeitet die Aufgaben strikt in der vorgegebenen Reihenfolge ab, sodass die Sicherheitslücke besteht, bevor die Firewall steht. fail2ban benötigt keine Konfiguration, um hier nützlich zu sein; die Ubuntu-Standardeinstellungen überwachen sshd ab Werk. Was die Jails tatsächlich bewirken und wie man sie optimiert, wird im fail2ban Guide für Ubuntu 24.04 behandelt.

Schritt 6: Testlauf mit --check, dann tatsächliche Ausführung

ansible-playbook site.yml --check

Der Check-Modus stellt eine Verbindung her, berechnet die geplanten Änderungen und führt keine Modifikationen aus. Lesen Sie die changed=-Anzahl in der PLAY RECAP am Ende – dies ist die Anzahl der Aufgaben, die jeden Host verändern würden. Ein wichtiger Hinweis: Der Check-Modus hat eine strukturelle Einschränkung, wenn eine spätere Aufgabe von Änderungen einer vorherigen Aufgabe abhängt. Das Standard-Server-Image von Ubuntu enthält ufw vorinstalliert, daher läuft dieses Playbook im Testlauf fehlerfrei durch. Auf einem minimalen Image ohne ufw schlagen die ufw-Aufgaben im Check-Modus fehl, da der Check-Modus das Paket nicht tatsächlich installiert hat und das Modul somit keinen Aufruf ausführen kann. Dies ist eine Einschränkung von Testläufen und kein Fehler im Playbook. Wenn der Plan korrekt aussieht:

ansible-playbook site.yml

Jede Aufgabe gibt eine Zeile pro Host aus – gelb changed, grün ok – und die Zusammenfassung sollte wie folgt aussehen:

PLAY RECAP *********************************************************************
web1 : ok=10  changed=9  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
web2 : ok=10  changed=9  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

Zehn ok bestehen aus der Datenerfassung, acht Aufgaben und dem Handler. Ihr changed-Wert kann sich von meinem um eins oder zwei unterscheiden: Das Standard-Image von Ubuntu enthält ufw und unattended-upgrades vorinstalliert, und fail2ban startet automatisch, sobald apt es installiert. Daher kann eine Aufgabe beim ersten Durchlauf legitim ok melden – der Zustand wird als bereits erreicht gemeldet. Die Werte für unreachable und failed müssen Null sein. Ein Hinweis zu become: true: Dies ist eine Formalität, während Sie als root verbunden sind. Sobald Sie ansible_user auf deploy setzen, ist sudo aktiv – und die durch dieses Playbook installierte NOPASSWD-sudoers-Datei verhindert, dass -K in der Befehlszeile erscheint. Ohne diese Konfiguration erhalten Sie Missing sudo password, wie unten beschrieben.

Schritt 7: Zweimal ausführen — Idempotenz in der Praxis

Führen Sie denselben Befehl sofort erneut aus:

web1 : ok=9  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=0 und ok verringern sich um eins, da der Handler ohne Benachrichtigung nie ausgeführt wurde. Es wurde nichts neu installiert, sshd wurde nicht neu gestartet und ufw wurde nicht verändert. Dies macht das Playbook ebenso zu einem Audit-Tool wie zu einem Provisioning-Tool: Fügen Sie nächsten Monat web3 zum Inventory hinzu und führen Sie es erneut aus — der neue Server wird erstellt, die alten Server werden verifiziert. Ein Wert ungleich Null bei changed auf einem Server, den Sie nicht bearbeitet haben, stellt Drift dar. Dies zeigt an, dass jemand manuell Änderungen an Elementen vorgenommen hat, die über das Playbook geändert werden sollten.

Ab diesem Punkt verstärkt sich das Muster. Das nächste nützliche Playbook installiert ein WireGuard VPN auf demselben VPS und verschärft die ufw-Regel, sodass SSH nur über den Tunnel antwortet; danach folgt eines, das Docker und Compose auf jedem App-Server installiert. Wenn site.yml mehr als drei Bildschirme füllt, teilen Sie es in Roles auf — aber nicht vorher.

Fehlerzustände und die angezeigten Meldungen

UNREACHABLE with Permission denied.

web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: root@10.0.0.10: Permission denied (publickey).",
    "unreachable": true
}

Der SSH-Transport fehlgeschlagen, bevor ein Modul ausgeführt wurde: ansible_user ist falsch, der Key wurde nie auf den Host kopiert oder es wird ein falscher Key angeboten. Reproduzieren Sie dies mit einfachem ssh root@10.0.0.10 und anschließend ssh -v, um die angebotenen Keys zu sehen. Wenn SSH mit Passwort funktioniert, aber Ansible nicht, wurde ssh-copy-id übersprungen.

Missing sudo password.

web1 | FAILED! => {
    "msg": "Missing sudo password"
}

Sie haben become: true gesetzt und verbinden sich als Nicht-Root-User, der ein Passwort für sudo benötigt. Fügen Sie entweder -K (--ask-become-pass) zur Befehlszeile hinzu oder erstellen Sie einen NOPASSWD sudoers-Eintrag für den User. Genau deshalb installiert das Playbook einen Eintrag für deploy, bevor Sie zu diesem User wechseln.

error: externally-managed-environment. Sie haben pip gegen das System-Python auf Ubuntu 24.04 ausgeführt. Erklärt in Schritt 1: nutzen Sie pipx statt pip und nicht --break-system-packages.

mapping values are not allowed in this context.

ERROR! Syntax Error while loading YAML.
  mapping values are not allowed in this context

Fast immer liegt es an der Einrückung: Ein Key befindet sich auf der falschen Ebene oder es fehlt ein Leerzeichen nach einem Doppelpunkt. Die angegebene Zeilennummer liegt nahe am Fehler, nicht direkt darauf — prüfen Sie auch die Zeile darüber. Der verwandte Fehler found character '\t' that cannot start any token bedeutet, dass ein Tab verwendet wurde; YAML verbietet Tabs. Machen Sie ansible-playbook site.yml --syntax-check zur Routine vor jedem Durchlauf und stellen Sie Ihren Editor auf zwei Leerzeichen Einrückung für YAML ein.

/usr/bin/python3: not found. Selten bei Standard-Ubuntu 24.04 Images, häufig bei minimalen oder Netboot-Images: Die Modulausführung schlägt fehl, weil das Zielsystem kein Python besitzt. Installieren Sie Python mit dem raw Modul; dies ist das einzige Modul, das auf der Zielseite keine Voraussetzungen benötigt: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, und führen Sie das Playbook dann erneut aus.

FAQ

Muss ich Ansible auf den verwalteten Servern installieren?

Nein. Ansible ist agentenlos: Die Control-Machine überträgt kleine Python-Module über SSH, führt diese aus und entfernt sie anschließend. Ein Zielsystem benötigt lediglich python3 und SSH-Zugriff; beides ist in Standard-Ubuntu-Images bereits enthalten. Die einzige Installation in diesem gesamten Leitfaden erfolgt auf Ihrer Control-Machine.

Warum gibt Ansible "Permission denied (publickey)" aus?

Der UNREACHABLE!-Block mit Permission denied (publickey) bedeutet, dass die SSH-Authentifizierung fehlgeschlagen ist, bevor Ansible eine Aktion ausgeführt hat. Prüfen Sie, ob ansible_user in der Inventory mit dem tatsächlich eingerichteten Benutzer übereinstimmt, ob Sie ssh-copy-id an diesen Host ausgeführt haben und ob ein normaler ssh user@host ohne Passwort funktioniert. Jede Lösung für den Standard-ssh-Befehl behebt auch das Problem in Ansible, da beide denselben Transportmechanismus nutzen.

Was bedeutet Idempotenz in Ansible?

Eine Task definiert einen gewünschten Zustand – zum Beispiel „dieses Paket ist installiert“ oder „diese Zeile befindet sich in dieser Datei“ – anstatt eine Aktion auszuführen. Wenn der Zustand bereits erreicht ist, unternimmt Ansible nichts und meldet ok anstatt changed. Deshalb zeigt die Ausführung eines Playbooks beim zweiten Mal changed=0 an und warum ein erneuter Durchlauf eine sichere Überprüfung statt einer riskanten Neuinstallation ist.

Sollte ich pip oder pipx verwenden, um Ansible auf Ubuntu 24.04 zu installieren?

pipx. Ubuntu 24.04 markiert das System-Python als extern verwaltet, weshalb pip install ansible konstruktionsbedingt mit error: externally-managed-environment fehlschlägt. pipx install --include-deps ansible installiert Ansible in einer isolierten virtualenv und stellt ansible, ansible-playbook und den Rest sauber in Ihrem PATH bereit.

Was ist der Unterschied zwischen den Paketen ansible und ansible-core?

ansible-core enthält die Engine sowie nur die ansible.builtin-Module. Das Paket ansible bündelt den Core mit kuratierten Community-Collections – einschließlich ansible.posix (das authorized_key-Modul) und community.general (das ufw-Modul), die beide in diesem Leitfaden verwendet werden. Beginnen Sie mit dem vollständigen Paket; reduzieren Sie auf Core plus handverlesene Collections nur dann, wenn es eine Notwendigkeit gibt.