🍺 Buy me a beer
🅰️

Ansible per Svogliati

YAML, idempotenza, playbook e "perché siamo finiti a gestire l'infrastruttura con un linguaggio di markup". Una guida scritta da chi non lo voleva imparare e ha dovuto farlo lo stesso.

"Tanto è semplice, è solo YAML." — la frase più ottimista mai pronunciata in informatica, subito dopo "lo finisco entro venerdì".

01 / 12

Cos'è Ansible (e perché esiste)

Uno strumento di automazione che ha vinto perché era "facile". Spoiler: era facile i primi cinque minuti.

Premessa onesta. Ansible nasce con un'idea geniale — "automatizziamo i server senza installare un agent" — e poi cresce in qualcosa che ti fa scrivere logica condizionale dentro file YAML, dentro template Jinja2, dentro role distribuiti su Galaxy, dentro playbook che importano altri playbook che importano variabili da venti posti diversi con ventidue livelli di precedenza. Il momento in cui scrivi when: foo is defined and foo|length > 0 and foo != "" in YAML, capisci che hai sbagliato vita. Detto questo, funziona, è ovunque, e qualcuno ti chiederà di usarlo. Quindi rassegnati.

🤔 Definizione asettica

Ansible è uno strumento di configuration management e orchestrazione open source, scritto in Python, sviluppato da Michael DeHaan dal 2012, comprato da Red Hat nel 2015, ora di IBM. Funziona via SSH, è agentless (non installa nulla sui target), e descrive lo stato desiderato dei sistemi tramite file YAML chiamati playbook.

L'idea è: tu dichiari "voglio che nginx sia installato e in esecuzione, con questo file di configurazione". Ansible si connette in SSH ai server, controlla lo stato attuale, e se non corrisponde lo modifica. Se già corrisponde, non fa nulla. Questo è il famoso "idempotente". Sulla teoria. (Vedi capitolo 09.)

🆓

Agentless

Niente da installare sui target. Solo SSH e Python. Il singolo motivo per cui ha vinto.

📜

Dichiarativo (più o meno)

Descrivi lo stato finale, non i passi. Tranne quando devi descrivere i passi. Cioè spesso.

🐍

Python ovunque

Il control node è Python. I moduli sono Python. Il target deve avere Python. È una cospirazione di Python.

📦

Batterie incluse

Migliaia di moduli per quasi tutto: pacchetti, file, cloud, database, network device, perfino le tende motorizzate (probabilmente).

ℹ️ Quando ha senso davvero usarlo Quando hai più di 3 server da configurare uguali, quando devi rifare lo stesso setup ogni 6 mesi su VM nuove, quando vuoi documentare la configurazione "in codice" invece che in un wiki che nessuno legge. Per un singolo server tuo, scrivi uno script bash. Per 50 server e 3 ambienti diversi, Ansible è effettivamente meno doloroso del fare le stesse cose a mano. Qui sta il trucco: non è bello, è solo meno brutto delle alternative.
02 / 12

Architettura — SSH e basta

Niente daemon, niente porte aperte, niente certificati X.509. Solo SSH e Python. La parte di Ansible che ancora oggi ha senso.

💡 L'analogia del fattorino con la valigia

Immagina che tu sia un capo cantiere con un foglio di istruzioni. Per ogni cantiere (server) mandi un fattorino (Ansible) con una valigia di attrezzi (i moduli Python). Il fattorino arriva via SSH, scarica gli attrezzi necessari nella tasca, esegue il lavoro, raccoglie i risultati e torna a casa lasciando il cantiere pulito. Niente operai stabili, niente strumenti lasciati lì. Quando il lavoro è finito, anche il fattorino sparisce. Il giorno dopo se serve un altro lavoro, parte di nuovo da zero.

⚙️ Come funziona davvero un task

  1. Tu lanci ansible-playbook site.yml dal control node (la tua macchina, un bastion, una VM dedicata).
  2. Ansible legge l'inventory, capisce su quali host deve girare.
  3. Per ogni task, prende il modulo Python corrispondente (es. ansible.builtin.apt), lo copia sul target via SSH in una directory temporanea (~/.ansible/tmp/).
  4. Esegue il modulo Python sul target con gli argomenti che hai messo nel task.
  5. Il modulo restituisce un JSON con il risultato (changed, ok, failed).
  6. Ansible cancella i file temporanei e passa al task successivo.
  7. Tutto questo, in serie, per ogni host. (O in parallelo, fino a forks host alla volta.)
