SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-07

Ansible playbook maken voor Ubuntu 24.04 VPS

Installeer Ansible met pipx op Ubuntu 24.04 en schrijf uw eerste playbook voor serverbeveiliging. Los direct Permission denied en sudo foutmeldingen op voor een vliegende start.

Wat u gaat bouwen

Eén controle-machine met Ansible geïnstalleerd en een of meer nieuwe Ubuntu 24.04 VPS-servers waarop alleen de standaard image staat. Aan het einde beschikt u over een inventory-bestand waarin uw servers staan benoemd, een ad-hoc ping die bewijst dat de authenticatie volledig werkt, en een playbook dat de volledige checklist voor nieuwe VPS-servers als code uitvoert: een deploy-gebruiker met uw SSH-sleutel, een beveiligde sshd, fail2ban, unattended-upgrades en een firewall die OpenSSH toestaat voordat al het overige verkeer wordt geweigerd. Richt het op één server of op twintig. Voer het twee keer uit en de tweede keer verandert er niets; dat is precies het doel.

Na vijftien jaar VPS-provisioning kan ik u het eerlijke patroon vertellen: iedereen zet de eerste vijf servers handmatig op, om vervolgens in het weekend bij de zesde server tijd te verliezen omdat niemand meer weet wat er bij de eerste vijf is gedaan. Deze handleiding verdiept het overzicht in het beheren van meerdere Linux-servers; begin hiermee op de dag dat u merkt dat u hetzelfde apt install in drie terminals tegelijk typt.

Wat Ansible in essentie is, in één alinea

Ansible is agentless. Er hoeft geen daemon te worden geïnstalleerd op de servers die worden beheerd: de control machine maakt verbinding via standaard SSH, kopieert een kleine Python-module naar het doel, voert deze uit, leest de JSON die de module genereert en verwijdert deze vervolgens weer. Het enige wat een doelserver nodig heeft is python3, wat standaard aanwezig is in elke Ubuntu-image. Het belangrijkste begrip hierbij is idempotent, wat simpelweg betekent: een taak beschrijft een status, geen actie. state: present voor een pakket betekent "zorg dat dit is geïnstalleerd", niet "voer het installatieprogramma uit". Als de status al overeenkomt, wijzigt Ansible niets en rapporteert dit als ok in plaats van changed. Die eigenschap vormt het hele product; het is wat het opnieuw uitvoeren van een playbook veilig maakt, en veilige herhalingen zijn wat een shell-script transformeert tot infrastructuur.

Vereisten en aandachtspunten vooraf

  • Een controle-machine: uw laptop of een kleine VPS. Ik ga uit van Ubuntu 24.04; macOS werkt identiek zodra pipx is geïnstalleerd via Homebrew.
  • Eén of meer doel-VPS'en met Ubuntu 24.04 op KVM, bereikbaar als root. Er wordt niets op deze systemen geïnstalleerd.
  • SSH-key-authenticatie naar elk doel. Ansible heeft exact dezelfde rechten als uw ssh-commando; als ssh root@host om een wachtwoord vraagt, zal Ansible falen.
  • Op Ubuntu 24.04 faalt pip install ansible met error: externally-managed-environment. Dit is een bewust distributiebeleid, geen defect. Gebruik pipx.
  • YAML-witruimte is syntax. Een verkeerde inspringing resulteert in mapping values are not allowed in this context en een tab-karakter op welke plek dan ook is fataal.
  • Houd een actieve SSH-sessie open op elk doel terwijl de playbook sshd verhardt. Elke buitensluiting waarbij ik een klant heb geholpen, ontstond door het sluiten van de laatste sessie "om vanaf een schone lei te testen".

Stap 1: installeer Ansible op de control machine met pipx, niet met pip

De klassieke reflex is pip3 install ansible. Op een volledig schone 24.04-image loopt dit proces voortijdig vast, Command 'pip3' not found, but can be installed with: sudo apt install python3-pip, en het installeren van pip leidt enkel tot het werkelijke probleem:

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 markeert de systeem-Python als extern beheerd (PEP 668), waardoor pip niet met apt kan concurreren om dezelfde bestanden. Gebruik niet --break-system-packages; de naam van deze flag is een waarschuwing. De juiste oplossing is pipx, dat Ansible een eigen geïsoleerde virtualenv geeft en de binaries in uw PATH plaatst:

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

Open een nieuwe shell na pipx ensurepath zodat de wijziging in het PATH wordt doorgevoerd. --include-deps is geen overbodige toevoeging: het ansible-pakket bevat zelf geen console-scripts, ansible, ansible-playbook en de rest zijn entry points van de ansible-core-dependency. Zonder deze flag weigert pipx de installatie met No apps associated with package ansible or its dependencies. Installeer bovendien het ansible-pakket en niet de kale ansible-core; het volledige pakket bevat de community-collecties, en dit playbook gebruikt modules uit twee daarvan (ansible.posix en community.general).

