Primi passi con Ansible – Parte 1

3

Ansible è un software open source utilizzato per l’automazione di parchi macchine. Può gestire l’installazione e la configurazione di qualsiasi componente del sistema così come la definizione di procedure di deploy automatizzate.

Creato nel 2012 da Michael DeHaan, già autore di Cobbler e co-autore di Func, è balzato subito tra i sistemi di configuration management più utilizzati grazie ad alcuni punti focali su cui è stato realizzato:

  • Non richiede l’installazione di un agent sulle macchine da gestire, basta un accesso ssh e la presenza di Python 2 (versioni 2.6 o 2.7), oppure di Python 3 (versioni 3.5 o successive), generalmente disponibili su qualsiasi OS;
  • Non richiede conoscenza di linguaggi di programmazione, la sintassi YAML con cui si scrivono le istruzioni è di semplice lettura e comprensione;
  • E’ scritto in python, il che lo rende multipiattaforma e non dipendente dalla distribuzione, leggero e performante;

Ha preso così tanto piede che è stata fondata la Ansible Inc. per commercializzare il supporto e sponsorizzare la soluzione e, successivamente, l’azienda è stata acquistata da Red Hat a fine 2015.

In questo primo articolo vedremo come installare Ansible, iniziando ad utilizzarlo per eseguire operazioni di base.

Terminologia

Per prima cosa, andiamo ad analizzare alcuni dei termini utilizzati dal software, così da comprendere meglio le sue componenti:

  • Inventario: Si tratta della lista di macchine sulle quali Ansible può operare. Opzionalmente tali macchine possono essere raggruppate in modo da avere più gruppi di host. L’inventario può essere scritto sia in formato ini che in yaml, ed il path in cui risiede di default è /etc/ansible/hosts, ma è chiaramente possibile effettuare l’override di questa posizione, passando a riga di comando locazioni differenti;
  • Moduli: i moduli possono essere visti come i comandi che eseguiamo sulle macchine. Esistono moduli per l’installazione e la rimozione di pacchetti, deployment di file (o generazione del loro contenuto), gestione dei servizi e tanto altro. La lista completa è davvero corposa;
  • Task: i task sono le operazioni che, di fatto, eseguiamo sulle macchine. Possono essere visti come la serie di comandi, in sequenza, che verranno lanciati sulla macchina. Un task è nella sostanza il richiamo di un modulo con particolari opzioni.
  • Handler: le operazioni associate agli handler si differenziano dai task poiché non vengono sempre eseguite, ma vengono richiamate come reazione ad un evento di un task. Un banale esempio è quello di avviare un servizio dopo aver installato il software necessario, o riavviarlo dopo la modifica del suo file di configurazione.
  • Playbook: i playbook sono una lista di task, handler e delle relative variabili legati a particolari gruppi di macchine in inventario. Vengono utilizzati per avere il “manuale di istruzioni” delle operazioni che Ansible dovrà eseguire.
  • Ruoli: i ruoli sono dei componenti dei playbook che raggruppano operazioni legate tra di loro con uno scopo specifico. Queste vengono di solito unificate per avere più riusabilità delle stesse. Ad esempio, l’installazione, la configurazione e la gestione del servizio NTP possono essere raggruppate in un ruolo, avendo la possibilità di riutilizzare questo ruolo in diversi Playbook.

Tutto il codice Ansible (e, volendo, anche l’inventario) è scritto in sintassi YAML, che permette una semplice scrittura e lettura del codice con qualisasi editor di testo. Il formato YAML ha precise regole: ad esempio la tabulazione non è accettata, in favore di un’indentazione a 2 spazi.

Installazione

Diverse sono le opzioni per l’installazione ed in molti casi i repository ufficiali delle nostre distribuzioni preferite già contengono i pacchetti necessari per installare Ansible:

RHEL/CentOS/Fedora
Su questi sistemi l’installazione è molto semplice. Se avete la versione 7 trovate gli rpm sul canale Extra, mentre per le versioni 6 degli OS potete aggiungere il repository EPEL ed avrete tutto il necessario. Dopodichè basterà installare il tutto:

controller# yum install ansible

Debian/Ubuntu
Ansible fornisce anche un repository PPA contenete i deb per una veloce installazione su Debian/Ubuntu. Basterà lanciare i seguenti comandi:

controller$ sudo apt-get update
controller$ sudo apt-get install software-properties-common
controller$ sudo apt-add-repository ppa:ansible/ansible
controller$ sudo apt-get update
controller$ sudo apt-get install ansible