🐢 Sì, è lento Ogni task = una sessione SSH (più o meno, c'è il pipelining). Per 100 task su 50 host fai i conti. Esistono mitigazioni: pipelining=True in ansible.cfg, ControlMaster SSH per riusare le connessioni, strategy: free, async. Ma non aspettarti la velocità di uno script bash locale. È il prezzo dell'agentless.
🐍 Il target deve avere Python Sì, anche oggi nel 2026. Quasi tutte le distro Linux serie ce l'hanno preinstallato. Su sistemi minimal (alcune Alpine, immagini Docker scarne) devi installarlo prima a mano, oppure usare il modulo raw per fare il bootstrap. Ironia: lo strumento "agentless" ha bisogno di un interprete da 30MB sul target. Ma vabbè.
03 / 12

Inventory — la rubrica dei server

Il file dove dichiari su chi vuoi rovesciare i tuoi playbook. Può essere un INI, uno YAML, oppure uno script Python che genera l'inventario al volo (per quando vuoi sentirti hacker).

📒 Inventory statico INI — il classico

inventory.ini
# singoli host nel gruppo "web"
[web]
web1.example.com
web2.example.com
web3.example.com ansible_host=10.0.30.13

# database con porta SSH custom
[db]
db1.example.com ansible_port=2222 ansible_user=ubuntu

# gruppo di gruppi (gerarchia)
[production:children]
web
db

# variabili per tutto un gruppo
[production:vars]
env=prod
ntp_server=ntp.example.com

🆕 Inventory YAML — il nuovo "raccomandato"

Più verboso, più difficile da scrivere a mano, ma è quello che la documentazione ufficiale consiglia. Quindi sicuramente lo userai.

inventory.yml
all:
  children:
    web:
      hosts:
        web1.example.com:
        web2.example.com:
    db:
      hosts:
        db1.example.com:
          ansible_port: 2222
          ansible_user: ubuntu
    production:
      children:
        web:
        db:
      vars:
        env: prod

🌩️ Inventory dinamico

Quando i tuoi server stanno su AWS/GCP/Hetzner e nascono e muoiono ogni ora, scrivere l'inventario a mano è ridicolo. Usi un plugin di inventory che interroga le API del provider e ti restituisce l'elenco aggiornato.

aws_ec2.yml — plugin EC2
plugin: amazon.aws.aws_ec2
regions:
  - eu-central-1
filters:
  tag:Environment: production
keyed_groups:
  - key: tags.Role
    prefix: role
hostnames:
  - private-ip-address
⚠️ Trappola tipica Il plugin dinamico fa una chiamata API a ogni esecuzione di playbook. Se ne lanci 50 al giorno, sei content di usarne la cache (cache: yes nella config). Altrimenti il rate limiting di AWS te lo ricorda lui.
04 / 12

Playbook — YAML, ma per soffrire

Un playbook è un file YAML che descrive cosa Ansible deve fare e su quali host. È anche il posto dove scoprirai che lo spazio prima dei due punti conta, e il tab è il diavolo.

📖 Anatomia di un playbook minimale

site.yml
---  # sì, i tre trattini servono. no, nessuno sa perché esattamente
- name: Configura web server
  hosts: web
  become: yes        # sudo
  vars:
    http_port: 80
    server_name: example.com

  tasks:
    - name: Installa nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: yes

    - name: Copia config nginx
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/default
        owner: root
        mode: '0644'
      notify: Restart nginx

    - name: Assicurati che nginx sia attivo
      ansible.builtin.systemd:
        name: nginx
        state: started
        enabled: yes

  handlers:
    - name: Restart nginx
      ansible.builtin.systemd:
        name: nginx
        state: restarted

Lo lanci con: ansible-playbook -i inventory.ini site.yml

💀 Le quattro morti tipiche del principiante YAML 1) Indentazione: due spazi, sempre. Mai tab. Un tab e l'errore è criptico.
2) Modi numerici come stringhe: mode: 0644 viene interpretato come decimale. Devi scrivere mode: '0644' con le virgolette.
3) Boolean trappola: yes/no/true/false/on/off sono tutti booleani in YAML 1.1. Se hai una password che è la stringa "no", sorpresa.
4) Liste vs stringhe: name: foo bar vs name: [foo, bar] danno comportamenti diversi e i messaggi di errore non aiutano.
05 / 12

