SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-27

Ansible: playbook o ruolo, quale scegliere?

Scopri quando basta un playbook Ansible e quando serve un ruolo: struttura delle directory, ansible-galaxy init, richiami e precedenza delle variabili.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Differenza tra playbook e ruolo Ansible

Un playbook Ansible è il file che esegui con ansible-playbook. Associa un gruppo di host alle operazioni da eseguire. Un ruolo Ansible è una directory con una struttura fissa che contiene task, template, handler e variabili predefinite. Un playbook richiama il ruolo per nome. La sintassi dei task è identica in entrambi i casi. La differenza non riguarda quindi ciò che puoi esprimere, ma il riutilizzo.

Inizia con un playbook semplice. Un singolo site.yml che contiene un elenco tasks: è la struttura corretta per la prima automazione e rimane adatta più a lungo di quanto ci si aspetti. Converti il playbook in un ruolo quando lo stesso blocco di task deve essere eseguito per un secondo gruppo di host oppure quando il file supera circa 100 righe e non riesci più a trovare un task scorrendo il contenuto.

Se non ne hai ancora scritto uno, inizia con un primo playbook su un singolo VPS e riprendi da qui quando inizia a crescere.

Quando la risposta corretta è un playbook piatto

Un playbook piatto è la scelta corretta quando l'attività viene eseguita una sola volta, su un solo host oppure quando nessun altro lo leggerà. Il provisioning di un singolo application server o l'applicazione di patch a un server prima di una finestra di manutenzione non giustificano una struttura di directory. Un ruolo aggiunge sette directory e un livello di indirezione. Se l'unico chiamante è il playbook che si trova accanto al ruolo, questa indirezione non offre alcun vantaggio e richiede un passaggio in più ogni volta che si vuole leggere ciò che viene effettivamente eseguito.

Il playbook piatto smette di essere la scelta corretta in un momento preciso, facile da individuare. Si copia un blocco di attività in un secondo playbook. Questa copia è il segnale. Da quel momento ogni correzione deve essere applicata due volte e, prima o poi, verrà applicata una volta sola.

Cosa contiene realmente una directory di ruolo

roles/common/
  defaults/main.yml
  vars/main.yml
  tasks/main.yml
  handlers/main.yml
  templates/99-hardening.conf.j2
  files/
  meta/main.yml
  • tasks/main.yml è il punto di ingresso. Ansible esegue questo file quando il ruolo viene richiamato; tutte le altre directory sono facoltative.
  • defaults/main.yml contiene le variabili che il chiamante dovrebbe sovrascrivere. È la fonte con la priorità più bassa in Ansible, quindi quasi qualsiasi altra impostazione ha la precedenza.
  • vars/main.yml contiene le variabili che il chiamante non dovrebbe sovrascrivere. Ha una priorità superiore a quella dell'inventory, una scelta che va fatta solo in casi motivati. Usatela raramente.
  • handlers/main.yml contiene i task attivati da notify. Un handler viene eseguito al termine del play, una sola volta, indipendentemente dal numero di task che lo hanno notificato.
  • files/ contiene i file copiati senza modifiche dal modulo copy, mentre templates/ contiene i template Jinja2 elaborati dal modulo template. All'interno di un ruolo, entrambi si referenziano usando il solo nome del file, senza il percorso, perché Ansible cerca prima nelle directory del ruolo.
  • meta/main.yml dichiara le dipendenze del ruolo e i metadati letti da Ansible Galaxy.

La struttura non è una preferenza stilistica. Ansible cerca i contenuti in questi percorsi esatti, quindi un template inserito in roles/common/template/ (al singolare) non viene semplicemente trovato.

Creare il ruolo comune con ansible-galaxy init

mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles common

Questo comando scrive l'intero scheletro in roles/common, incluse directory che non utilizzerai e stub main.yml contenenti soltanto ---. Elimina quelli che lasci vuoti. Un vars/main.yml vuoto non causa problemi ad Ansible, ma nasconde quali file del ruolo sono effettivamente importanti.