Altre opzioni
Alternativamente, nel caso non siate soddisfatti dei pacchetti o vogliate installare l’ultimissima versione disponibile, la procedura di installazione dal repository GitHub del progetto è molto semplice:

controller$ git clone https://github.com/ansible/ansible.git --recursive
controller$ cd ./ansible
controller$ source ./hacking/env-setup
controller$ sudo easy_install pip
controller$ sudo pip install -r ./requirements.txt

Per l’installazione su altri OS o per l’aggiornamento, la documentazione ufficiale è molto esaustiva.

Prepariamo l’ambiente

Una volta installato, la prima cosa da fare è andare a creare un Inventario, ovvero andare a censire gli host che saranno gestiti da Ansible.

A differenza di altri software di automazione, come ad esempio Puppet, Ansible necessita esclusivamente di una connessione SSH tra la macchina “controller” (ad esempio il nostro laptop o un server centralizzato installato a tale scopo) ed i nodi da controllare; l’”agent” della macchina controllata sarà Python (normalmente già presente).

Di default Ansible utilizza sftp per scambiare le informazioni, ma se le vostre policy non lo consentono potete configurare l’uso di scp molto facilmente.

Per prima cosa, quindi, dobbiamo assicurarci che l’host sul quale girerà Ansible possa accedere via ssh senza password ai sistemi in inventario. Generiamo quindi la coppia di chiavi sull’host controller:

controller$ ssh-keygen -t rsa
...

dopodichè nasterà copiare la chiave su tutti i nodi interessati:

controller$ scp ~/.ssh/id_rsa.pub \
> utente@host1:~/.ssh/authorized_keys
Password: ...
controller$ ssh utente@host1 "chmod 600 ~/.ssh/authorized_keys"
Password: ...
controller$ ssh utente@host1
host1$ exit
controller$
...

Come vedete abbiamo abilitato l’accesso via ssh senza password dalla macchina controller ai vari host, con un utente non privilegiato. Adesso dobbiamo dire ai vari nodi che l’utente in questione (per il quale dovrà essere scelta una password forte) è abilitato a lanciare comandi amministrativi su questo sistema:

host1# echo "utente ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers

Questo permetterà nel nostro inventario di impostare le seguenti variabili:

ansible_become: true
ansible_become_method: sudo

Così da assicurare la connessione con utenti non privilegiati e permettere ad Ansible di fare comunque le operazioni sulle macchine utilizzando sudo.

Il censimento degli host nell’inventario avviene popolando il file /etc/ansible/hosts, anche se è possibile utilizzare diversi file (magari per separare meglio la propria infrastruttura) e passarli come opzione ai comandi Ansible.

L’Inventario può essere scritto in due formati differenti; il formato INI è estremamente semplice, ed appare grosso modo così:

host1.example

[webservers]
web1.example 
web2.example
web3.example 

[dbservers] 
db1.example 
db2.example 
db3.example

Come si può notare host1 è fuori dai eventuali gruppi, gli host web1, web2 e web3 sono raggruppati sotto il gruppo webservers e così via.

L’inventario può essere definito anche in formato YAML. A volte si utilizza questo formato per la sua capacità di essere interoperabile con gli inventari definiti su Ansible Tower (o AWX, se preferite la versione community), un’interfaccia per la gestione di Ansible via web. Un altro motivo per scegliere il formato YAML è dato dalla complessità delle variabili che si possono inserire in esso, e per la sua uniformità con quella che sarà la sintassi utilizzata nelle altre componenti (Playbook e Ruoli).

Lo stesso inventario scritto con la sintassi YAML appare in questo modo

all:
  hosts:
    host1:
  vars:
    ansible_user: utente
    ansible_become: true
    ansible_become_method: sudo
  children:
    webservers:
      hosts:
        web1:
        web2:
    dbservers:
      hosts:
        db1:
        db2:
        db3:

Come notiamo, seppur questa sintassi introduce keyword non presenti nel formato ini (quali hosts, children, etc.), è comunque molto parlante, non richiede particolari nozioni di programmazione e mostra, anche visivamente, il raggruppamento dei vari host.

Usiamo i moduli

Finita la creazione dell’inventario, possiamo procedere a provare subito l’esecuzione diretta di un modulo su una o più macchine.

Uno dei primi moduli che normalmente vengono utilizzati per testare l’ambiente è il modulo ping. Questo modulo verifica il collegamento e la versione Python dell’host, e risponde pong nel caso tutto sia funzionante.

Vediamo quindi se abbiamo configurato bene l’inventario e caricato correttamente la chiave sulla macchina web1

controller$ ansible web1 -m ping
web1 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

Ottimo, la risposta conferma come la connessione sia avvenuta con successo. Nel l’inventario sia in un file dedicato, è possibile passarlo come opzione a riga di comando (-i):