Tasks, Modules, Handlers

I tre amici. Un task usa un module per fare una cosa. Un handler è un task che si attiva solo se notificato. Sembra semplice, lo è anche, finché non lo è più.

🔧 Module = unità di lavoro

Un module è un pezzo di codice Python che fa una cosa specifica in modo idempotente: installare un pacchetto, copiare un file, creare un utente, gestire un servizio. Ce ne sono migliaia. I principali da sapere a memoria:

  • apt / dnf / package — pacchetti
  • copy / template / file — file e directory
  • systemd / service — servizi
  • user / group — utenti
  • lineinfile / blockinfile — modifica righe in file (terribile, ma comodo)
  • shell / command — eseguire comandi (la valvola di sfogo)

🔔 Handler = task pigro

Un handler è un task definito a parte che gira solo se notificato da un altro task e solo se quel task è in stato changed. Esempio classico: cambi un file di config, e solo allora riavvii il servizio.

Tutti gli handler notificati durante un play vengono eseguiti alla fine del play, non subito. Questo è un problema se cambi tre file e te ne accorgi solo dopo il restart che il terzo era rotto.

🎛️ Condizionali, loop, gestione errori

la parte dove YAML inizia a sembrare un linguaggio di programmazione
# condizionale
- name: Installa nginx solo su Debian
  ansible.builtin.apt:
    name: nginx
    state: present
  when: ansible_os_family == "Debian"

# loop
- name: Crea più utenti
  ansible.builtin.user:
    name: "{{ item }}"
    state: present
  loop:
    - alice
    - bob
    - charlie

# ignora errori (la versione "vediamo che succede")
- name: Prova qualcosa di rischioso
  ansible.builtin.shell: /usr/local/bin/legacy-script.sh
  ignore_errors: yes
  register: result

# cattura il risultato e usalo dopo
- name: Mostra cosa ha fatto
  ansible.builtin.debug:
    var: result.stdout
🪤 Shell e command sono il peccato originale Usare shell o command rompe l'idempotenza, perché Ansible non sa cosa fa il comando: lo eseguirà sempre, ogni volta, anche se lo stato è già quello giusto. La scappatoia è creates: / removes: (esegui solo se il file non esiste / esiste). Vai prima a cercare se esiste un modulo dedicato. Spoiler: nove volte su dieci esiste.
06 / 12

Variabili (e i 22 livelli di precedenza)

La parte di Ansible in cui finisci a debuggare per ore "ma da dove diavolo arriva questo valore?" e la risposta è "da uno dei 22 posti possibili, in ordine di priorità che nessuno ricorda".

Sì, ventidue. Esiste un ordine di precedenza ufficiale tra le variabili definite in playbook, role default, role vars, host_vars, group_vars, inventory plain, inventory group_vars, inventory host_vars, command line con -e, registered vars, set_fact, env vars, e altre amenità che non ti elenco perché altrimenti non finiamo più. Quando una variabile sembra "sbagliata", è quasi sempre perché c'è un'altra definizione da qualche altra parte che vince. Lo strumento giusto è ansible-inventory --host nomehost --vars per vedere cosa realmente Ansible vede per quell'host. Risparmia ore.

📍 Dove mettere le variabili — la versione sana

Esistono molti posti per dichiarare variabili. Per non impazzire, usa pochi posti e in modo coerente:

  • group_vars/all.yml → variabili condivise tra tutti gli host
  • group_vars/<groupname>.yml → variabili specifiche di un gruppo (es. group_vars/web.yml)
  • host_vars/<hostname>.yml → variabili specifiche di un singolo host (es. il NODE_ID univoco)
  • roles/xxx/defaults/main.yml → default del role (priorità bassa, sovrascrivibili)
  • roles/xxx/vars/main.yml → costanti del role (priorità alta, non toccarle)
  • command line con -e "var=value" → override emergenziale, vince su quasi tutto