Ora compila i file che eseguono il lavoro. Inizia dai valori predefiniti, perché costituiscono l'interfaccia pubblica del ruolo.

# roles/common/defaults/main.yml
---
common_packages:
  - ufw
  - fail2ban
  - unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no"

Racchiudi "no" e "yes" tra virgolette. Ansible analizza YAML con PyYAML, che interpreta un no non racchiuso tra virgolette come il valore booleano false; di conseguenza, la riga di configurazione generata diventa PermitRootLogin False e sshd la rifiuta. Le virgolette mantengono il valore come stringa.

# roles/common/tasks/main.yml
---
- name: Install the base packages
  ansible.builtin.apt:
    name: "{{ common_packages }}"
    state: present
    update_cache: true
    cache_valid_time: 3600

- name: Create the admin group
  ansible.builtin.group:
    name: "{{ common_admin_group }}"
    state: present

- name: Install the sshd hardening drop-in
  ansible.builtin.template:
    src: 99-hardening.conf.j2
    dest: /etc/ssh/sshd_config.d/99-hardening.conf
    owner: root
    group: root
    mode: "0644"
    validate: /usr/sbin/sshd -t -f %s
  notify: Restart sshd
# roles/common/handlers/main.yml
---
- name: Restart sshd
  ansible.builtin.service:
    name: ssh
    state: restarted
# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}

Su Debian e Ubuntu l'unità systemd si chiama ssh, mentre sui sistemi della famiglia RHEL si chiama sshd. Un handler che indica l'unità errata fallisce soltanto quando qualcosa modifica effettivamente il template. Per questo l'errore emerge spesso dopo diverse settimane.

La riga validate è l'elemento più utile di quel task. Ansible genera il template in un file temporaneo, sostituisce %s con il percorso di quel file ed esegue il comando. La destinazione viene sostituita soltanto se il comando termina con codice 0. Inserisci nel template una direttiva non valida ed esegui di nuovo il task: il task fallisce con failed to validate, il vero /etc/ssh/sshd_config.d/99-hardening.conf rimane intatto e puoi ancora accedere al server. Tieni presente che il controllo verifica più della sola sintassi. Se sshd -t non riesce a leggere le chiavi host, termina con sshd: no hostkeys available -- exiting. e Ansible segnala lo stesso failed to validate. Prima di attribuire il problema al template, consulta msg del modulo.

Come un playbook richiama un ruolo

# site.yml
---
- name: Base configuration for every server
  hosts: all
  become: true
  roles:
    - common
# inventory.ini
[local]
localhost ansible_connection=local
ansible-playbook -i inventory.ini site.yml

Il play deve terminare con failed=0 nel riepilogo. Passa i parametri nel punto della chiamata usando la forma estesa. In questo modo un ruolo può essere usato per due gruppi di host:

  roles:
    - role: common
      common_admin_group: ops
      common_permit_root_login: prohibit-password

Esiste una regola sull'ordine che sorprende quasi tutti. Un play può contenere pre_tasks, roles, tasks e post_tasks, e Ansible li esegue in quell'ordine indipendentemente dall'ordine in cui li hai scritti nel file. Inserisci tasks: prima di roles:, ma i ruoli vengono comunque eseguiti per primi. Quindi, se un'operazione deve essere eseguita prima di un ruolo, deve trovarsi in pre_tasks:, non all'inizio di tasks:.

- name: Ordering demonstration
  hosts: local
  gather_facts: false
  pre_tasks:
    - name: Runs first
      ansible.builtin.debug:
        msg: pre
  roles:
    - common
  tasks:
    - name: Runs after the role
      ansible.builtin.debug:
        msg: task
  post_tasks:
    - name: Runs last
      ansible.builtin.debug:
        msg: post

