SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-07

Ansible: playbook o role, quale usare e quando?

Scopri quando basta un playbook Ansible e quando conviene un role: 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.

Differenze tra playbook e role di Ansible

Un playbook Ansible è il file che esegui con ansible-playbook. Associa un gruppo di host alle attività che devono eseguire. Un role Ansible è una directory con una struttura fissa che contiene task, template, handler e variabili predefinite; un playbook lo richiama per nome. La sintassi dei task è identica in entrambi i casi, quindi non è una questione di ciò che puoi esprimere. È una questione di riutilizzo.

Inizia con un playbook semplice. Un singolo site.yml che contiene un elenco tasks: è la struttura giusta per la prima automazione e rimane adatta più a lungo di quanto ci si aspetti. Converti il contenuto in un role 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 torna qui quando inizia a crescere.

Quando un playbook semplice è la scelta corretta

Un playbook semplice è corretto quando l'attività viene eseguita una sola volta, su un solo host oppure quando nessun altro lo leggerà. Provisionare un singolo application server o applicare patch a un server prima di una finestra di manutenzione non richiede una struttura di directory. Un role aggiunge sette directory e un livello di indirezione. Se l'unico chiamante è il playbook che si trova accanto, questa indirezione non offre alcun vantaggio e costringe a fare un passaggio in più ogni volta che si vuole leggere ciò che viene effettivamente eseguito.

Il playbook semplice smette di essere la scelta corretta in un momento preciso, facile da individuare. Si copia un blocco di task 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 effettivamente 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 viene richiamato il ruolo; tutte le altre directory sono facoltative.
  • defaults/main.yml contiene le variabili che il chiamante dovrebbe sovrascrivere. In Ansible ha la priorità più bassa, quindi quasi qualsiasi altra origine ha la precedenza.
  • vars/main.yml contiene le variabili che il chiamante non dovrebbe sovrascrivere. Ha una priorità superiore a quella dell'inventory, quindi è una scelta con conseguenze rilevanti. Usatela raramente.
  • handlers/main.yml contiene i task attivati da notify. Un handler viene eseguito alla fine 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, fate riferimento a entrambi 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 semplice preferenza stilistica. Ansible cerca i file 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'intera struttura in roles/common, incluse directory che non userai e file main.yml che contengono soltanto ---. Elimina quelli che lasci vuoti. Un vars/main.yml vuoto non crea problemi ad Ansible, ma nasconde quali file del ruolo sono effettivamente importanti.

Ora compila i file che eseguono le operazioni. 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 quotato 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 specifica il nome errato fallisce soltanto quando una modifica effettiva aggiorna il template, perciò il problema in genere emerge dopo alcune settimane.

La riga validate è l'elemento più utile di quel task. Ansible esegue il rendering del 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 una direttiva non valida nel template ed esegui di nuovo il task: il task fallisce con failed to validate, il vero /etc/ssh/sshd_config.d/99-hardening.conf non viene modificato e puoi ancora accedere al server. Tieni presente che il controllo verifica aspetti ulteriori rispetto alla sintassi. Se sshd -t non riesce a leggere le host key, 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 uno stesso ruolo può gestire 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. Ansible li esegue in quest'ordine, indipendentemente dall'ordine in cui li hai scritti nel file. Inserisci tasks: prima di roles:, ma i ruoli vengono comunque eseguiti per primi. 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 durante l'analisi e i relativi task diventano parte del play. Di conseguenza, ansible-playbook --list-tasks site.yml li elenca e un tag applicato all'importazione si applica a ogni task incluso. include_role è dinamico. Ansible non legge nulla finché il task non viene eseguito. Questo consente di determinare il nome del ruolo tramite una variabile o un loop. Lo svantaggio è che questi task non sono visibili a --list-tasks e --start-at-task.

Qui è presente un problema comune. Un when: in 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 la 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 default del ruolo per i valori utilizzati dal ruolo stesso.

Quale variabile prevale: defaults, group_vars, vars, extra vars

Ansible documenta più di venti livelli di precedenza delle variabili. Quattro di questi risolvono quasi ogni discussione reale. Eccoli dal più debole al più forte.

  • roles/<name>/defaults/main.yml si trova quasi alla base della gerarchia. Quasi tutto ciò che si imposta altrove lo sovrascrive. Per questo è il posto corretto per i parametri configurabili 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 netto i valori predefiniti del ruolo.
  • roles/<name>/vars/main.yml prevale su host_vars. Un valore definito qui non può essere sovrascritto dall'inventory. Riservalo agli 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 di chiamata prevale su vars/main.yml, mentre -e nella riga di comando prevale su tutto, inclusi i parametri del ruolo.

