Il primo Playbook: da zero a produzione

Nel post precedente abbiamo capito perché Ansible esiste e cosa lo rende diverso dagli altri strumenti di automazione. Ora passiamo al pratico: scriveremo il primo playbook, riga per riga, e lo eseguiremo su un server reale.

L'obiettivo è installare e configurare Nginx su un server remoto, il classico "Hello World" dell'automazione infrastrutturale. Semplice abbastanza da capire tutto, complesso abbastanza da toccare tutti i concetti fondamentali.

Prima di scrivere una riga di YAML

Per seguire questo laboratorio servono tre cose: Ansible installato sul control node, un server raggiungibile via SSH, e una coppia di chiavi SSH già configurata.

  • Control Node
    Mac o Linux da cui si lancia Ansible. Windows richiede WSL2
  • Managed Node
    Un server Linux (Ubuntu 22.04 ideale). Può essere una VM locale, un VPS, o un'istanza cloud
  • Chiavi SSH
    La chiave pubblica deve essere presente in ~/.ssh/authorized_keys sul server
  • Ansible
    Versione 2.14 o superiore. Installabile con pip in un virtualenv
# Create a dedicated virtualenv
$ python3 -m venv ~/.venv/ansible
$ source ~/.venv/ansible/bin/activate
 
# Install Ansible and linter
$ pip install ansible ansible-lint
 
# Check Ansible installation
$ ansible --version
ansible [core 2.17.0]
python version = 3.11.4

Installazione di Ansible

Pro tip

Usa sempre un virtualenv per Ansible. Isola le dipendenze, permette di avere versioni diverse per progetti diversi, e non inquina il sistema Python globale.

L'Inventory: dire ad Ansible dove andare

Prima di scrivere cosa fare, Ansible ha bisogno di sapere dove farlo. L'inventory è esattamente questo: l'elenco dei server che deve gestire, organizzati in gruppi logici.

Crea una cartella per il progetto e al suo interno un file inventory.ini:

# Groups are enclosed in square brackets
[webservers]
web01 ansible_host=192.168.1.10 ansible_user=ubuntu
web02 ansible_host=192.168.1.11 ansible_user=ubuntu
 