ansible --version

Het juiste resultaat begint met een regel zoals ansible [core 2.19.x] en vermeldt de Python-versie waaronder het draait; elke recente core-release volstaat voor deze handleiding. ansible: command not found betekent daarentegen dat ~/.local/bin nog niet in uw PATH staat, dat u een nieuwe shell moet openen, of dat source ~/.bashrc is opgetreden.

Dit is de volledige installatie. De targets hebben niets nodig.

Stap 2: SSH-sleuteltoegang tot elke doelserver

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

Bevestig dit vervolgens, één keer per host:

ssh root@10.0.0.10 true && echo ok

Deze regel voert twee taken uit: het bevestigt dat authenticatie via sleutels zonder wachtwoord werkt en het registreert de host-sleutel in known_hosts. Voer dit nu uit, omdat Ansible een niet-geregistreerde host-sleutel presenteert als een interactieve prompt midden in een proces, wat eruitziet alsof het systeem is vastgelopen.

Stap 3: de inventory, eerst INI, daarna YAML bij groei

De inventory is een tekstbestand met een lijst van machines die Ansible mag benaderen. Maak inventory.ini aan in een nieuwe projectmap:

[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20

[vps:vars]
ansible_user=root

web1 is een alias die u kiest; dit is de naam die in de uitvoer verschijnt en die u target met --limit web1. ansible_host is het werkelijke adres. [vps] is een groep en [vps:vars] stelt variabelen in voor elke host in die groep; ansible_user is de gebruiker waarmee Ansible inlogt. Voeg daarnaast een ansible.cfg toe zodat u nooit meer -i hoeft te typen:

[defaults]
inventory = inventory.ini

Ansible leest ansible.cfg vanuit de huidige map. Dezelfde inventory in YAML, opgeslagen als inventory.yml en aangeroepen met ansible.cfg, heeft de voorkeur zodra hosts elk meerdere variabelen bevatten:

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

Ze zijn equivalent. INI is overzichtelijker bij twee servers; YAML schaalt beter bij twintig. Kies er een en besteed er verder geen aandacht meer aan.

Stap 4: ad-hoc commando's, de groene pong die alles bevestigt

ansible all -m ping

Dit is geen ICMP. De ping-module is een volledige generale repetitie: SSH-login, kopiëren van de module, Python-uitvoering op de doelserver en opschoning. Het juiste resultaat is groen, één blok per host:

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

Een groene SUCCESS betekent dat de authenticatie, de Python-interpreter en het transport allemaal werken; het playbook zal dan ook werken. Een rode UNREACHABLE! betekent dat het transport faalde voordat er een module werd uitgevoerd; de exacte foutmelding en de oplossing staan in de sectie over foutmodi hieronder. Twee andere ad-hoc commando's die nuttig zijn om te kennen:

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

Ad-hoc is bedoeld voor eenmalige acties en controles. Alles wat u vaker dan één keer zou uitvoeren, hoort thuis in een playbook.

Stap 5: het eerste playbook, de checklist voor een nieuwe VPS als code

Dit is alles wat u handmatig zou doen in de eerste tien minuten op een nieuwe server. Sla dit op 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

De regels die het waard zijn om te begrijpen in plaats van ze alleen te kopiëren:

Variabelen staan onder vars: en worden aangeroepen met "{{ deploy_user }}". Zet de gehele expressie tussen aanhalingstekens wanneer een waarde begint met een accolade, anders leest de YAML-parser deze verkeerd. De lookup('file', ...) leest uw publieke sleutel tijdens runtime van de control-machine, zodat het playbook geen sleutelmateriaal bevat.

De loop. loop: "{{ baseline_services }}" voert de service-taak één keer per item uit en de uitvoer toont elk item op een eigen regel. Merk op dat de apt-taak de volledige pakketlijst in één keer verwerkt; één apt-transactie is sneller en is het geprefereerde patroon voor pakketten. Loops zijn bedoeld voor modules die daadwerkelijk één ding tegelijk verwerken.

De handler is het concept dat u zich eigen moet maken. notify: Restart ssh betekent niet "herstart ssh nu". Het plaatst de handler in de wachtrij, die aan het einde van de play één keer wordt uitgevoerd, en alleen als de taak die de melding verstuurt daadwerkelijk changed rapporteert. Voer het playbook morgen opnieuw uit: het drop-in bestand is al correct, de copy-taak rapporteert ok, en sshd wordt niet opnieuw opgestart. De regel validate: is de beveiliging op de trigger; sshd controleert het bestand voordat het oude wordt vervangen, waardoor een typefout de taak laat falen in plaats van de daemon te verbreken.

PermitRootLogin prohibit-password, niet no, bewust. Dit playbook logt in als root met een sleutel. prohibit-password schakelt root-inloggen met wachtwoord uit terwijl uw eigen toegang behouden blijft. Zodra de deploy-gebruiker is geverifieerd (ssh deploy@10.0.0.10 sudo true, het kale adres, aangezien web1 alleen een alias is die Ansible kent), wijzigt u ansible_user=deploy in de inventory en scherpt u dit aan naar no in een latere run. Verhard de server in een volgorde waardoor u uzelf niet buitensluit.

Het voorvoegsel 00- is van belang. Voor de meeste trefwoorden respecteert sshd de eerste instantie die het parseert, en Ubuntu's sshd_config bevat sshd_config.d/*.conf in lexicale volgorde vóór de eigen inhoud. Ubuntu 24.04 cloud-images bevatten al een 60-cloudimg-settings.conf in die map, en providers die inloggen met wachtwoord via cloud-init inschakelen, voegen een 50-cloud-init.conf toe met PasswordAuthentication yes; door de onze 00-hardening.conf te noemen, sorteert deze als eerste en wint hij het van beide.

De volgorde van taken is de veiligheid van de firewall. Allow OpenSSH wordt uitgevoerd vóór Enable ufw met een deny-beleid. Ansible voert taken strikt uit in de vermelde volgorde, dus het gat bestaat voordat de muur wordt opgetrokken. fail2ban heeft geen configuratie nodig om hier nuttig te zijn; de standaardinstellingen op Ubuntu monitoren sshd direct uit de doos. Wat de jails daadwerkelijk doen en hoe u deze afstelt, wordt behandeld in de handleiding voor fail2ban op Ubuntu 24.04.

Stap 6: dry run met --check, daarna definitief uitvoeren

ansible-playbook site.yml --check

De check-modus maakt verbinding, berekent wat de acties zouden zijn en wijzigt niets. Lees het changed=-aantal in de PLAY RECAP onderaan; dit is het aantal taken dat per host wijzigingen zou doorvoeren. Een belangrijke kanttekening: de check-modus heeft een structurele beperking wanneer een latere taak afhankelijk is van de wijzigingen van een eerdere taak. De standaard server-image van Ubuntu bevat ufw, waardoor dit playbook in de dry run foutloos verloopt. Op een minimale image zonder ufw zullen de ufw-taken echter falen in de check-modus, omdat de module in de check-modus het pakket niet daadwerkelijk installeert en de module vervolgens niets heeft om aan te roepen. Dit is een beperking van dry runs, geen bug in uw playbook. Wanneer het plan er correct uitziet:

ansible-playbook site.yml

Elke taak print een regel per host, geel voor changed, groen voor ok, en de samenvatting moet als volgt zijn:

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

Tien ok staat voor het verzamelen van feiten plus acht taken plus de handler. Uw changed mag één of twee afwijken van de mijne: de standaard Ubuntu-image bevat al ufw en unattended-upgrades, en fail2ban start direct zodra apt het installeert. Een taak kan daarom legitiem ok rapporteren bij de eerste uitvoering, omdat de gedeclareerde status al aanwezig was. De getallen die nul moeten zijn, zijn unreachable en failed. Een opmerking over become: true: dit is een formaliteit zolang u verbinding maakt als root, maar zodra u ansible_user op deploy zet, is sudo actief. Het NOPASSWD sudoers-bestand dat dit playbook installeert, is precies wat -K van uw opdrachtregel houdt. Zonder dit bestand krijgt u Missing sudo password, wat hieronder wordt behandeld.

Stap 7: voer het twee keer uit, zo ziet idempotentie eruit

Voer het commando direct nogmaals uit:

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

changed=0, en ok zijn met één verlaagd omdat de niet-geïnformeerde handler nooit is uitgevoerd. Er is niets opnieuw geïnstalleerd, sshd is niet herstart en ufw is niet aangeraakt. Dit is wat de playbook zowel een audit als een provisioner maakt: voeg volgende maand web3 toe aan de inventory en voer het opnieuw uit; de nieuwe server wordt gebouwd en de oude servers worden geverifieerd. Een changed die niet nul is op een server die u niet heeft aangeraakt, duidt op drift. Dit geeft aan dat iemand handmatig wijzigingen heeft aangebracht die in de playbook hadden moeten staan.

Vanaf hier breidt het patroon zich uit. De volgende playbook die de moeite waard is om te schrijven, installeert een WireGuard VPN op dezelfde VPS en scherpt de ufw-regel aan zodat SSH alleen via de tunnel reageert; daarna volgt een playbook die Docker en Compose op elke app-server installeert. Wanneer site.yml langer wordt dan drie schermen, splitst u deze op in rollen, maar niet eerder.

Foutmodi en de bijbehorende meldingen

UNREACHABLE met 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
}