Puoi osservare questa risoluzione in circa un minuto. Assegna a un piccolo ruolo 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 a sua volta sovrascritta dalla variabile del ruolo. La seconda esecuzione stampa internal=from-cli, perché le extra vars si trovano al livello più alto e nessun livello inferiore può sovrascriverle. Per lo stesso motivo, -e va bene per un'esecuzione occasionale, ma è una scelta errata in uno script che viene mantenuto: sovrascrive silenziosamente ogni decisione considerata nel repository.

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

Verificare l’idempotenza del ruolo: eseguirlo due volte

Un’esecuzione di Ansible affidabile produce lo stesso risultato alla seconda esecuzione e segnala che non è cambiato nulla. Eseguire il playbook due volte e leggere 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 indica che ogni modulo ha verificato lo stato corrente e ha rilevato che l’operazione era già stata eseguita. changed=2 alla seconda esecuzione indica che due task non riescono a distinguere lo stato corrente da quello precedente. Di conseguenza, continueranno a riscrivere i file e a riavviare i servizi senza fine. La causa più comune è command o shell, perché Ansible non può sapere che cosa ha 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

Eseguire il playbook due volte, quindi contare le righe contenenti wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt contiene due righe e /tmp/guarded.txt ne contiene una. Alla 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 fornisce al modulo un prodotto visibile da cercare prima. Quando un comando non lascia un prodotto di questo tipo, registrare il relativo output e decidere autonomamente con changed_when.

ansible-playbook --check --diff site.yml prevede le modifiche senza applicarle, mentre --diff stampa le righe esatte che un template riscriverebbe. Leggere l’output tenendo presente un’avvertenza: i task shell e command vengono ignorati in check mode. Di conseguenza, un piano che sembra privo di modifiche può comunque nascondere operazioni da eseguire.

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

Questo messaggio indica che site.yml e roles/ non sono più allineati e mostra i percorsi che Ansible ha verificato. Mantienili nella stessa directory. Puoi eseguire il comando da una directory padre, perché conta il percorso del playbook:

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

Esiste anche una variante meno evidente dello stesso problema. Ansible ignora un ansible.cfg nella directory corrente quando questa è scrivibile da tutti, perché qualsiasi utente del sistema 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 espliciti 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 incorporati. Controllali entrambi quando un'esecuzione si comporta come se la configurazione non esistesse.

Condividere i ruoli: requirements.yml e una versione fissata

Un ruolo scritto da altri 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 questo parametro viene usato il contenuto del branch predefinito disponibile quando esegui il comando. Di conseguenza, un deployment che funzionava il mese scorso può non funzionare più 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é questo 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 vengono scaricati in modo riproducibile con una versione fissata tramite tag.

Dove i ruoli non sono più la soluzione

Un ruolo è un'unità di riutilizzo all'interno di un'esecuzione di Ansible. Non crea server né record DNS presso il provider. Cercare di fargli svolgere anche queste operazioni è il modo più rapido per trasformare i playbook in qualcosa che nessuno vuole più mantenere. Prima di iniziare, conviene leggere la suddivisione delle attività tra Ansible e Terraform. Un ruolo non sostituisce neppure la progettazione dell'inventory: quando si supera un numero limitato di macchine, il modo in cui si raggruppano e si raggiungono quei server conta più di come vengono organizzate le attività.

Anche le misure di hardening installate da questo ruolo common richiedono decisioni specifiche. Il drop-in precedente imposta soltanto due direttive. Prima di decidere cosa includere nel ruolo per tutti gli host gestiti, 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 un secondo play o su un secondo gruppo di host. Copiare i task tra playbook è il segnale, perché 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 nello stesso play?

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

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

Verifica se la variabile è impostata in vars/main.yml del ruolo invece che in defaults/main.yml. vars/ ha precedenza su 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 è il posto corretto per tutto ciò che il chiamante deve poter modificare. Per confermare che la causa sia la precedenza e non un refuso, esegui una volta 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 stampa i percorsi che ha provato, 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 usi roles_path da 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.

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 costituisce già un ruolo funzionante. ansible-galaxy init --init-path roles common evita di scrivere manualmente la struttura e fornisce lo scheletro completo, inclusi meta/main.yml e una bozza del 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