TDD Ansible - primi passi

Se sei qui, è perché stai usando Infrastructure-as-Code (IaC), giusto? Hai mai sentito parlare di Test-Driven Development…forse starai pensando: “TDD quella roba da sviluppatori maledetti”. Ottimo! Il fatto è che quando fai IaC tu stai realmente scrivendo del codice, quindi è molto importante scrivere anche i test e TDD è probabilmente il metodo più efficace per scrivere codice di qualità con una buona copertura di test case!

Introduzione

Ansible è uno strumento agent-less di orchestrazione IT scritto in Python. Esso semplifica l’automazione e il deploy dell’infrastruttura IT, dei pacchetti software e la loro configurazione.

Le caratteristiche di Ansible sembrano davvero interessanti, ma voglio fare un passo in avanti nel mio viaggio sulla via “DevOps” e applicare il concetto di “Infrastructure as Code”. Come posso fare? Di certo non basta prendere questi file e metterli in un repository condiviso… devo trovare un modo per verificare quello che scrivo, magari usare un linter, eseguire dei “test veri” della mia automazione e, infine, dato che sto scrivendo codice (usando un DSL), perché non applicare TDD o BDD? Quali strumenti possono aiutarmi?

Il codice completo scritto durante questo tutorial e altri esempi di playbook Ansible scritti in TDD li potete trovare nel seguente repsitory: https://github.com/bombo82/tdd-ansible

Obiettivo

Installare un pacchetto di SOS Report su macchine Ubuntu e CentOS.

SOS Report è uno strumento per acquisire le informazioni di debug in formato tarball compresso che può essere utilizzato per analizzare un problema oppure inviato al supporto tecnico per ulteriori analisi.

Non mi dilungo nel raccontarvi cosa sia TDD o su come scrivere dei buoni test… per questi argomenti vi rimando ai seguenti link: Wikipedia-TDD, Agile Book - TDD, Kent Beck - Programmer Test Principles

Pre-requisiti

I pre-requisiti sono veramente minimi! Ci basta un computer con installato Docker e Python, senza alcuna esigenza particolare per il sistema operativo (ovviamente una versione attuale di esso è consigliata).

Strumenti

In questo breve tutorial verranno utilizzati i seguenti strumenti:

Molecule contiene una serie di strumenti che ci aiutano a sviluppare e testare ruoli Ansible. I ruoli Ansible possono essere testati su più sistemi operativi e distribuzioni, provider di virtualizzazione come Docker e Vagrant, framework di test come testinfra e Goss possono essere utilizzati tramite Molecule.

Testinfra permette di scrivere unit test in Python per testare lo stato attuale dei server, a prescindere che essi configurati manualmente o da strumenti di gestione come Ansible, Salt, Puppet, Chef e così via.

Per prima cosa procediamo con l’installazione degli strumenti che useremo in questo tutorial. Il comando sotto riportato usa il package manager di Python per installare tutto quello che ci serve con le relative dipendenze. L’istruzione installa l’ultima versione stabile disponibile al momento della scrittura di questo tutorial, ma siete liberi di utilizzare tali versioni, oppure rimuoverle e utilizzare l’ultima versione disponibile.

pip install --user ansible==2.8.4 testinfra==3.1.0 molecule==2.22

Creare lo scheletro del ruolo Ansible e lo scenario di test

Possiamo utilizzare due differenti approcci per creare il ruolo (escludendo a priori l’idea di crearlo manualmente).

Approccio 1: ansible-galaxy init command

In questo approccio creiamo prima il ruolo Ansible usando il comando ansible-galaxy init e poi aggiungiamo lo scenario di test mediante molecule init.

ansible-galaxy init sos-report-galaxy
cd sos-report-galaxy
molecule init scenario --scenario-name default --role-name sos-report-galaxy

Approccio 2: molecule init command

Con questo apporoccio useremo un solo comando che creerà automaticamente sia il ruolo Ansible che lo scenario di test.

molecule init role --role-name sos-report-molecule

Approccio 1 vs. Approccio 2

Una cosa che ho imparato, e ci è voluto tanto sudore, è che nell’informatica se usi 2 procedure differenti per raggiungere lo stesso scopo non otterrai mai lo stesso risultato, ma 2 risultati differenti seppur equivalenti.

Nel primo approccio, usando 2 comandi (3 contando il cd) viene screato uno scheletro più completo e che rispetta tutte le best-practices di Ansible, mentre con il secondo approccio lo scheletro non è realmente completo e alcuni file non saranno perfettamente coerenti con l’ultimo template Ansible, ma non vi è alcune problema e usiamo solo un comando.