Per richiamare un ruolo dall'interno di un elenco di task invece di usare la chiave roles:, usa import_role o include_role.

  tasks:
    - name: Static, read when the playbook is parsed
      ansible.builtin.import_role:
        name: common

    - name: Dynamic, resolved when the task runs
      ansible.builtin.include_role:
        name: postgres
      when: "'db' in group_names"

import_role è statico. Ansible legge il ruolo in fase di analisi e i relativi task diventano parte del play. Di conseguenza, ansible-playbook --list-tasks site.yml li elenca e un tag sull'importazione viene applicato a ogni task incluso. include_role è dinamico. Il contenuto non viene letto finché il task non viene eseguito. Questo permette di determinare il nome del ruolo tramite una variabile o un ciclo. Il compromesso è che questi task non sono visibili a --list-tasks e a --start-at-task.

Qui esiste un errore comune. Un when: su un task include_role viene valutato prima che defaults/main.yml del ruolo incluso sia disponibile. Se scrivi when: common_packages | length > 0 sull'include, l'esecuzione si interrompe con 'common_packages' is undefined, anche se quella variabile è definita proprio nel ruolo che stai includendo. La soluzione consiste nello spostare l'opzione fuori dal ruolo: inseriscila in group_vars/all.yml, dove è disponibile ovunque, e lascia i valori predefiniti del ruolo per i valori utilizzati dal ruolo stesso.

Quale variabile prevale: valori predefiniti, group_vars, vars, extra vars

Ansible documenta più di venti livelli di precedenza delle variabili. Quattro di questi risolvono quasi ogni caso reale. Sono elencati dal livello più debole a quello più forte.

  • roles/<name>/defaults/main.yml si trova nella parte bassa della gerarchia. Quasi qualsiasi valore impostato altrove lo sovrascrive. Per questo è il posto corretto per i parametri regolabili di un ruolo.
  • group_vars/ e host_vars/ si trovano nella parte centrale. Qui vanno inseriti i valori specifici del sito, che sovrascrivono in modo chiaro i valori predefiniti del ruolo.
  • roles/<name>/vars/main.yml ha una precedenza maggiore di host_vars. Un valore impostato qui non può essere sovrascritto dall'inventory. Usalo per gli elementi che il ruolo deve mantenere coerenti internamente, ad esempio un nome di pacchetto che deve corrispondere al nome di un servizio.
  • Un parametro del ruolo passato nel punto in cui il ruolo viene chiamato prevale su vars/main.yml, mentre -e nella riga di comando prevale su tutto, inclusi i parametri del ruolo.

Puoi osservare il risultato in circa un minuto. Crea un ruolo semplice con un valore predefinito e una variabile del ruolo, quindi imposta gli stessi nomi in host_vars.

# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults
# roles/prec/vars/main.yml
---
prec_internal: from-rolevars
# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars
# roles/prec/tasks/main.yml
---
- name: Show which value survived
  ansible.builtin.debug:
    msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"
ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cli

La prima esecuzione stampa tunable=from-hostvars internal=from-rolevars. L'inventory ha prevalso sul valore predefinito del ruolo, ma è stata sovrascritta dalla variabile del ruolo. La seconda esecuzione stampa internal=from-cli, perché gli extra vars si trovano al livello più alto e nessun valore sottostante può prevalere su di essi. Per lo stesso motivo, -e è adatto a un'esecuzione occasionale, ma non a uno script che mantieni nel tempo: sovrascrive silenziosamente ogni decisione considerata nel repository.

La regola pratica è questa: se vuoi che un valore possa essere modificato, inseriscilo in defaults/. Inserirlo in vars/ comunica a ogni futuro utente del ruolo che l'inventory non può modificarlo. A volte è la scelta corretta, ma nella maggior parte dei casi è un errore accidentale.

Verifica che il ruolo sia idempotente: eseguilo due volte

Un'esecuzione Ansible affidabile produce lo stesso risultato anche la seconda volta e segnala che non è cambiato nulla. Esegui il playbook due volte e leggi il riepilogo.

ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml

