Molecule: desarrollo TDD en Ansible

A estas alturas no es nada nuevo decir que el desarrollo con TDD aporta grandes ventajas a nuestros proyectos (mayor calidad del código, orientado a necesidades, simplicidad, menor número de errores…).

Esta metodología, pensada por y para programadores, gracias a su filosofía devops, también es válida para los ingenieros de sistemas, que tenemos la necesidad de generar “código”, ya sea en un lenguaje de programación o un fichero YAML para configurar Ansible. Y para ello, Molecule viene como anillo al dedo.

Antes de entrar en materia, dejemos un par de conceptos claros:

  • Por si vives en una cueva, Ansible es un gestor de configuraciones que, además, permite el despliegue masivo de las mismas. Si no lo has usado nunca, su documentación es muy exhaustiva y a partir de aquí podrás dar los primeros pasos.
  • ¿Qué es un rol y qué es un playbook? Comparándolo con la programación, un rol es una función y un playbook el programa que la ejecuta. Quedará más claro en el siguiente ejemplo en Python.

Esto sería un ejemplo de rol:

def hola_mundo():
    print("Hola Mundo!")

Y esto sería uno de playbook:

def hola_mundo():
    print("Hola Mundo!")
 
if __name__ == '__main__':
    hola_mundo()

Dicho de otro modo, un playbook es el pegamento que une uno o más roles de Ansible. Teniendo esto claro, vayamos a la parte interesante: el desarrollo con TDD.

Molecule es un framework de desarrollo de roles de Ansible hecho en Python (¡cómo no!) y desarrollado por Metacloud, la nube que vende Cisco.

Provee de playbooks que, mediante la virtualización, testean desde las funcionalidades del rol hasta la sintaxis de éste, pasando por buenas prácticas y tests unitarios de infraestructura.

Los pasos que sigue Molecule cuando ejecuta un test son los siguientes:

  • Destrucción: se encarga de limpiar las instancias que hubiesen antes de empezar con los tests.
  • Dependencia: en esta fase se añaden los roles ya desarrollados sin los cuales no podrían funcionar los tests.
  • Sintaxis: una vez llegado a este punto se realiza la comprobación de la sintaxis de los ficheros de YAML (el formato que usa Ansible) con yamllint y de Python (el formato de los tests) con flake8.
  • Creación: la creación de las instancias es uno de los pasos más importantes. Por defecto se lanzan contenedores docker, pero se pueden usar muchos más drivers: Openstack, EC2, GCE, LXC, LXD y Vagrant. Es probable que varios de ellos necesiten que se instale un módulo que permita a Molecule controlarlos.
  • Convergencia: en esta etapa se ejecuta el rol que queremos probar en las instancias creadas en el paso anterior.
  • Idempotencia: la idempotencia es la cualidad de multiplicarse por sí mismo y seguir obteniendo el mismo elemento. En este contexto, se ejecuta el rol que testeamos dos veces y que la segunda no se ejecute. Uno de los puntos fuertes de Ansible es que no ejecuta una tarea si esta no necesita ser ejecutada. Por lo tanto, si ejecuta dos veces la misma tarea es que el rol no está bien hecho (aunque hay excepciones, no todos los módulos son idempotentes).
  • Lint: llegando hacia el final, se ejecuta ansible-lint, un programa que comprueba que se siguen las buenas prácticas de Ansible.
  • Verificación: por último, se ejecutan los tests unitarios. Por defecto se usa testinfra, un plugin de Pytest, uno de los frameworks de tests unitarios de Python más usados, aunque también soporta Goss, un equivalente en el lenguaje Go.

Ahora recordemos cuál sería el ciclo de vida de TDD:

  • Elegir un requisito.
  • Programar un test.
  • Verificar que el test no se pasa.
  • Programar la implementación para que pase el test.
  • Ejecutar los tests.
  • Refactorizar.

Una vez refrescados los términos, vamos a imaginar que queremos instalar Emacs, el “fantástico” editor de texto.