🔍 Variabili magiche di Ansible

Ci sono variabili "speciali" predefinite che Ansible riempie da solo:

VariabileCosa contiene
inventory_hostnameIl nome dell'host nell'inventory
ansible_hostL'IP/DNS effettivo (può essere diverso dall'inventory_hostname)
ansible_factsTutti i fatti raccolti dal target (OS, IP, RAM, dischi, ecc.)
ansible_os_familyDebian / RedHat / Suse / Archlinux
groupsDizionario di tutti i gruppi e i loro host
hostvarsDizionario delle variabili di tutti gli host (utile per cross-host config)
play_hostsHost attualmente nel play in esecuzione
07 / 12

Templates Jinja2

Quando devi generare file di configurazione che cambiano a seconda dell'host. Un template engine dentro YAML dentro Python — la stratificazione che meritiamo.

📄 Template Jinja2 in azione

templates/nginx.conf.j2
server {
    listen {{ http_port }};
    server_name {{ server_name }};

    root /var/www/{{ server_name }};
    index index.html;

    # condizionale dentro il template
    {% if enable_ssl %}
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/{{ server_name }}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{{ server_name }}/privkey.pem;
    {% endif %}

    # loop dentro il template
    {% for upstream in backend_servers %}
    location /api/{{ loop.index }} {
        proxy_pass http://{{ upstream.host }}:{{ upstream.port }};
    }
    {% endfor %}
}

🎚️ Filtri Jinja2 utili davvero

  • {{ var | default('fallback') }} — valore di default se vuota
  • {{ list | join(',') }} — concatena
  • {{ dict | to_nice_json }} — pretty JSON
  • {{ password | password_hash('sha512') }} — hash password
  • {{ var | bool }} — forza a boolean
  • {{ list | unique }} — elimina duplicati
  • {{ var | mandatory }} — fallisce se la variabile non è definita (ottimo per evitare errori silenziosi)