Il secondo riepilogo dovrebbe essere simile al seguente:

PLAY RECAP *********************************************************************
localhost   : ok=4  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=0 significa che ogni modulo ha verificato lo stato corrente e ha rilevato che il lavoro era già stato completato. changed=2 in una seconda esecuzione significa che due task non riescono a distinguere lo stato corrente da quello desiderato, quindi continueranno a riscrivere i file e a riavviare i servizi senza fine. Il colpevole abituale è command o shell, perché Ansible non può sapere che cosa abbia fatto un comando arbitrario.

# traps.yml
---
- name: Command modules do not know what they changed
  hosts: local
  gather_facts: false
  tasks:
    - name: This appends a line on every run
      ansible.builtin.shell: "echo run >> /tmp/grow.txt"

    - name: This appends a line only once
      ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
      args:
        creates: /tmp/guarded.txt

Esegui quel playbook due volte, quindi conta le righe con wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt contiene due righe e /tmp/guarded.txt ne contiene una. Nella seconda esecuzione il task con la condizione non è stato eseguito e il relativo risultato contiene il messaggio skipped, since /tmp/guarded.txt exists, perché creates consente al modulo di cercare prima un prodotto visibile. Quando un comando non lascia un prodotto di questo tipo, registra il relativo output e valuta autonomamente il risultato con changed_when.

ansible-playbook --check --diff site.yml prevede le modifiche senza applicarle, mentre --diff stampa le righe esatte che un template riscriverebbe. Leggi l'output tenendo presente una limitazione: i task shell e command vengono saltati in check mode, quindi un piano che sembra completo può comunque nascondere del lavoro.

Anche un'altra colonna del riepilogo richiede la stessa attenzione: un host a cui Ansible non è riuscito a connettersi viene conteggiato in unreachable invece che in failed e nessuno dei suoi task è stato eseguito. Prima di applicare questo ruolo a più di un paio di macchine, decidi in anticipo se un host irraggiungibile debba interrompere l'intera esecuzione.

Perché Ansible segnala che il ruolo non è stato trovato

Ansible cerca una directory roles/ accanto al file del playbook, quindi in roles_path. La ricerca segue il playbook, non la shell.

ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely

Il messaggio indica che site.yml e roles/ non sono più allineati e mostra i percorsi che Ansible ha provato. Mantieni i due elementi nella stessa directory. Puoi eseguire il comando da una directory padre, perché ciò che conta è il percorso del playbook:

ansible-playbook -i infra/inventory.ini infra/site.yml

Esiste una variante meno evidente dello stesso problema. Ansible ignora un ansible.cfg nella directory corrente quando questa è scrivibile da tutti, perché qualsiasi utente del server potrebbe inserirvi una configurazione e modificare il comportamento dell'esecuzione.