Het SSH-transport faalde voordat een module kon worden uitgevoerd: ansible_user is onjuist, de sleutel is nooit naar die host gekopieerd, of de verkeerde sleutel wordt aangeboden. Reproduceer dit met een standaard ssh root@10.0.0.10 en gebruik daarna ssh -v om te zien welke sleutels werden aangeboden. Als SSH met een wachtwoord wel werkt maar Ansible niet, dan heeft u ssh-copy-id overgeslagen.

Ontbrekend sudo-wachtwoord.

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

U heeft become: true ingesteld en bent verbonden als een niet-rootgebruiker, en die gebruiker heeft een wachtwoord nodig voor sudo. Voeg ofwel -K (--ask-become-pass) toe aan de opdrachtregel, of geef de gebruiker een NOPASSWD-vermelding in sudoers. Dit is precies de reden waarom het playbook er een installeert voor deploy voordat u er ooit naar overschakelt.

error: externally-managed-environment. U heeft pip uitgevoerd op de systeem-Python van Ubuntu 24.04. Dit is behandeld in stap 1: gebruik pipx, niet pip, en zeker niet --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

Dit is bijna altijd een inspringfout: een sleutel op het verkeerde niveau, of een ontbrekende spatie na een dubbele punt. Het gerapporteerde regelnummer wijst naar de omgeving van de fout, niet noodzakelijkerwijs naar de fout zelf; controleer ook de regel erboven. De verwante fout found character '\t' that cannot start any token betekent dat er een tabteken is ingeslopen; YAML staat deze niet toe. Maak ansible-playbook site.yml --syntax-check een automatisme voor elke uitvoering en stel uw editor in op twee spaties inspringing voor YAML.