Io sono uno sviluppatore veramente pigro e normalmente uso il secondo approccio e poi mi lamento della mancanza di alcune cartelle o alcuni file che sarebbero stati presenti se avessi usato il primo approccio :-(

Iniziamo

Dato che useremo TDD è necessario configurare e impostare l’ambiente di test e solo in seguito possiamo procedere scrivendo del codice.

1. Configurazione test framework e molecule

Innanzitutto, dobbiamo verificare ed eventualmente ridefinire il file di configurazione predefinito di Molecule. Esso viene generato dal comando molecule init e contiene il driver, le piattaforme, i verifier e la sequenza di test (se non vogliamo utilizzare quella predefinita) per il nostro scenario.


---
dependency:
  name: galaxy
driver:
  name: docker
lint:
  name: yamllint
platforms:
  - name: instance
    image: centos:7
provisioner:
  name: ansible
  lint:
    name: ansible-lint
verifier:
  name: testinfra
  lint:
    name: flake8

Sopra trovate il file <role_name>/molecule/molecule.yml creato dal comando molecule init ed è quasi perfetto per il nostro scopo… noi vorremmo creare il playbook compatibile sia per CentOS sia per Ubuntu, ma applicando lo spirito TDD direi che possiamo lasciare inalterato il file, iniziando con CentOS e aggiungere Ubuntu in un secondo momento.

2. Prima esecuzione dei test

Ora molecule è configurato e il comando molecule init ha creato un test per noi… il test è molto semplice, esso verrà eseguito tramite PyTest e grazie a TestInfra verifica l’esistenza del file /etc/hosts (file che in ogni distribuzione linux è sempre presente).

I test sono eseguiti tramite il comando molecule test che, prima di eseguire i test veri e propri, si preoccupa di scaricare le immagini Docker necessarie, eseguire alcuni linter per verificare la correttezza sintattica e formale del codice e altri controlli sul nostro playbook Ansible e.g idempotenza e side’effects.

Eccoci al momento della verità e come dice una vecchia canzone… La verità mi fa male, lo sai! A differenza da quello che uno si aspetta l’esecuzione del test fallisce, o meglio a fallire è il linter del playbook Ansible. L’errore è abbastanza banale e ci segnala che il file <role_name>/meta/main.yml contiene alcuni valori di default che andrebbero modificati e l’indicazione delle platforms è mancante. Potete correggere i metadata oppure eliminare il file… questo file è indispensabile solo quando il ruolo viene pubblicato e condiviso su Ansible Galaxy o un altro repository per i ruoli.

Sistemato il file <role_name>/meta/main.yml tutti i test saranno verdi e possiamo procede a scrivere il codice per installare il pacchetto sosreport.

3. Assicurarsi che il pacchetto sos-report sia installato

Scriviamo il test che verifica la presenza del pacchetto sosreport, quindi editiamo il file <role_name>/molecule/default/test_default.py, rimuovendo il test case di default e inseriamo il seguente codice:

def test_sos_report_package(host):
    assert host.package('sosreport').is_installed

Dopo aver lanciato il test con il comando molecule test e verificato che esso fallisce, possiamo procedere scrivendo la parte di playbook Ansible che installa il pacchetto sosreport. Editate il file <role_name>/task/main.yml e inserite il seguente codice:


---
- name: ensure sosreport is installed
  yum:
    name: sosreport
    state: present

A questo punto il test sarà verde e abbiamo terminato la parte d’installazione del pacchetto. Abbiamo scritto così poco codice che non è necessario fare refactor, anzi provare a fare del refactoring ora significa fare speculazioni e azzardi su come potrebbe evolversi il nostro playbook, quindi quello che abbiamo scritto è perfetto.

4. Aggiungiamo il supporto a Ubuntu

In generale, aggiugnere il supporto per una distribuzione significa fare 3 cose:

  1. aggiungere la distribuzione desiderata nella sezione platforms del file <role_name>/meta/main.yml
  2. aggiugnere la distribuzione da testare nella sezione platforms del file <role_name>/molecule/default/molecule.yml
  3. modificare l’implementazione, ed eventualmente i test, per quanto riguarda le parti specifiche per la distribuzione

Procediamo con ordine e modifichiamo la sezione platforms del file <role_name>/meta/main.yml ed eseguimo nuovamente i test.

  platforms:
    - name: centos
      versions: 7
    - name: ubuntu
      versions: 18.04

Perfetto, i test passano con successo! quindi possiamo procedere modificando il file molecule.yml in modo da testare il playbook sia con CentOS sia con Ubuntu. Modifichiamo come segue la sezione platforms del file <role_name>/molecule/default/molecule.yml ed eseguiamo nuovamente i test.

platforms:
  - name: centos7
    image: centos:7
  - name: ubuntu1804
    image: ubuntu:18.04

Questa volta il comando molecule test fallirà! Il motivo è molto semplice… la parte di automazione che installa il pacchetto sosreport dipende dalla distribuzione target, per il semplice motivo che il nome del pacchetto è diverso in CentOS e in Ubuntu :-(

A questo punto dobbiamo modificare l’implementazione, prima dei test e poi dell’automazione al fine di gestire la differenza tra i nomi. La soluzione che normalmente adotto nei test è creare una mappa distribuzione -> nome contenente i valori di nostro interesse.

def _get_sosreport_package_name(distribution):
    return {
        "ubuntu": "sosreport",
        "centos": "sos"
    }.get(distribution)


def test_sos_report_is_installed(host):
    package_name = _get_sosreport_package_name(host.system_info.distribution)
    package = host.package(package_name)

    assert package.is_installed

Per la parte di Ansible invece ci sono più soluzioni possibili. La più semplice prevede di duplicare il blocco di codice per l’installazione del pacchetto ed eseguire in modo condizionale il blocco per CentOS o per Ubuntu.


---
- name: ensure sosreport is installed (RedHat-based)
  yum:
    name: sos
    state: present
  when: ansible_os_family == 'RedHat'

- name: ensure sosreport is installed (Debian-based)
  apt:
    name: sosreport
    state: present
  when: ansible_os_family == 'Debian'

5. Code Refactor

Il codice scritto durante questo tutorial è abbastanza semplice e soprattutto sono pochissime righe. All’interno del playbook abbiamo 2 blocchi differenti di codice molto simile tra loro e che verranno eseguiti in modo condizionale in base alla distribuzione target. Un possibile refactor è quello di rimuovere i blocchi condizionali in favore di spezzare il playbook in più file da includere in base alla distribuzione. Visto la semplicità e brevità del playbook non faccio questo refactor, ma potete vedere un esempio nel playbook lamp-live che trovate sempre in questo repository.

https://github.com/bombo82/tdd-ansible