🤯 YAML + Jinja2 = sintassi orribile Quando hai espressioni Jinja in YAML, devi metterle tra virgolette se iniziano con {, perché altrimenti il parser YAML pensa sia un dizionario inline. Risultato: name: "{{ inventory_hostname }}". Sempre. Anche quando non sembra servire. Soffri ora, soffri meno dopo.
08 / 12

Roles & Galaxy

Un modo per organizzare i playbook in pacchetti riutilizzabili. E un mercato di role di terzi (Galaxy) di cui circa il 60% è abbandonato dal 2019.

📁 Struttura standard di un role

roles/nginx/
roles/
└── nginx/
    ├── defaults/
    │   └── main.yml       # variabili default (priorità bassa)
    ├── vars/
    │   └── main.yml       # variabili "costanti" (priorità alta)
    ├── tasks/
    │   └── main.yml       # i task del role
    ├── handlers/
    │   └── main.yml       # gli handler
    ├── templates/
    │   └── nginx.conf.j2  # template Jinja2
    ├── files/
    │   └── default.html   # file statici da copiare
    ├── meta/
    │   └── main.yml       # dipendenze, autore, supporto OS
    └── README.md          # che nessuno legge

🪐 Ansible Galaxy

Galaxy è il "registry pubblico" di role e collection. L'idea è che invece di reinventare la ruota, scarichi un role pronto.

requirements.yml + galaxy
# requirements.yml
roles:
  - name: geerlingguy.docker
    version: "7.4.1"

collections:
  - name: community.general
  - name: ansible.posix

# installazione
ansible-galaxy install -r requirements.yml
ansible-galaxy collection install -r requirements.yml
🧓 Onesto avviso Su Galaxy ci sono diamanti (i role di Jeff Geerling sono praticamente standard de facto), e poi c'è un cimitero di role mai aggiornati dal 2018, con dipendenze rotte e supporto solo per CentOS 6. Verifica sempre la data dell'ultimo commit prima di farti dipendere da qualcosa. Ansible Galaxy è un po' come un mercatino delle pulci: se sai cosa cerchi trovi tesori, altrimenti porti a casa una lampada con la spina sbagliata.
09 / 12

Idempotenza — la promessa

"Lo stesso playbook eseguito due volte produce lo stesso risultato." In teoria. In pratica dipende molto da quanto sei stato bravo a non scrivere shell:.

🎯 Il senso dell'idempotenza

Un task idempotente verifica lo stato corrente prima di agire. Se lo stato è già quello desiderato, non fa nulla e segnala ok. Se non lo è, lo modifica e segnala changed. La promessa è che puoi rilanciare il playbook ogni notte senza paura: se è già tutto ok, non succede nulla.

Tutti i moduli "buoni" di Ansible sono idempotenti per design: apt, file, user, systemd, copy, template. Se metti state: present e il pacchetto c'è già, ti dice "ok" e prosegue.

✓ Idempotente

  • Usare i moduli ufficiali (apt, copy, template, user, ecc.)
  • Specificare sempre state: present/absent/started esplicitamente
  • Usare creates: / removes: con shell quando inevitabile
  • Usare check_mode: yes per testare senza applicare (--check)

✗ Non idempotente

  • Usare shell e command senza creates:
  • Appendere righe a file con shell: echo ... >> file
  • Generare file con timestamp dentro (cambia ogni volta)
  • Modificare risorse esterne tramite API senza controllo dello stato
🧪 Test di idempotenza fai-da-te Lancia il playbook una prima volta. Lancialo una seconda volta subito dopo. Conta i changed. Se sono > 0, hai un task non idempotente. Trovalo e sistemalo. Esiste anche ansible-lint e molecule per testare in modo serio, ma il "doppio lancio" è il test più rapido.
10 / 12

Ansible Vault — i segreti

Per non committare le password in chiaro su git. Non è il sistema di secret più bello del mondo, ma è quello che hai se non vuoi tirarci dentro Vault di HashiCorp.

🔐 Cifrare un file

ansible-vault
# cifra un file esistente
ansible-vault encrypt group_vars/all/secrets.yml

# crea un file nuovo già cifrato
ansible-vault create group_vars/all/secrets.yml

# modifica un file cifrato (apre l'editor)
ansible-vault edit group_vars/all/secrets.yml

# vedi il contenuto in chiaro (su stdout)
ansible-vault view group_vars/all/secrets.yml

# cambia la password
ansible-vault rekey group_vars/all/secrets.yml

# lancia il playbook chiedendo la password vault
ansible-playbook site.yml --ask-vault-pass

# meglio: file con la password (in .gitignore!)
ansible-playbook site.yml --vault-password-file ~/.vault_pass.txt
💡 Pattern utile: variabili pubbliche + variabili segrete Tieni due file paralleli per ogni gruppo: group_vars/web/main.yml (in chiaro, leggibile dai colleghi) e group_vars/web/vault.yml (cifrato, password e chiavi API). Nel main usi db_password: "{{ vault_db_password }}" e nel file vault definisci vault_db_password: .... Così quando guardi il config di un host vedi che la variabile esiste, senza dover decifrare nulla.
🪦 Quando Vault non basta Vault è simmetrico, condiviso, e una volta che hai la password apri tutto. Se la tua azienda ha un secret manager serio (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager), Ansible ha lookup plugin per leggere i segreti al volo da lì. Più sicuro, più rotabile, più rotture per configurarlo. Scegli il tuo dolore.
11 / 12

Debug, errori tipici, sopravvivenza

La parte in cui passi più tempo. La modalità debug di Ansible è fatta di stampe a schermo e tanta pazienza.

🔍 Le opzioni di debug che useri davvero

flag salvavita
# dry run — non applica nulla, mostra cosa farebbe
ansible-playbook site.yml --check

# mostra anche le diff dei file modificati
ansible-playbook site.yml --check --diff

# verbose — sale a -vvvv per vedere SSH e tutto
ansible-playbook site.yml -vvv

# lancia solo task con un certo tag
ansible-playbook site.yml --tags nginx

# salta task con un certo tag
ansible-playbook site.yml --skip-tags slow

# inizia da un task specifico (per riprendere da dove hai fallito)
ansible-playbook site.yml --start-at-task "Configure database"

# limita a un solo host (per testare)
ansible-playbook site.yml --limit web1.example.com

# test ad-hoc senza playbook
ansible web -m ping
ansible web -m shell -a "uptime"
ansible web -m setup   # mostra tutti i fact dell'host

😩 Errori e come si traducono

MessaggioCosa significa davvero
UNREACHABLE!Non riesco a connettermi in SSH. Chiave sbagliata, host down, firewall, IP scaduto.
FAILED! ... non-zero return codeUn comando shell ha ritornato errore. Scorri il stderr nell'output JSON.
Missing sudo passwordIl task ha become: yes ma non hai NOPASSWD su sudo. Aggiungi --ask-become-pass.
'dict object' has no attribute XStai accedendo a una chiave di un dizionario che non c'è. Probabile problema di facts non raccolti.
The conditional check ... failedErrore di sintassi Jinja in un when:. Quasi sempre virgolette dimenticate.
could not find or access fileIl path di un template/file è relativo a posti che non immagini. Usa {{ role_path }} o path assoluti.
🛟 Il salvataggio delle 2 di notte Quando un playbook fallisce a metà su 30 host: --limit @site.retry. Ansible scrive automaticamente nella directory un file .retry con gli host falliti. Lo riusi per ripartire solo da quelli, dopo aver capito cosa è andato storto. Tipo "salva e ricarica" del videogioco.
12 / 12

Alternative — perché Ansible ha vinto

Non perché era il migliore. Perché era il meno doloroso al primo impatto. La storia dell'IT in una frase.

⚔️ Confronto rapido

ToolLinguaggioArchitetturaVerdetto svogliato
AnsibleYAMLAgentless (SSH)Brutto, lento, ma ovunque. Il vincitore.
SaltYAML + JinjaMaster/MinionTecnicamente più potente, più complicato da iniziare. Vivo nei datacenter grossi.
PuppetDSL customMaster/AgentGlorioso passato, presente di nicchia. Lo trovi negli ambienti enterprise classici.
ChefRuby DSLServer/Client"Configuration as code in Ruby." Già la frase fa scappare.
Terraform / OpenTofuHCLProvider pluginNon è un concorrente diretto: gestisce infrastruttura, non configurazione di sistema. Spesso si usano insieme: Terraform crea le VM, Ansible le configura.
Bash + SSH loopBashNientePer <5 server è ancora la risposta giusta. Non lo dice nessuno ma lo sappiamo tutti.

🏆 Perché Ansible ha vinto

  • Niente agent = niente da installare, niente porte da aprire, niente certificati. Per i sysadmin del 2013 era una rivoluzione.
  • YAML = "non è un linguaggio di programmazione, quindi possono usarlo anche i non-dev". Bugia parziale, ma efficace nel marketing.
  • SSH = qualcosa che già sapevi fare. Nessuna nuova astrazione da imparare il primo giorno.
  • Red Hat = backing aziendale serio dal 2015, ecosistema (AWX/Tower), Ansible Automation Platform per le aziende che pagano per il bottone "fai cose".
  • Galaxy = role pronti per i casi più comuni, anche se di qualità variabile.
Verdetto finale. Ansible non è bello. È brutto e funziona. Lo userai contro la tua volontà perché è il default che la tua azienda ha già scelto, perché c'è un playbook dell'ex collega che nessuno vuole riscrivere, perché Galaxy ha già il role per quella cosa che ti serve, perché a fine giornata "ansible-playbook site.yml" è meno doloroso di entrare in 40 server uno per uno. Non innamorartene. Imparalo abbastanza per fare il tuo lavoro, scrivici sopra ruoli puliti e idempotenti, mettilo in CI, e poi pensa ad altro. Lui non si offenderà. È uno strumento, mica una fede.
🍺 Cheat finale 1) ansible-playbook --check --diff prima di applicare. Sempre.
2) group_vars / host_vars per le variabili. Mai inline nei playbook.
3) Vault per i segreti, mai password in chiaro nel repo.
4) Roles piccoli, focalizzati, con default sensati.
5) Tag sui task per poterli rilanciare a pezzi.
6) ansible-lint in CI prima del merge.
7) Quando hai un dubbio: ansible-doc nome_modulo e ansible-inventory --host x --vars. Sono tuoi amici.