controller$ ansible -i inventory.yaml web1 -m ping
web1 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

Una buona organizzazione dell’inventory permetterà di essere più selettivi nell’esecuzione dei moduli, garantendo l’esecuzione dello stesso task su un intero gruppo di macchine. Ad esempio, per verificare tutte e tre le macchine DB presenti nel nostro inventario, sarà possibile lanciare il modulo ping sul gruppo dbservers:

controller$ ansible dbservers -m ping
db1 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
db2 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
db3 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

Passando, invece di un gruppo, la keyword all potremo eseguire il modulo su tutti gli host in inventario:

controller$ ansible all -m ping
host1 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
web1 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
web2 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
...

Una volta verificato il corretto funzionamento del tutto possiamo già riscontrare i benefici dell’avere Ansible integrato nell’infrastruttura. Ad esempio, nel caso abbiamo installato il webserver nginx sui nostri webserver e necessitiamo di sapere lo stato di questo servizio sulle varie macchine, possiamo finalmente dimenticare di collegarci manualmente su tutte le macchine (o scrivere script ad hoc che lo facciano per noi) e sfruttare Ansible per eseguire il comando su tutti gli host desiderati e fornirci il risultato:

controller$ ansible webservers -a "systemctl status nginx"
web1 | SUCCESS | rc=0 >>
● nginx.service - The nginx HTTP and reverse proxy server
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled)
   Active: active (running) since Wed 2017-11-08 11:00:28 CET; 2 months 3 days ago
 Main PID: 8173 (nginx)
   CGroup: /system.slice/nginx.service
           ├─8173 nginx: master process /usr/sbin/ngin
           ├─8174 nginx: worker proces
           ├─8175 nginx: worker proces
           ├─8176 nginx: worker proces
           └─8177 nginx: worker proces

Warning: Journal has been rotated since unit was started. Log output is incomplete or unavailable.
web2 | SUCCESS | rc=0 >>
● nginx.service - The nginx HTTP and reverse proxy server
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled)
   Active: active (running) since Wed 2017-11-08 11:00:33 CET; 2 months 3 days ago
 Main PID: 31567 (nginx)
   CGroup: /system.slice/nginx.service
           ├─30765 nginx: worker process is shutting dow
           ├─31567 nginx: master process /usr/sbin/ngin
           ├─31568 nginx: worker proces
           ├─31569 nginx: worker proces
           ├─31570 nginx: worker proces
           └─31571 nginx: worker proces
controller$

Questa è la reale comodità di Ansible: che siano due o duecento macchine, se queste sono censite in inventario e con la chiave ssh inserita, l’effort per recuperare informazioni è assolutamente lo stesso.

Ovviamente utilizzando questo metodo possiamo fare praticamente tutto sulle nostre macchine. Assicuriamoci ad esempio che il servizio telegraf sia attivo su tutti i nostri server database:

controller$ ansible dbservers -m service -a 'name=telegraf enabled=yes state=started'
db1 | SUCCESS => {
    "changed": false,
    "enabled": true,
    "name": "telegraf",
    "state": "started"
}
db2 | SUCCESS => {
    "changed": false,
    "enabled": true,
    "name": "telegraf",
    "state": "started"
}
db3 | CHANGED => {
    "changed": true,
    "enabled": true,
    "name": "telegraf",
    "state": "started"
}

Abbiamo per questa attività utilizzato il modulo service che permette di controllare lo stato dei servizi sui sistemi, dicendo di verificare che il servizio di nome telegraf sia in stato enabled (quindi che si avvii automaticamente al boot del sistema) e started (quindi attualmente attivo). Su uno dei tre nodi (db3) il servizio risultava fermo, quindi è stato attivato. Potete notare lo stato CHANGED nell’output, che indica il fatto che il sistema non era conforme alle richieste e che una o più modifiche sono state fatte per portarlo allo stato richiesto dal modulo.

Conclusioni

Già questo primo assaggio di Ansible ci permette di sbirciare quelli che saranno i vantaggi dell’utilizzare il prodotto nella propria infrastruttura. Avere la possibilità di operare su interi ambienti senza doversi preoccupare di quelle che sono realmente le macchine nell’inventario è già di per se un passo avanti molto significativo.

Nelle prossime puntate vedremo come l’uso di Playbook e Ruoli permettano di definire stati completi delle macchine, andando ad avere dei file di “codice” che rappresentano lo stato degli host e rendendo qualsiasi ambiente estremamente replicabile in tempi brevissimi.

Restate sintonizzati, quindi, ne vedremo delle belle.