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:

Generate role skeleton
$ 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.yml

Ora 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: 80

defaults/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 Nginx

tasks/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: restarted

handlers/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:
    - nginx

site.yml

Se serve sovrascrivere una variabile di default per un gruppo specifico, si usa group_vars:

# group_vars/webservers.yml
nginx_port: 8080

group_vars/webservers.yml

Esecuzione

L'esecuzione è identica a quella di un playbook normale:

Run playbook with role
$ 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=0

Notare 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 logic

Il 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.

Iscriviti a Ansible Automation Blog

Non perdere gli ultimi articoli. Iscriviti ora per essere aggiornato sulle nuove pubblicazioni.
mario@esempio.it
Iscriviti