Ansible Roles: struttura e riuso del codice
Nel post precedente è stato scritto un playbook che installa e configura Nginx su due server. Funziona, ma ha un limite: tutto il codice è in un unico file. Se si vuole riutilizzare la stessa logica per un terzo server, o in un progetto diverso, si è costretti a copiare il playbook e adattarlo. Ansible Roles risolvono esattamente questo problema.
Cos'è un Role
Un Role è un'unità di automazione autonoma e riutilizzabile. Invece di scrivere tasks direttamente in un playbook, li si organizza in una struttura di cartelle standardizzata che Ansible sa come interpretare.
Il concetto è semplice: un Role incapsula tutto quello che serve per configurare un componente specifico, variabili, tasks, template, file statici, e handler. Il playbook diventa una lista di Role da applicare a gruppi di server, senza logica al suo interno.
Analogia
Un Role è come una funzione in programmazione: la scrivi una volta, la chiami quante volte vuoi, con parametri diversi. Il playbook è il programma principale che chiama le funzioni.
La struttura di un Role
Ansible si aspetta una struttura di cartelle precisa. Non è obbligatorio creare tutte le sottocartelle: Ansible carica solo quelle presenti.
roles/
└── nginx/
├── tasks/
│ └── main.yml # entry point: list of tasks
├── handlers/
│ └── main.yml # handlers triggered by notify
├── templates/
│ └── nginx.conf.j2 # Jinja2 templates
├── files/
│ └── index.html # static files
├── vars/
│ └── main.yml # role variables (high priority)
└── defaults/
└── main.yml # default variables (low priority)Ogni cartella ha un ruolo preciso:
- tasks/ contiene la logica principale. Il file
main.ymlè l'entry point caricato automaticamente - handlers/ contiene gli handler: tasks speciali eseguiti solo quando notificati, tipicamente per riavviare un servizio dopo una modifica alla configurazione
- templates/ contiene file Jinja2 con variabili dinamiche, utili per file di configurazione che cambiano in base all'host
- defaults/ contiene le variabili con priorità più bassa: facilmente sovrascrivibili dal playbook o dall'inventory
- vars/ contiene variabili con priorità alta: difficilmente sovrascrivibili, usate per valori fissi del role
Creare il Role nginx
Ansible mette a disposizione un comando per generare la struttura automaticamente:
$ ansible-galaxy role init roles/nginx
- Role roles/nginx was created successfully
$ tree roles/nginx
roles/nginx
├── defaults/
│ └── main.yml
├── files/
├── handlers/
│ └── main.yml
├── meta/
│ └── main.yml
├── tasks/
│ └── main.yml
├── templates/
├── tests/
│ ├── inventory
│ └── test.yml
└── vars/
└── main.ymlOra si popola il Role con la logica estratta dal playbook precedente, suddivisa nei file corretti.
defaults/main.yml — le variabili
Le variabili con valori di default vanno in defaults/main.yml. Chiunque usa il Role può sovrascriverle senza toccare il codice interno.
---
# Package name to install
nginx_package: nginx
# Service state after installation
nginx_service_state: started
nginx_service_enabled: true
# Port to listen on
nginx_port: 80defaults/main.yml
tasks/main.yml — la logica
I tasks usano le variabili definite nei defaults. Se il valore di default va bene, non serve specificare nulla. Se serve un comportamento diverso, si sovrascrive la variabile nel playbook.
---
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install Nginx
ansible.builtin.apt:
name: "{{ nginx_package }}"
state: present
- name: Ensure Nginx is started and enabled
ansible.builtin.service:
name: "{{ nginx_package }}"
state: "{{ nginx_service_state }}"
enabled: "{{ nginx_service_enabled }}"
notify: Restart Nginxtasks/main.yml
handlers/main.yml — il restart
Un handler è un task speciale: viene eseguito solo se notificato, e solo una volta al termine del play, anche se notificato più volte. È il meccanismo corretto per riavviare un servizio dopo una modifica alla configurazione.
---
- name: Restart Nginx
ansible.builtin.service:
name: "{{ nginx_package }}"
state: restartedhandlers/main.yml
Nota tecnica
Il nome dell'handler in notify deve corrispondere esattamente al campo name dell'handler. È case-sensitive.
Il playbook che usa il Role
Con il Role pronto, il playbook si riduce a poche righe. Tutta la logica è nel Role: il playbook dichiara solo chi fa cosa.
---
- name: Configure webservers
hosts: webservers
become: true
roles:
- nginxsite.yml
Se serve sovrascrivere una variabile di default per un gruppo specifico, si usa group_vars:
# group_vars/webservers.yml
nginx_port: 8080group_vars/webservers.yml
Esecuzione
L'esecuzione è identica a quella di un playbook normale:
$ ansible-playbook -i inventory.ini site.yml
PLAY [Configure webservers] **********************************
TASK [Gathering Facts] ***************************************
ok: [web01]
ok: [web02]
TASK [nginx : Update apt cache] *********************************
ok: [web01]
ok: [web02]
TASK [nginx : Install Nginx] ************************************
ok: [web01]
ok: [web02]
TASK [nginx : Ensure Nginx is started and enabled] **********
ok: [web01]
ok: [web02]
PLAY RECAP **************************************************
web01 : ok=4 changed=0 unreachable=0 failed=0
web02 : ok=4 changed=0 unreachable=0 failed=0Notare il prefisso nginx : davanti a ogni task nell'output: Ansible mostra il nome del Role di provenienza. Con più Role attivi è immediato capire quale componente sta eseguendo cosa.
Struttura finale del progetto
Con i Roles introdotti, la struttura del progetto diventa:
nginx-project/
├── ansible.cfg
├── inventory.ini
├── site.yml # main playbook — roles only
├── group_vars/
│ └── webservers.yml # variable overrides
└── roles/
└── nginx/
├── defaults/
│ └── main.yml # default variables
├── handlers/
│ └── main.yml # restart handlers
└── tasks/
└── main.yml # task logicIl playbook site.yml non contiene più logica: è una mappa che associa gruppi di server a Role. Aggiungere un nuovo componente significa creare un nuovo Role e aggiungerlo alla lista, senza toccare il codice esistente.
Attenzione
I Roles cercano i propri file relativamente alla cartella roles/ nella root del progetto, oppure nei path configurati in ansible.cfg tramite roles_path. Se il Role non viene trovato, Ansible restituisce un errore al momento dell'esecuzione, non durante la scrittura del playbook.
Conclusione
I Roles non aggiungono funzionalità nuove ad Ansible: tutto quello che fa un Role si può scrivere anche in un playbook flat. Quello che aggiungono è struttura, e la struttura rende il codice riutilizzabile, testabile e condivisibile.
Un progetto Ansible maturo è una collezione di Role ben definiti, ognuno responsabile di un singolo componente. Il playbook diventa una dichiarazione di alto livello: quali componenti installare, su quali server, con quali parametri.