Ansible tutorial: eerste playbook op VPS
Leer Ansible installeren via pipx op Ubuntu 24.04. Los Permission denied en sudo errors op bij het harden van uw VPS met een eerste inventory en playbook.
Wat u bouwt
Eén control machine met geïnstalleerde Ansible, en één of meerdere nieuwe Ubuntu 24.04 VPSes met enkel de standaard image. Aan het einde heeft u een inventory file waarin uw servers zijn benoemd, een ad-hoc ping die bewijst dat de authenticatie overal werkt, en een playbook dat de volledige checklist voor nieuwe VPSes als code uitvoert: een deploy user met uw SSH key, hardened sshd, fail2ban, unattended upgrades, en een firewall die OpenSSH toestaat voordat alles wordt geblokkeerd. Gebruik dit voor één server of twintig. Voer het twee keer uit en de tweede keer verandert er niets — dat is het hoofddoel.
Na vijftien jaar het provisionen van VPSes kan ik u het eerlijke patroon vertellen: iedereen stelt de eerste vijf servers handmatig in, en verliest daarna een weekend aan de zesde server omdat niemand meer weet wat er bij de eerste vijf is gedaan. Deze gids is een verdieping van het beheren van meerdere Linux servers — begin hiermee op de dag dat u merkt dat u dezelfde apt install in drie verschillende terminals typt.
Wat Ansible precies is, in één paragraaf
Ansible is agentless. Er hoeft geen daemon te worden geïnstalleerd op de beheerde servers: de control machine maakt verbinding via standaard SSH, kopieert een kleine Python-module naar het doelwit, voert deze uit, leest de JSON-output en verwijdert de module vervolgens. Het doelwit heeft alleen python3 nodig, wat standaard aanwezig is in elke Ubuntu-image. Het cruciale concept is idempotent. Dit betekent simpelweg dat een taak een toestand beschrijft en geen handeling. state: present voor een pakket betekent "zorg dat dit geïnstalleerd is" en niet "voer de installer uit". Als de gewenste toestand al aanwezig is, verandert Ansible niets en wordt dit gerapporteerd als ok in plaats van changed. Deze eigenschap is de kern van het product; het zorgt ervoor dat het opnieuw uitvoeren van een playbook veilig is. Veilige herhalingen maken van een shell script infrastructuur.
Vereisten en belangrijke aandachtspunten
- Een beheermachine: uw laptop of een kleine VPS. Ik ga uit van Ubuntu 24.04; macOS werkt identiek zodra pipx via Homebrew is geïnstalleerd.
- Eén of meerdere doel-VPSen met Ubuntu 24.04 op KVM, bereikbaar als root. Er wordt niets op deze machines geïnstalleerd.
- SSH key authenticatie voor elke doelmachine. Ansible gebruikt dezelfde authenticatie als uw
sshcommando — alsssh root@hostom een wachtwoord vraagt, faalt Ansible. - Op Ubuntu 24.04 crasht
pip install ansiblemeterror: externally-managed-environment. Dit is een bewuste beleidswijziging van de distributie en geen fout. Gebruik pipx. - YAML-whitespace is syntax. Een foutieve inspringing veroorzaakt
mapping values are not allowed in this context, en een tab-teken op een willekeurige plek is fataal. - Houd een werkende SSH-sessie open op elke doelmachine terwijl het playbook sshd beveiligt. Elke lockout waarbij ik een klant heb kunnen helpen, ontstond doordat de laatste sessie werd gesloten "om vanaf een schone sessie te testen".
Stap 1: installeer Ansible op de control machine met pipx, niet met pip
De standaardmethode is pip3 install ansible. Op een volledig nieuwe 24.04 image mislukt de installatie al in een vroeg stadium — Command 'pip3' not found, but can be installed with: sudo apt install python3-pip — en het installeren van pip leidt enkel tot de volgende fout:
pip3 install ansibleerror: 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 het systeem-Python als extern beheerd (PEP 668), waardoor pip niet kan concurreren met apt voor dezelfde bestanden. Gebruik geen --break-system-packages; de flag is eerlijk benoemd. De juiste oplossing is pipx. Dit creëert een geïsoleerde virtualenv voor Ansible en plaatst de binaries in uw PATH:
sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansibleOpen een nieuwe shell na pipx ensurepath zodat de wijziging in de PATH wordt toegepast. --include-deps is geen versiering: het ansible pakket bevat zelf geen console scripts — ansible, ansible-playbook en de rest zijn entry points van de ansible-core dependency — zonder de flag weigert pipx de installatie met No apps associated with package ansible or its dependencies. Installeer het ansible pakket, niet het kale ansible-core — het volledige pakket bevat de community collections, en dit playbook gebruikt modules uit twee hiervan (ansible.posix en community.general).
ansible --versionHet correcte resultaat begint met een regel zoals ansible [core 2.19.x] en vermeldt de Python-versie die wordt gebruikt; elke huidige core release is geschikt voor deze stappen. ansible: command not found betekent dat ~/.local/bin nog niet in uw PATH staat — gebruik een nieuwe shell of source ~/.bashrc.
Dit is de volledige installatie. De targets ontvangen niets.
Stap 2: SSH-sleutel toegang tot elke target
ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20Bewijs dit vervolgens, eenmaal per host:
ssh root@10.0.0.10 true && echo okDeze ene regel heeft twee functies: het bevestigt dat sleutel-authenticatie zonder wachtwoord werkt, en het registreert de host-sleutel in known_hosts. Voer dit nu uit. Ansible geeft namelijk een interactieve prompt weer als een host-sleutel niet is geregistreerd. Dit gebeurt midden in een actieve run en lijkt hierdoor precies op een vastgelopen proces.
Stap 3: de inventory — INI eerst, YAML wanneer deze groeit
De inventory is een tekstbestand met een lijst van machines die Ansible kan beheren. 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=rootweb1 is een door u gekozen alias — dit is wat in de output verschijnt en wat 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. Daarnaast staat een ansible.cfg zodat u -i nooit meer handmatig hoeft in te voeren:
[defaults]
inventory = inventory.iniAnsible leest ansible.cfg vanuit de huidige directory. Dezelfde inventory in YAML-formaat — sla deze op als inventory.yml en wijs ansible.cfg naar die naam in plaats daarvan — is wat u zult verkiezen zodra hosts meerdere variabelen bevatten:
vps:
hosts:
web1:
ansible_host: 10.0.0.10
web2:
ansible_host: 10.0.0.20
vars:
ansible_user: rootBeide formaten zijn gelijkwaardig. INI is makkelijker te controleren bij twee servers; YAML schaalt beter bij twintig servers. Kies er één en stop met erover nadenken.
Stap 4: ad-hoc commando's — de groene pong die alles bewijst
ansible all -m pingDit is geen ICMP. De ping module is een volledige oefening: SSH-login, modulekopie, Python-uitvoering op het doelwit en opschoning. Het juiste resultaat is groen, met één blok per host:
web1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}Groen SUCCESS betekent dat de authenticatie, de Python-interpreter en het transport werken — het playbook zal dit ook doen. Rood UNREACHABLE! betekent dat het transport is mislukt voordat een module is uitgevoerd; de exacte foutmelding en de oplossing staan in de sectie met foutmodi hieronder. Twee andere ad-hoc commando's die belangrijk zijn:
ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --becomeAd-hoc is bedoeld voor eenmalige acties en controles. Alles wat u twee keer zou uitvoeren, hoort in een playbook.
Stap 5: het eerste playbook — de new-VPS checklist als code
Dit bevat alle handelingen die u in de eerste tien minuten op een nieuwe server handmatig zou uitvoeren. 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: restartedDe regels die u moet begrijpen in plaats van kopiëren:
Variables staan onder vars: en worden gerefereerd met "{{ deploy_user }}" — gebruik aanhalingstekens voor de volledige expressie als een waarde met een accolade begint, anders leest de YAML-parser dit foutief. De lookup('file', ...) leest uw publieke sleutel van de control machine tijdens runtime, waardoor het playbook geen sleutelmateriaal bevat.
De loop. loop: "{{ baseline_services }}" voert de service-taak één keer uit per item, en de output toont elk item op een eigen regel. Let op dat de apt-taak de volledige pakketlijst in één keer verwerkt — één apt-transactie is sneller en is het voorkeurspatroon voor pakketten; loops zijn bedoeld voor modules die daadwerkelijk één ding tegelijk verwerken.
De handler is het concept dat u moet begrijpen. notify: Restart ssh betekent niet "start ssh nu opnieuw op". Het plaatst de handler in een wachtrij, die één keer aan het einde van het play wordt uitgevoerd, en alleen als de notifier-taak 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 herstart. De validate: regel is de veiligheid op de trekker — sshd controleert het bestand voordat het de oude vervangt, zodat een typefout de taak laat falen in plaats van de daemon te breken.
PermitRootLogin prohibit-password, niet no — bewust. Dit playbook logt in als root met een sleutel. prohibit-password schakelt wachtwoord-logins voor root uit, terwijl uw eigen toegang behouden blijft. Zodra de deploy-user is geverifieerd (ssh deploy@10.0.0.10 sudo true — het gewone adres, aangezien web1 alleen een alias is die Ansible kent), wijz de ansible_user=deploy in de inventory en verscherp dit naar no in een latere run. Verhard het systeem in een volgorde die u niet buitensluit.
Het 00- prefix is belangrijk. Voor de meeste keywords respecteert sshd de eerste voorkomende instantie die wordt geparsed, en de Ubuntu sshd_config bevat sshd_config.d/*.conf in lexicale volgorde vóór de eigen body. Ubuntu 24.04 cloud images bevatten al een 60-cloudimg-settings.conf in die directory, en providers die wachtwoord-logins inschakelen via cloud-init voegen een 50-cloud-init.conf met PasswordAuthentication yes toe; door de onze 00-hardening.conf te noemen, wordt deze als eerste gesorteerd en krijgt deze voorrang op 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 volgorde waarin ze staan, dus de opening bestaat voordat de muur staat. fail2ban heeft geen configuratie nodig om hier nuttig te zijn; de Ubuntu-standaarden bewaken sshd direct uit de doos, en wat de jails daadwerkelijk doen — en wat u moet tunen — staat beschreven in de fail2ban op Ubuntu 24.04 gids.
Stap 6: dry run met --check, voer daarna de echte uitvoering uit
ansible-playbook site.yml --checkDe check mode maakt verbinding, berekent wat er zou gebeuren en verandert niets. Bekijk de changed= count in de PLAY RECAP onderaan — dit is het aantal taken dat elke host zou wijzigen. Een belangrijke kanttekening: check mode heeft een structurele beperking wanneer een latere taak afhankelijk is van wijzigingen uit een eerdere taak. De standaard Ubuntu server image bevat ufw al vooraf, dus deze playbook voert de dry-run zonder fouten uit. Op een minimale image zonder ufw zullen de ufw taken falen in check mode. Dit komt omdat check mode het pakket nooit daadwerkelijk heeft geïnstalleerd en het module vervolgens niets heeft om aan te roepen. Dit is een beperking van dry runs, geen fout in uw playbook. Wanneer het plan correct lijkt:
ansible-playbook site.ymlElke taak print een regel per host — geel changed, groen 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=0Tien ok bestaat uit fact-gathering plus acht taken plus de handler. Uw changed mag één of twee afwijken van de mijne: de standaard Ubuntu image bevat ufw en unattended-upgrades al vooraf, en fail2ban start direct zodra apt het installeert. Hierdoor kan een taak bij de allereerste uitvoering legitiem ok rapporteren — de staat die het al heeft. De getallen die nul moeten zijn, zijn unreachable en failed. Een opmerking over become: true: dit is een formaliteit terwijl u als root verbinding maakt, maar zodra u ansible_user naar deploy wijzigt, is sudo actief. Het NOPASSWD sudoers bestand dat deze playbook installeert, zorgt ervoor dat -K niet in uw command line verschijnt. Zonder dit krijgt u Missing sudo password, zoals hieronder beschreven.
Stap 7: voer het twee keer uit — wat idempotentie inhoudt
Voer het commando direct opnieuw uit:
web1 : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 en ok zijn met één afgenomen omdat de handler zonder melding niet is uitgevoerd. Er is niets opnieuw geïnstalleerd, sshd is niet herstart en ufw is niet aangepast. Dit maakt de playbook evenzeer een audit als een provisioner: voeg volgende maand web3 toe aan de inventory en voer het opnieuw uit — de nieuwe machine wordt opgebouwd, de bestaande machines worden gecontroleerd. Een waarde van niet-nul voor changed op een machine die u niet heeft aangepast, duidt op drift. Dit geeft aan dat iemand handmatig wijzigingen heeft aangebracht die via de playbook hadden moeten gebeuren.
Vanaf dit punt wordt het patroon krachtiger. De volgende nuttige playbook installeert een WireGuard VPN op dezelfde VPS en verscherpt de ufw-regel zodat SSH alleen via de tunnel reageert; daarna volgt een playbook die Docker en Compose installeert op elke app-server. Wanneer site.yml meer dan drie schermen vult, splitst u deze op in roles — maar doe dit pas op dat moment.
Foutmodi, met de strings die u zult zien
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
}De SSH-verbinding is mislukt voordat een module is uitgevoerd: ansible_user is onjuist, de key is nooit naar die host gekopieerd, of de verkeerde key wordt aangeboden. Reproduceer dit met gewone ssh root@10.0.0.10, en gebruik daarna ssh -v om te zien welke keys zijn aangeboden. Als SSH met een wachtwoord wel werkt maar Ansible niet, dan heeft u ssh-copy-id overgeslagen.
Missing sudo password.
web1 | FAILED! => {
"msg": "Missing sudo password"
}U heeft become: true ingesteld en bent verbonden als een non-root gebruiker, en die gebruiker heeft een wachtwoord nodig voor sudo. Voeg ofwel -K (--ask-become-pass) toe aan de command line, of geef de gebruiker een NOPASSWD sudoers-entry — dit is precies de reden waarom het playbook een entry installeert voor deploy voordat u naar die gebruiker wisselt.
error: externally-managed-environment. U heeft pip uitgevoerd op de systeem-Python op Ubuntu 24.04. Beschreven in stap 1: gebruik pipx in plaats van pip, en 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 contextBijna altijd een fout in de indentatie: een key op de verkeerde diepte, of een ontbrekende spatie na een dubbele punt. Het gemelde lijnnummer wijst nabij de fout, niet direct ernaar — controleer ook de regel erboven. De variant found character '\t' that cannot start any token betekent dat er een tab is gebruikt; YAML staat dit niet toe. Maak ansible-playbook site.yml --syntax-check een automatisme voor elke uitvoering, en stel uw editor in op twee spaties voor YAML-indentatie.
/usr/bin/python3: not found. Zeldzaam op standaard Ubuntu 24.04 images, maar komt vaak voor op minimal of netboot images: de uitvoering van de module mislukt omdat de target geen Python heeft. Installeer Python met de raw module, de enige module die niets nodig heeft aan de kant van de target: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, en voer het playbook daarna opnieuw uit.
FAQ
Moet ik Ansible installeren op de servers die ik beheer?
Nee. Ansible is agentless: de control machine stuurt kleine Python modules via SSH, voert deze uit en verwijdert ze daarna weer. Een target heeft alleen python3 en SSH-toegang nodig. Beide zijn standaard aanwezig in Ubuntu images. De enige installatie in deze volledige gids vindt plaats op uw control machine.
Waarom geeft Ansible de foutmelding "Permission denied (publickey)"?
De UNREACHABLE! blok met Permission denied (publickey) betekent dat de SSH-authenticatie is mislukt voordat Ansible een actie kon uitvoeren. Controleer of ansible_user in de inventory overeenkomt met het account dat u heeft aangemaakt. Controleer ook of u ssh-copy-id naar die host heeft uitgevoerd en of een normale ssh user@host zonder wachtwoord kan inloggen. De oplossing voor het normale ssh-commando is ook de oplossing voor Ansible, omdat zij hetzelfde transport gebruiken.
Wat betekent idempotentie in Ansible?
Een task beschrijft een gewenste staat — bijvoorbeeld "dit pakket is aanwezig" of "deze regel staat in dit bestand" — in plaats van een actie die moet worden uitgevoerd. Als de staat al correct is, doet Ansible niets en wordt ok gerapporteerd in plaats van changed. Daarom geeft het uitvoeren van een playbook een tweede keer changed=0 aan, en is een herhaling een veilige audit 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 het systeem-Python als extern beheerd. Hierdoor mislukt pip install ansible met error: externally-managed-environment door ontwerp. pipx install --include-deps ansible plaatst Ansible in een geïsoleerde virtualenv en voegt ansible, ansible-playbook en de rest netjes toe aan uw PATH.
Wat is het verschil tussen de ansible en ansible-core pakketten?
ansible-core is de engine inclusief alleen de ansible.builtin modules. Het ansible pakket bundelt de core met gecureerde community collections — inclusief ansible.posix (de authorized_key module) en community.general (de ufw module), die beide in deze gids worden gebruikt. Begin met het volledige pakket; beperk dit pas tot core plus handgekozen collections wanneer u daar een specifieke reden voor heeft.