Tutorial Ansible: primo playbook per un VPS
Installa Ansible con pipx su Ubuntu 24.04, crea inventory e playbook per mettere in sicurezza un VPS. Risolvi anche gli errori Permission denied e sudo.
Cosa stai creando
Una macchina di controllo con Ansible installato e uno o più VPS Ubuntu 24.04 appena creati, con la sola immagine standard. Al termine avrai un file di inventory che identifica i server, un comando ping ad hoc che dimostra che l'autenticazione funziona dall'inizio alla fine e un playbook che esegue come codice l'intera checklist per un nuovo VPS: un utente di deployment con la tua chiave SSH, sshd configurato in modo più sicuro, fail2ban, aggiornamenti automatici non interattivi e un firewall che consente OpenSSH prima di negare tutto il resto. Puoi applicarlo a un server o a venti. Eseguilo due volte: la seconda esecuzione non modifica nulla. Questo è l'obiettivo.
Dopo quindici anni di provisioning di VPS, posso descrivere lo schema reale: tutti configurano manualmente i primi cinque server, poi perdono un fine settimana con il sesto perché nessuno ricorda che cosa ha fatto sui primi cinque. Questa guida approfondisce la panoramica in gestione di più server Linux; riprendila quando ti accorgi che stai digitando lo stesso apt install in tre terminali.
Che cos'è davvero Ansible, in un paragrafo
Ansible è agentless. Non è necessario installare alcun demone sui server che gestisce: la macchina di controllo si connette tramite il normale SSH, copia un piccolo modulo Python sul sistema di destinazione, lo esegue, legge il JSON che stampa e lo elimina. L'unico requisito per il sistema di destinazione è python3, già presente in ogni immagine Ubuntu standard. Il termine importante è idempotente e ha un significato semplice: un task descrive uno stato, non un'azione. state: present per un pacchetto significa «verificare che sia installato», non «eseguire il programma di installazione». Se lo stato è già quello richiesto, Ansible non modifica nulla e lo segnala come ok invece di changed. Questa proprietà è il fondamento del prodotto: rende sicura l'esecuzione ripetuta di un playbook, e le riesecuzioni sicure trasformano uno script shell in infrastruttura.
Prerequisiti e problemi da conoscere subito
- Una macchina di controllo: il laptop o un piccolo VPS. Si presume Ubuntu 24.04; macOS funziona nello stesso modo dopo aver installato pipx da Homebrew.
- Uno o più VPS di destinazione con Ubuntu 24.04 su KVM, raggiungibili come root. Su questi sistemi non viene installato nulla.
- Autenticazione SSH tramite chiave per ogni destinazione. Ansible usa esattamente lo stesso metodo di autenticazione del comando
ssh; sessh root@hostrichiede una password, Ansible non riesce. - Su Ubuntu 24.04,
pip install ansibletermina conerror: externally-managed-environment. È una scelta deliberata della distribuzione, non un malfunzionamento. Usare pipx. - Gli spazi bianchi in YAML fanno parte della sintassi. Un'indentazione errata produce
mapping values are not allowed in this contexte la presenza di un carattere di tabulazione in qualsiasi punto causa un errore irreversibile. - Lasciare aperta una sessione SSH funzionante su ogni destinazione mentre il playbook rende più sicuro sshd. Ogni blocco dell'accesso da cui ho aiutato un cliente a riprendersi ha comportato la chiusura dell'ultima sessione, "per provare da una sessione pulita".
Passaggio 1: installare Ansible sulla macchina di controllo con pipx, non con pip
L’approccio abituale è pip3 install ansible. Su un’immagine 24.04 completamente nuova, il comando si interrompe un passaggio prima con Command 'pip3' not found, but can be installed with: sudo apt install python3-pip. Installare pip porta quindi al vero blocco:
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 considera Python di sistema gestito esternamente (PEP 668), quindi pip non può competere con apt sugli stessi file. Non usare --break-system-packages: il nome del flag descrive esattamente il problema. La soluzione corretta è pipx, che assegna ad Ansible un virtualenv isolato e inserisce i binari nel PATH:
sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansibleApri una nuova shell dopo pipx ensurepath, in modo che la modifica al PATH diventi effettiva. --include-deps non è un dettaglio: il pacchetto ansible non installa direttamente script console, ansible, ansible-playbook e gli altri sono entry point della sua dipendenza ansible-core. Senza quel flag, pipx rifiuta l’installazione con No apps associated with package ansible or its dependencies. Installa inoltre il pacchetto ansible, non il solo ansible-core: il pacchetto completo include le collection community e questo playbook usa moduli di due di esse (ansible.posix e community.general).
ansible --versionIl risultato corretto inizia con una riga simile a ansible [core 2.19.x] e indica il Python utilizzato; per tutto il contenuto di questa guida va bene qualsiasi release core aggiornata. ansible: command not found indica invece che ~/.local/bin non è ancora nel PATH: apri una nuova shell oppure esegui source ~/.bashrc.
L’installazione è completa. Sui target non viene installato nulla.
Passaggio 2: accesso tramite chiave SSH a ogni destinazione
ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20Quindi verificalo una volta per ogni host:
ssh root@10.0.0.10 true && echo okQuella riga svolge due funzioni: conferma che l'autenticazione tramite chiave funziona senza password e registra la chiave dell'host in known_hosts. Esegui ora questo comando, perché Ansible presenta una chiave dell'host non registrata come una richiesta interattiva durante l'esecuzione, che appare esattamente come un blocco.
Passaggio 3: l’inventory, prima INI e YAML quando cresce
L’inventory è un file di testo che elenca le macchine che Ansible può gestire. Crea inventory.ini in una nuova directory del progetto:
[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20
[vps:vars]
ansible_user=rootweb1 è un alias scelto da te. È il nome mostrato nell’output e quello a cui indirizzi --limit web1. ansible_host è l’indirizzo reale. [vps] è un gruppo e [vps:vars] imposta variabili per ogni host che ne fa parte; ansible_user indica l’account con cui Ansible accede ai sistemi. Accanto a questo, aggiungi un ansible.cfg per non dover digitare di nuovo -i:
[defaults]
inventory = inventory.iniAnsible legge ansible.cfg dalla directory corrente. La stessa inventory in YAML, salvata come inventory.yml e indicata a ansible.cfg al suo posto, è la scelta preferibile quando ogni host ha diverse variabili:
vps:
hosts:
web1:
ansible_host: 10.0.0.10
web2:
ansible_host: 10.0.0.20
vars:
ansible_user: rootSono equivalenti. Con due server, il formato INI è più facile da controllare a colpo d’occhio; con venti, YAML si adatta meglio. Scegline uno e non pensarci più.
Passaggio 4: comandi ad hoc, il pong verde che conferma il corretto funzionamento
ansible all -m pingNon si tratta di ICMP. Il modulo ping è una prova completa: accesso SSH, copia del modulo, esecuzione di Python sul target e pulizia. Il risultato corretto è verde, con un blocco per ogni host:
web1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}Il verde SUCCESS indica che l'autenticazione, l'interprete Python e il trasporto funzionano; anche il playbook funzionerà. Il rosso UNREACHABLE! indica che il trasporto non è riuscito prima dell'esecuzione di qualsiasi modulo. La stringa esatta e la correzione sono riportate nella sezione sulle modalità di errore più avanti. Ecco altri due comandi ad hoc utili:
ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --becomeI comandi ad hoc servono per operazioni singole e verifiche. Qualsiasi comando che eseguiresti due volte deve essere inserito in un playbook.
Passaggio 5: il primo playbook, la checklist del nuovo VPS come codice
Queste sono tutte le operazioni che eseguiresti manualmente nei primi dieci minuti su un nuovo server. Salvale in 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: restartedEcco le righe che è importante comprendere, anziché copiarle semplicemente:
Le variabili si trovano in vars: e vengono referenziate con "{{ deploy_user }}". Racchiudi l'intera espressione tra virgolette quando un valore inizia con una graffa, altrimenti il parser YAML può interpretarla in modo errato. lookup('file', ...) legge la chiave pubblica dalla macchina di controllo al runtime, quindi il playbook non contiene materiale crittografico.
Il ciclo. loop: "{{ baseline_services }}" esegue l'attività sul servizio una volta per ogni elemento e l'output mostra ogni elemento su una riga distinta. Nota che l'attività apt riceve l'intero elenco dei pacchetti in un'unica operazione. Una transazione apt singola è più veloce ed è il modello preferito per i pacchetti; i cicli servono per i moduli che operano realmente su un elemento alla volta.
L'handler è il concetto da comprendere. notify: Restart ssh non significa "riavviare ssh subito". Mette l'handler in coda, che viene eseguito una volta al termine del play e solo se l'attività che lo notifica ha effettivamente restituito changed. Se esegui di nuovo il playbook domani, il file drop-in è già corretto, l'attività di copia restituisce ok e sshd non viene riavviato. La riga validate: protegge il trigger: sshd controlla il file prima di sostituire quello precedente, quindi un errore di sintassi fa fallire l'attività invece di interrompere il daemon.
PermitRootLogin prohibit-password, non no, intenzionalmente. Questo playbook accede come root usando una chiave. prohibit-password disabilita gli accessi root tramite password mantenendo attivo il tuo accesso. Dopo avere verificato che l'utente di deploy funziona (ssh deploy@10.0.0.10 sudo true, l'indirizzo effettivo, perché web1 è solo un alias noto ad Ansible), sostituisci ansible_user=deploy nell'inventory e impostalo su no in un'esecuzione successiva. Applica l'hardening in un ordine che non possa lasciarti senza accesso.
Il prefisso 00- è importante. Per la maggior parte delle direttive, sshd utilizza la prima occorrenza che analizza e sshd_config di Ubuntu include sshd_config.d/*.conf in ordine lessicale prima del proprio corpo. Le cloud image Ubuntu 24.04 includono già un 60-cloudimg-settings.conf in quella directory e i provider che abilitano gli accessi tramite password con cloud-init aggiungono un 50-cloud-init.conf con PasswordAuthentication yes. Assegnare al nostro file il nome 00-hardening.conf lo fa ordinare per primo, consentendogli di prevalere su entrambi.
L'ordine delle attività garantisce la sicurezza del firewall. Allow OpenSSH viene eseguito prima di Enable ufw con una policy di deny. Ansible esegue le attività esattamente nell'ordine elencato, quindi il varco esiste prima che venga attivata la barriera. fail2ban non richiede alcuna configurazione per essere utile in questo caso: i valori predefiniti di Ubuntu monitorano sshd immediatamente. Il funzionamento effettivo delle jail e i parametri da ottimizzare sono descritti nella guida a fail2ban su Ubuntu 24.04.
Passaggio 6: eseguire una prova con --check, quindi applicare le modifiche
ansible-playbook site.yml --checkLa modalità di controllo stabilisce la connessione, calcola le operazioni che verrebbero eseguite e non modifica nulla. Leggi il conteggio changed= nel PLAY RECAP in fondo: indica il numero di attività che modificherebbero ciascun host. C'è però una limitazione strutturale: la modalità di controllo non può gestire correttamente i casi in cui un'attività successiva dipende dalle modifiche apportate da un'attività precedente. L'immagine server standard di Ubuntu include già ufw, quindi questo playbook viene simulato senza errori. In un'immagine minimale che ne è priva, invece, le attività ufw falliscono in modalità di controllo, perché questa modalità non installa effettivamente il pacchetto e il modulo non ha quindi nulla da chiamare. È una limitazione delle prove a secco, non un errore del playbook. Quando il piano è corretto:
ansible-playbook site.ymlOgni attività stampa una riga per ogni host: changed in giallo, ok in verde. Il riepilogo dovrebbe essere:
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=0Quei ok comprendono la raccolta dei fatti, otto attività e l'handler. Il tuo changed può differire dal mio di una o due unità: l'immagine standard di Ubuntu include già ufw e unattended-upgrades, e fail2ban si avvia non appena apt lo installa. Per questo, alla prima esecuzione, un'attività può legittimamente riportare ok, perché lo stato dichiarato è già presente. I valori che devono essere pari a zero sono unreachable e failed. Una nota su become: true: mentre ti connetti come root è una formalità, ma quando imposti ansible_user su deploy sudo diventa effettivo. Il file sudoers NOPASSWD installato da questo playbook impedisce esattamente che -K venga richiesto nella riga di comando. Senza questo file, ottieni Missing sudo password, come descritto di seguito.
Passaggio 7: eseguirlo due volte per vedere l'idempotenza
Eseguire subito lo stesso comando:
web1 : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 e ok sono diminuiti di uno perché l'handler non notificato non è mai stato eseguito. Non è stato reinstallato nulla, sshd non è stato riavviato e ufw non è stato modificato. È questo che rende il playbook uno strumento di audit oltre che di provisioning: aggiungere web3 all'inventario il mese prossimo e rieseguirlo consente di configurare il nuovo server e verificare quelli esistenti. Un valore changed diverso da zero su un server che non è stato modificato indica una deriva di configurazione e segnala che qualcuno ha modificato manualmente ciò che avrebbe dovuto essere modificato nel playbook.
Da qui il modello si estende. Il prossimo playbook utile configura una VPN WireGuard sullo stesso VPS e restringe la regola di ufw, in modo che SSH risponda solo tramite il tunnel; dopo, se ne può scrivere uno che installi Docker e Compose su ogni server applicativo. Quando site.yml supera le tre schermate, suddividerlo in ruoli, ma non prima.
Modalità di errore, con le stringhe visualizzate
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
}Il trasporto SSH non è riuscito prima dell'esecuzione di qualsiasi modulo: ansible_user è errato, la chiave non è mai stata copiata su quell'host oppure viene offerta la chiave sbagliata. Riproduci il problema con il comando ssh root@10.0.0.10, quindi usa ssh -v per verificare quali chiavi sono state offerte. Se SSH con password funziona ma Ansible no, hai omesso ssh-copy-id.
Missing sudo password.
web1 | FAILED! => {
"msg": "Missing sudo password"
}Hai impostato become: true, ti sei connesso con un utente non root e quell'utente deve fornire una password per sudo. Aggiungi -K (--ask-become-pass) alla riga di comando oppure assegna all'utente una voce NOPASSWD in sudoers. È esattamente il motivo per cui il playbook installa una voce per deploy prima di passare a quell'utente.
error: externally-managed-environment. Hai eseguito pip sul Python di sistema in Ubuntu 24.04. Questo caso è trattato nel passaggio 1: usa pipx, non pip e non --break-system-packages.
mapping values are not allowed in this context.
ERROR! Syntax Error while loading YAML.
mapping values are not allowed in this contextQuasi sempre la causa è l'indentazione: una chiave si trova al livello errato oppure manca uno spazio dopo i due punti. Il numero di riga indicato punta vicino all'errore, non necessariamente alla riga errata; controlla anche la riga precedente. L'errore correlato found character '\t' that cannot start any token indica che è stato inserito un carattere di tabulazione; YAML non lo consente. Rendi ansible-playbook site.yml --syntax-check un controllo automatico prima di ogni esecuzione e configura l'editor per usare un'indentazione di due spazi nei file YAML.
/usr/bin/python3: not found. È raro nelle immagini Ubuntu 24.04 standard, ma comune in quelle minimal o netboot: l'esecuzione del modulo non riesce perché sul target non è installato Python. Installalo con il modulo raw, l'unico modulo che non richiede nulla sul sistema remoto: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, quindi esegui nuovamente il playbook.
FAQ
Devo installare Ansible sui server che gestisce?
No. Ansible non richiede agent: la macchina di controllo invia piccoli moduli Python tramite SSH, li esegue e li rimuove. Una macchina di destinazione richiede soltanto python3 e l'accesso SSH, già disponibili nelle immagini Ubuntu standard. L'unica installazione prevista in questa guida viene eseguita sulla macchina di controllo.
Perché Ansible restituisce "Permission denied (publickey)"?
Il blocco UNREACHABLE! con Permission denied (publickey) indica che l'autenticazione SSH non è riuscita prima che Ansible eseguisse qualsiasi operazione. Verifica che ansible_user nell'inventory corrisponda all'account configurato, che tu abbia eseguito ssh-copy-id verso quell'host e che il comando ssh user@host effettui l'accesso senza password. Qualsiasi correzione applicata al comando ssh normale risolve anche il problema di Ansible, perché usano lo stesso trasporto.
Che cosa significa idempotente in Ansible?
Un task dichiara uno stato desiderato, ad esempio "questo pacchetto è presente" o "questa riga è presente in questo file", invece di un'azione da eseguire. Se lo stato è già quello richiesto, Ansible non esegue alcuna modifica e restituisce ok invece di changed. Per questo, alla seconda esecuzione di un playbook viene visualizzato changed=0 e una nuova esecuzione è un controllo sicuro, non una reinstallazione rischiosa.
Devo usare pip o pipx per installare Ansible su Ubuntu 24.04?
pipx. Ubuntu 24.04 considera Python di sistema gestito esternamente, quindi pip install ansible fallisce intenzionalmente con error: externally-managed-environment. pipx install --include-deps ansible installa Ansible in un virtualenv isolato ed espone ansible, ansible-playbook e gli altri comandi nel PATH in modo corretto.
Qual è la differenza tra i pacchetti ansible e ansible-core?
ansible-core è il motore insieme soltanto ai moduli ansible.builtin. Il pacchetto ansible include il core e le collection community selezionate, tra cui ansible.posix (il modulo authorized_key) e community.general (il modulo ufw), entrambi utilizzati in questa guida. Inizia con il pacchetto completo; passa a core più collection selezionate manualmente solo quando hai un motivo concreto.