Veamos cómo sería el ciclo en el caso de la instalación de este editor:

  • Elegir un requisito: instalar Emacs.
  • Codificar test: comprobar que el programa está instalado.
  • Verificar que la prueba falla.
  • Hacer un rol que instale emacs.
  • Ejecutar las pruebas automatizadas.
  • Refactorizar.

Pongámonos a ello:

molecule init role --role-name emacs
: --> Initializing new role emacs...
: Initialized role in ~/Trabajo/Paradigma/articulos/emacs successfully.

Este orden actúa como un envoltorio de ansible-galaxy e igual que este nos crea un esqueleto de directorios y ficheros tal que así:

tree emacs
emacs
├── defaults
│   └── main.yml
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── molecule
│   └── default
│       ├── create.yml
│       ├── destroy.yml
│       ├── Dockerfile.j2
│       ├── INSTALL.rst
│       ├── molecule.yml
│       ├── playbook.yml
│       ├── prepare.yml
│       └── tests
│           ├── test_default.py
│           └── test_default.pyc
├── README.md
├── tasks
│   └── main.yml
└── vars
    └── main.yml
 
8 directories, 15 files

Todos los directorios son los habituales en Ansible, menos el directorio molecule, que es en el que ocurrirá toda la magia.

Como ya tenemos el esqueleto de nuestro rol y tenemos el requisito escogido (instalar emacs), vamos a programar la prueba que decidirá que emacs está instalado correctamente.

import os
 
import testinfra.utils.ansible_runner
 
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')
 
 
def test_emacs_binary(host):
    f = host.file('/usr/bin/emacs')
 
    assert f.exists

Esta primera prueba comprobará que existe un fichero llamado emacs en a ruta /usr/local/bin/, que es propiedad del usuario y grupo root y que tiene permisos de ejecución para todos los usuarios. Comprobemos que la prueba falla:

Podemos ver que hay muchas secciones llamadas Action, que son las mismas órdenes que ya hemos explicado antes. Por lo tanto, podemos ver que el parámetro test lo que en realidad hace es ejecutar todos los demás parámetros. No entraremos en mucho más detalle, ya que la salida es bastante clara.

La parte que nos interesa es la última: verify. Es la parte en la que se ejecuta el test de testinfra, que no hemos pasado. Concretamente, vemos que el fichero /usr/bin/emacs no existe.

Ahora programaremos el código necesario para que pasemos esta prueba. En emacs/tasks/main.yml escribimos:

- name: Instala emacs, el fantástico sistema operativo
  become: yes
  package:
    name: "{{ item }}"
    state: present
  with_items:
    - epel-release # necesario para instalar emacs
    - emacs

Re-ejecutamos el test y veremos los siguiente:

¡Y ya hemos pasado los tests! Ahora quedaría refactorizar. Siendo como es un rol bien simple, poco hay que añadir.

Por mostrar un poco más la utilidad de testinfra y sus módulos, nos aseguraremos de que el binario pertenezca al usuario y grupo root, que tenga permisos de ejecución para todo el mundo y que ejecute en emac lisp un “Hola mundo!”.

Como sólo queremos que se ejecuten los tests y los contenedores de docker están levantados (como se puede ver con un docker ps), con ejecutar molecule verify después de añadir los tests tendremos suficiente y será mucho más rápido.

import os
 
import testinfra.utils.ansible_runner
 
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')
 
 
def test_hosts_file(host):
    f = host.file('/usr/bin/emacs')
 
    assert f.exists
    assert f.user == 'root'
    assert f.group == 'root'
    assert oct(f.mode) == '0777'
    assert host.check_output("emacs -Q --batch --eval '(message \"Hola Mundo" +
                             "!\")' 2>&1") == 'Hola Mundo!'


Y con esto ya hemos cumplido el ciclo de TDD. Solo queda empezar a usarlo. ¿Te animas?

Soy Ingeniero de Sistemas por la gracia de Paradigma, mis estudios reglados quedaron en la Formación Profesional, ya que soy más de aprender por mi cuenta. Me apasiona la tecnología en una única variante, que es la del software libre. Cuando no estoy trasteando servidores, dedico algo de tiempo a programar en Python y demasiado tiempo a Emacs.

Ver toda la actividad de Álex Pérez-Pujol

Escribe un comentario