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ì".
Uno strumento di automazione che ha vinto perché era "facile". Spoiler: era facile i primi cinque minuti.
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.
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.)
Niente da installare sui target. Solo SSH e Python. Il singolo motivo per cui ha vinto.
Descrivi lo stato finale, non i passi. Tranne quando devi descrivere i passi. Cioè spesso.
Il control node è Python. I moduli sono Python. Il target deve avere Python. È una cospirazione di Python.
Migliaia di moduli per quasi tutto: pacchetti, file, cloud, database, network device, perfino le tende motorizzate (probabilmente).
Niente daemon, niente porte aperte, niente certificati X.509. Solo SSH e Python. La parte di Ansible che ancora oggi ha senso.
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.
ansible-playbook site.yml dal control node (la tua macchina, un bastion, una VM dedicata).ansible.builtin.apt), lo copia sul target via SSH in una directory temporanea (~/.ansible/tmp/).changed, ok, failed).forks host alla volta.)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.
raw per fare il bootstrap. Ironia: lo strumento "agentless" ha bisogno di un interprete da 30MB sul target. Ma vabbè.
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).
# 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
Più verboso, più difficile da scrivere a mano, ma è quello che la documentazione ufficiale consiglia. Quindi sicuramente lo userai.
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
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.
plugin: amazon.aws.aws_ec2
regions:
- eu-central-1
filters:
tag:Environment: production
keyed_groups:
- key: tags.Role
prefix: role
hostnames:
- private-ip-address
cache: yes nella config). Altrimenti il rate limiting di AWS te lo ricorda lui.
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.
--- # 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
mode: 0644 viene interpretato come decimale. Devi scrivere mode: '0644' con le virgolette.yes/no/true/false/on/off sono tutti booleani in YAML 1.1. Se hai una password che è la stringa "no", sorpresa.name: foo bar vs name: [foo, bar] danno comportamenti diversi e i messaggi di errore non aiutano.
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ù.
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 — pacchetticopy / template / file — file e directorysystemd / service — serviziuser / group — utentilineinfile / blockinfile — modifica righe in file (terribile, ma comodo)shell / command — eseguire comandi (la valvola di sfogo)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.
# 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 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.
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".
-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.
Esistono molti posti per dichiarare variabili. Per non impazzire, usa pochi posti e in modo coerente:
group_vars/web.yml)-e "var=value" → override emergenziale, vince su quasi tuttoCi sono variabili "speciali" predefinite che Ansible riempie da solo:
| Variabile | Cosa contiene |
|---|---|
inventory_hostname | Il nome dell'host nell'inventory |
ansible_host | L'IP/DNS effettivo (può essere diverso dall'inventory_hostname) |
ansible_facts | Tutti i fatti raccolti dal target (OS, IP, RAM, dischi, ecc.) |
ansible_os_family | Debian / RedHat / Suse / Archlinux |
groups | Dizionario di tutti i gruppi e i loro host |
hostvars | Dizionario delle variabili di tutti gli host (utile per cross-host config) |
play_hosts | Host attualmente nel play in esecuzione |
Quando devi generare file di configurazione che cambiano a seconda dell'host. Un template engine dentro YAML dentro Python — la stratificazione che meritiamo.
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 %}
}
{{ 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){, 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.
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.
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
Galaxy è il "registry pubblico" di role e collection. L'idea è che invece di reinventare la ruota, scarichi un role pronto.
# 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
"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:.
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.
state: present/absent/started esplicitamentecreates: / removes: con shell quando inevitabilecheck_mode: yes per testare senza applicare (--check)shell e command senza creates:shell: echo ... >> filechanged. 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.
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.
# 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
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.
La parte in cui passi più tempo. La modalità debug di Ansible è fatta di stampe a schermo e tanta pazienza.
# 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
| Messaggio | Cosa significa davvero |
|---|---|
| UNREACHABLE! | Non riesco a connettermi in SSH. Chiave sbagliata, host down, firewall, IP scaduto. |
| FAILED! ... non-zero return code | Un comando shell ha ritornato errore. Scorri il stderr nell'output JSON. |
| Missing sudo password | Il task ha become: yes ma non hai NOPASSWD su sudo. Aggiungi --ask-become-pass. |
| 'dict object' has no attribute X | Stai accedendo a una chiave di un dizionario che non c'è. Probabile problema di facts non raccolti. |
| The conditional check ... failed | Errore di sintassi Jinja in un when:. Quasi sempre virgolette dimenticate. |
| could not find or access file | Il path di un template/file è relativo a posti che non immagini. Usa {{ role_path }} o path assoluti. |
--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.
Non perché era il migliore. Perché era il meno doloroso al primo impatto. La storia dell'IT in una frase.
| Tool | Linguaggio | Architettura | Verdetto svogliato |
|---|---|---|---|
| Ansible | YAML | Agentless (SSH) | Brutto, lento, ma ovunque. Il vincitore. |
| Salt | YAML + Jinja | Master/Minion | Tecnicamente più potente, più complicato da iniziare. Vivo nei datacenter grossi. |
| Puppet | DSL custom | Master/Agent | Glorioso passato, presente di nicchia. Lo trovi negli ambienti enterprise classici. |
| Chef | Ruby DSL | Server/Client | "Configuration as code in Ruby." Già la frase fa scappare. |
| Terraform / OpenTofu | HCL | Provider plugin | Non è un concorrente diretto: gestisce infrastruttura, non configurazione di sistema. Spesso si usano insieme: Terraform crea le VM, Ansible le configura. |
| Bash + SSH loop | Bash | Niente | Per <5 server è ancora la risposta giusta. Non lo dice nessuno ma lo sappiamo tutti. |
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.
ansible-doc nome_modulo e ansible-inventory --host x --vars. Sono tuoi amici.