# Variables shared across the group
[webservers:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_private_key_file=~/.ssh/id_ed25519

inventory.ini

Prima di procedere, verificare che Ansible riesca a raggiungere i server con il modulo ping. Attenzione: non è il ping ICMP standard, è un test di connettività Ansible via SSH:

Connectivity test
$ ansible webservers -i inventory.ini -m ping web01 | SUCCESS => { "changed": false, "ping": "pong" } web02 | SUCCESS => { "changed": false, "ping": "pong" }
Nota

Se il risultato è SUCCESS su tutti i server, si può procedere. In caso di errori di connessione, verificare che la chiave SSH sia corretta e che il firewall permetta connessioni sulla porta 22.

Anatomia del playbook

Un playbook è un file YAML che descrive cosa ottenere sui server. Non una serie di comandi shell da eseguire in sequenza, ma una dichiarazione dello stato finale desiderato. Questa differenza è fondamentale.

Creiamo install_nginx.yml per installare Nginx ed eseguirlo sugli host remoti.

Intestazione del playbook

Un play inizia sempre con tre informazioni fondamentali:

  • il nome descrittivo
  • il gruppo di server target
  • i privilegi di esecuzione

---
- name: Install and configure Nginx  # human-readable name for this play
  hosts: webservers                  # target group defined in inventory
  become: true                       # escalate privileges via sudo

Task nei playbook

Ogni task ha un nome descrittivo e chiama un modulo Ansible. Il nome non è obbligatorio ma è una best practice: comparirà nell'output durante l'esecuzione.

  tasks:
 
    - name: Update apt cache         # task name shown in the output
      ansible.builtin.apt:           # fully qualified module name (FQCN)
        update_cache: true           # module parameter
        cache_valid_time: 3600       # skip update if cache is fresh

Stati

Il parametro state è il cuore dell'idempotenza: non si dice ad Ansible di installare Nginx, si dichiara che Nginx deve essere presente. Se lo è già, Ansible non fa nulla.

    - name: Install Nginx
      ansible.builtin.apt:
        name: nginx
        state: present               # desired state, not an action

Lo stesso principio vale per i servizi: state: started significa che il servizio deve essere in esecuzione, enabled: true che deve partire automaticamente al boot.

    - name: Ensure Nginx is started and enabled
      ansible.builtin.service:
        name: nginx
        state: started               # service must be running
        enabled: true                # start automatically on boot
Nota Tecnica

Il prefisso ansible.builtin davanti al nome del modulo è il Fully Qualified Collection Name (FQCN). Non è obbligatorio, ma è la best practice raccomandata: evita ambiguità quando il progetto usa moduli di collezioni esterne con lo stesso nome.

Mettendo tutto insieme, il playbook finale è questo:

---
- name: Install and configure Nginx
  hosts: webservers
  become: true
 
  tasks:
 
    # Update the apt cache before installing packages
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600
 
    - name: Install Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
 
    - name: Ensure Nginx is started and enabled
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

Playbook installazione Nginx

Attenzione a YAML

YAML è sensibile all'indentazione: usare sempre spazi, mai tab. Configurare l'editor con expandtab e indentazione a 2 spazi per i file .yml e .yaml. Un errore di indentazione è spesso la causa di errori misteriosi.

Esegui il playbook

Prima di eseguire su produzione, usare sempre il flag --check per un dry-run: Ansible simula l'esecuzione senza toccare nulla sui server.

Dry-run — no changes applied
$ ansible-playbook -i inventory.ini install_nginx.yml --check PLAY [Install and configure Nginx] *************************** TASK [Gathering Facts] *************************************** ok: [web01] ok: [web02] TASK [Update apt cache] ***************************************** changed: [web01] changed: [web02] TASK [Install Nginx] ********************************************* changed: [web01] changed: [web02] TASK [Ensure Nginx is started and enabled] *************** ok: [web01] ok: [web02]

Il dry-run mostra cosa cambierebbe senza applicare modifiche. Quando non ci sono errori, si rimuove --check ed esegue il playbook:

Live run
$ ansible-playbook -i inventory.ini install_nginx.yml PLAY [Install and configure Nginx] *************************** TASK [Gathering Facts] *************************************** ok: [web01] ok: [web02] TASK [Update apt cache] ***************************************** changed: [web01] changed: [web02] TASK [Install Nginx] ********************************************* changed: [web01] changed: [web02] TASK [Ensure Nginx is started and enabled] *************** ok: [web01] ok: [web02] PLAY RECAP ************************************************** web01 : ok=3 changed=2 unreachable=0 failed=0 web02 : ok=3 changed=2 unreachable=0 failed=0

Leggi il PLAY RECAP come un semaforo

  • ok significa che il task ha verificato lo stato e non ha dovuto fare nulla: il server era già nello stato desiderato
  • changed significa che qualcosa è stato modificato
  • failed e unreachable a zero: tutto è andato come previsto

Idempotenza: esegui mille volte, stesso risultato

Eseguendo lo stesso playbook senza aver modificato nulla nel frattempo, il risultato cambia:

Prima esecuzione

First run
$ ansible-playbook -i inventory.ini install_nginx.yml PLAY [Install and configure Nginx] *************************** TASK [Gathering Facts] *************************************** ok: [web01] ok: [web02] TASK [Update apt cache] ***************************************** changed: [web01] changed: [web02] TASK [Install Nginx] ********************************************* changed: [web01] changed: [web02] TASK [Ensure Nginx is started and enabled] *************** ok: [web01] ok: [web02] PLAY RECAP ************************************************** web01 : ok=3 changed=2 unreachable=0 failed=0 web02 : ok=3 changed=2 unreachable=0 failed=0

Seconda esecuzione: idempotenza

Second run - idempotency
$ ansible-playbook -i inventory.ini install_nginx.yml PLAY [Install and configure Nginx] *************************** TASK [Gathering Facts] *************************************** ok: [web01] ok: [web02] TASK [Update apt cache] ***************************************** ok: [web01] ok: [web02] TASK [Install Nginx] ********************************************* ok: [web01] ok: [web02] TASK [Ensure Nginx is started and enabled] *************** ok: [web01] ok: [web02] PLAY RECAP ************************************************** web01 : ok=3 changed=0 unreachable=0 failed=0 web02 : ok=3 changed=0 unreachable=0 failed=0

Alla seconda esecuzione: zero changed. Ansible ha verificato che Nginx era già installato e avviato, e non ha fatto nulla. Questo è l'idempotenza.

In pratica: è possibile eseguire un playbook quante volte si vuole senza timore di rompere qualcosa. È il fondamento della configuration drift prevention: se un sysadmin modifica manualmente un server, la prossima esecuzione del playbook riporta tutto allo stato desiderato.

Struttura il progetto come un professionista

Organizzare i file correttamente fin dall'inizio evita refactoring dolorosi in seguito. Una struttura di riferimento:

nginx-project/
├── ansible.cfg          # project-level config
├── inventory.ini        # managed hosts
├── install_nginx.yml    # main playbook
├── group_vars/
│   └── webservers.yml   # group variables
├── host_vars/
│   └── web01.yml        # host-specific variables
└── roles/               # next post...

Il file ansible.cfg nella root del progetto permette di non dover specificare l'inventory ad ogni esecuzione e di configurare comportamenti di default.

[defaults]
inventory         = inventory.ini
remote_user       = ubuntu
host_key_checking = False   ; disable only in dev
stdout_callback   = yaml    ; human-readable output

[privilege_escalation]
become        = True
become_method = sudo

Con questa configurazione l'esecuzione si semplifica da:

ansible-playbook -i inventory.ini install_nginx.yml

a semplicemente:

ansible-playbook install_nginx.yml

Conclusione

Scrivere un playbook Ansible non richiede molto: poche righe di YAML bastano per installare e avviare Nginx su due server in parallelo, senza toccarli uno per uno.

Ma la cosa più importante emersa in questo post non è il playbook in sé; è quello che succede alla seconda esecuzione: nessun changed, zero modifiche. Ansible verifica che lo stato desiderato sia già raggiunto e non fa nulla. Questa è l'idempotenza, ed è il motivo per cui è possibile eseguire un playbook su 200 server in produzione senza paura.

Iscriviti a Ansible Automation Blog

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