/usr/bin/python3: not found. Zeldzaam op standaard Ubuntu 24.04-images, maar gebruikelijk op minimale of netboot-images: de uitvoering van de module faalt omdat de doelmachine geen Python heeft. Bootstrap de machine met de raw-module, de enige module die niets vereist aan de ontvangende kant: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, en voer daarna het playbook opnieuw uit.

FAQ

Moet ik Ansible installeren op de servers die ik beheer?

Nee. Ansible is agentless: de control machine verstuurt kleine Python-modules via SSH, voert deze uit en verwijdert ze weer. Een doelserver heeft enkel python3 en SSH-toegang nodig; beide zijn standaard aanwezig op Ubuntu-images. De enige installatie in deze gehele handleiding vindt plaats op uw control machine.

Waarom geeft Ansible de melding "Permission denied (publickey)"?

Het UNREACHABLE!-blok met Permission denied (publickey) betekent dat de SSH-authenticatie mislukte voordat Ansible iets kon uitvoeren. Controleer of de ansible_user in de inventory overeenkomt met het account dat u daadwerkelijk heeft ingesteld, of u ssh-copy-id naar die host heeft uitgevoerd en of een standaard ssh user@host inlogt zonder wachtwoord. Alles wat het standaard ssh-commando herstelt, lost ook het probleem in Ansible op, aangezien zij hetzelfde transport gebruiken.

Wat betekent idempotent in Ansible?

Een taak declareert een gewenste status, zoals "dit pakket is aanwezig" of "deze regel staat in dit bestand", in plaats van een uit te voeren actie. Als de status al overeenkomt, doet Ansible niets en rapporteert ok in plaats van changed. Daarom toont het tweemaal uitvoeren van een playbook de tweede keer changed=0, en daarom is een herhaling een veilige controle in plaats van een risicovolle herinstallatie.

Moet ik pip of pipx gebruiken om Ansible te installeren op Ubuntu 24.04?

pipx. Ubuntu 24.04 markeert de systeem-Python als extern beheerd, waardoor pip install ansible standaard faalt met error: externally-managed-environment. pipx install --include-deps ansible plaatst Ansible in een geïsoleerde virtualenv en stelt ansible, ansible-playbook en de rest netjes beschikbaar op uw PATH.

Wat is het verschil tussen de pakketten ansible en ansible-core?

ansible-core is de engine plus enkel de ansible.builtin-modules. Het ansible-pakket bundelt core met de samengestelde community-collecties, inclusief ansible.posix (de authorized_key-module) en community.general (de ufw-module), die beide in deze handleiding worden gebruikt. Begin met het volledige pakket; stap pas over naar core met handmatig gekozen collecties wanneer u daar een specifieke reden voor heeft.