[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.

Le impostazioni roles_path e inventory risultano quindi assenti senza messaggi e la ricerca del ruolo non riesce per un motivo che non riguarda i ruoli. ansible --version mostra il config file effettivamente caricato, mentre ansible-config dump --only-changed mostra ogni impostazione diversa dai valori predefiniti integrati. Controllali entrambi quando un'esecuzione si comporta come se la configurazione non esistesse.

Condivisione dei ruoli: requirements.yml e versione fissata

Un ruolo scritto da terzi viene installato, non copiato. Dichiaralo una sola volta:

# requirements.yml
---
roles:
  - name: postgres
    src: https://github.com/example/ansible-role-postgres
    scm: git
    version: v1.4.0
ansible-galaxy install -r requirements.yml -p galaxy_roles

Imposta sempre version. Senza questa opzione viene usato il contenuto del branch predefinito disponibile nel giorno in cui esegui il comando. Di conseguenza, un deployment che funzionava il mese scorso può interrompersi senza alcuna modifica nel tuo repository. Imposta roles_path sulla directory di download ed escludi questa directory da git:

# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_roles

I ruoli in roles/ accanto al playbook vengono comunque trovati, perché quel percorso viene sempre cercato in aggiunta a roles_path. In questo modo i tuoi ruoli restano versionati e sottoposti a revisione, mentre i ruoli di terze parti sono download riproducibili fissati a un tag.

Quando i ruoli non sono più la soluzione

Un ruolo è un'unità di riutilizzo all'interno di una singola esecuzione di Ansible. Non crea server né record DNS presso il provider. Cercare di fargli svolgere queste attività trasforma i playbook in codice che nessuno vuole più mantenere. Prima di iniziare, conviene leggere come suddividere il lavoro tra Ansible e Terraform. Un ruolo non sostituisce nemmeno la progettazione dell'inventory: superato un numero limitato di macchine, come raggruppare e raggiungere quei server diventa più importante di come sono organizzate le attività.

Anche l'hardening installato da questo ruolo common richiede decisioni specifiche. Il drop-in precedente imposta soltanto due direttive. Prima di decidere cosa includere nel ruolo per ogni host gestito, leggi quali impostazioni SSH vale davvero la pena modificare e come fare in modo che Ubuntu applichi automaticamente gli aggiornamenti di sicurezza.

FAQ

Quando devo trasformare un playbook Ansible in un ruolo?

Quando lo stesso blocco di task deve essere eseguito in una seconda play o su un secondo gruppo di host. Copiare task tra playbook è il segnale: da quel momento ogni correzione deve essere applicata due volte e, prima o poi, verrà applicata una sola volta. Un singolo playbook di circa 100 righe o meno, destinato sempre a un solo gruppo, non trae vantaggio da un ruolo; le directory aggiuntive ne rendono più difficile la lettura.

I ruoli vengono eseguiti prima dei task della stessa play?

Sì. Ansible esegue pre_tasks, quindi tutto ciò che è elencato sotto roles:, poi tasks: e infine post_tasks:; ignora l'ordine in cui queste chiavi compaiono nel file. Scrivere tasks: sopra roles: non fa eseguire prima quei task. Se qualcosa deve essere eseguito prima di un ruolo, inseriscilo in pre_tasks:.

Perché il valore in group_vars non sovrascrive quello del ruolo?

Controlla se la variabile è impostata in vars/main.yml del ruolo invece che in defaults/main.yml. vars/ ha una precedenza maggiore di group_vars e host_vars nell'ordine di precedenza di Ansible, quindi l'inventory non può sovrascriverlo. Sposta la variabile in defaults/main.yml, che si trova vicino al fondo dell'ordine ed è la posizione corretta per qualsiasi valore che il chiamante debba poter modificare. Per verificare che la causa sia la precedenza e non un errore di battitura, esegui una volta il playbook con -e name=value, che ha precedenza su qualsiasi altra origine.

Perché Ansible dice che il ruolo non è stato trovato?

La ricerca inizia accanto al file del playbook, quindi site.yml e roles/ devono trovarsi nella stessa directory. L'errore mostra i percorsi provati, come in the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. Eseguire il playbook da una directory padre non è un problema, perché la ricerca segue il percorso del playbook e non la directory di lavoro della shell. Se dipendi da roles_path di ansible.cfg, verifica che il file sia stato caricato con ansible --version, perché una directory di lavoro scrivibile da chiunque fa sì che Ansible lo ignori.

Mi serve ansible-galaxy init per creare un ruolo?

No. Un ruolo è costituito soltanto da directory con i nomi previsti, quindi mkdir -p roles/common/tasks più un tasks/main.yml costituiscono già un ruolo funzionante. ansible-galaxy init --init-path roles common evita di digitare manualmente la struttura e crea lo scheletro completo, inclusi meta/main.yml e un modello di README. Elimina le directory che lasci vuote, perché un vars/main.yml vuoto nasconde quali file del ruolo eseguono effettivamente delle operazioni.

#ansible#roles#playbook#structure#automation