Ansible (V): testeando roles con Molecule

En el post anterior sobre Ansible ya teníamos un rol plenamente funcional, pero siempre podemos mejorarlo un poco más.

Imaginemos que realizamos cambios en nuestro rol y queremos probarlos a medida que seguimos desarrollándolo y que en una de las pruebas omitimos sin querer algun test y al aplicarno en nuestros servidores rompemos algo. Todo el desarrollo realizado anteriormente podría provocar pérdidas de datos o de servicio debido a un error humano.

¿Cómo podemos evitarlo? Podemos añadir a nuestro rol, Molecule.

Nota del autor: Molecule ha sido completamente reescrito en los últimos tiempos y el contenido de este post no es válido en las últimas versiones de la herramienta. Por lo tanto, este artículo queda con carácter consultivo y quien desee ver información actualizada, puede hacerlo en éste otro.

Introducción

Molecule es un programa escrito en python que nos permite probar cada cambio que realicemos en nuestro rol contra una instancia que se crea y se destruye para cada ocasión. De esta forma podremos probar cada cambio que realicemos en nuestros roles de Ansible y asegurarnos cual será su comportamiento cuando los despleguemos en los servidores reales, evitando molestias y sustos.

Por defecto, Molecule utiliza contenedores para levantar entornos contra los que hacer pruebas por lo que deberemos instalar el demonio de Docker, pero se puede utilizar otras tecnologías de virtualización como KVM o proveedores de Cloud Pública como AWS o Azure.

Si realizamos molecule --help veremos todos los comandos que podremos utilizar. No vamos a citarlos todos pero si los más importantes:

  • init : Nos creará la estructura básica de un rol y preparará los ficheros usados por Molecule para poder generar de forma automática las máquinas virtuales o los contenedores en los que realizar las pruebas.

  • create: Utiliza el provisioner (el encargado de generar la instancia) para generar una plantilla y una instancia compatible con el uso de Ansible.

  • converge: Realizará una serie de pasos que podemos definir para añadir características propias a nuestras plantillas. Por ejemplo, si necesitamos que nuestro rol utilice una base de datos particular, podemos crearla en este paso.

  • test: Realizará paso a paso todos los tests posibles: desde sintaxis hasta los tests que programemos con pytest o testinfra que añadirán más comprobaciones a todo el proceso.

  • destroy: Destruye las instancias creadas anteriormente y permite empezar de cero todo el proceso.


Añadimos Molecule a nuestro rol

A día de hoy no podemos añadir Molecule a un rol ya creado previamente de forma automática. Pero se puede hacer un pequeño truco, que consiste en renombrar temporalmente nuestro rol, crearlo con molecule y luego copiar el contenido original a donde se encuentra el rol. Por ejemplo:

mv backupninja backupninja_old
molecule init role backupninja

cp -R backupninja_old/* backupninja/

Ahora dentro de la carpeta de backupninja tendremos una nueva carpeta que se llama molecule. Vamos a inspeccionarla:

ls -laRt
.:
total 12
drwxr-xr-x 11 tangelov tangelov 4096 feb 28 10:40 ..
drwxr-xr-x  4 tangelov tangelov 4096 feb 28 10:40 default
drwxr-xr-x  3 tangelov tangelov 4096 feb 27 15:04 .

./default:
total 36
drwxr-xr-x 4 tangelov tangelov 4096 feb 28 10:40 .
-rw-rw-r-- 1 tangelov tangelov 3195 feb 28 10:20 prepare.yml
-rw-rw-r-- 1 tangelov tangelov  687 feb 27 15:06 molecule.yml
drwxr-xr-x 3 tangelov tangelov 4096 feb 27 15:04 ..
drwxr-xr-x 3 tangelov tangelov 4096 feb 27 13:42 tests
-rw-r--r-- 1 tangelov tangelov  614 ene 11  2020 converge.yml
drwxr-xr-x 2 tangelov tangelov 4096 ene  7  2020 files
-rw-r--r-- 1 tangelov tangelov 1153 ene  7  2020 Dockerfile.j2
-rw-r--r-- 1 tangelov tangelov  369 ene  7  2020 INSTALL.rst

./default/tests:
total 16
drwxr-xr-x 4 tangelov tangelov 4096 feb 28 10:40 ..
drwxr-xr-x 2 tangelov tangelov 4096 feb 27 15:03 __pycache__
drwxr-xr-x 3 tangelov tangelov 4096 feb 27 13:42 .
-rw-r--r-- 1 tangelov tangelov  523 ene  7  2020 test_default.py

Primeros pasos con Molecule

Ahora nos movemos a la carpeta molecule/default dentro la carpeta de nuestro rol. Vamos a centrarnos en tres ficheros:

  • molecule.yml : es el fichero principal para configurar cómo se va a comporar molecule en nuestro rol.

  • prepare.yml: es el fichero principal encargado de ejecutar los pasos que preparan las instancias para poder ejecutar luego nuestros roles dentro de molecule.

  • converge.yml: es el fichero encargado de ejecutar el rol dentro de molecule.

  • Dockerfile.j2: Por defecto, Molecule utiliza Docker para crear las instancias, salvo que indiquemos que utilice otro provider siempre tendremos este fichero y es el encargado de hacer que nuestro contenedor sea compatible con Ansible.

Simplemente si hacemos molecule create veremos algo similar a esto:

--> Validating schema ~/backupninja/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix
    
└── default
    ├── create
    └── prepare
    
--> Scenario: 'default'
--> Action: 'create'
    
    PLAY [Create] ******************************************************************
    
    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item=None) 
    skipping: [localhost]
    
    TASK [Create Dockerfiles from image names] *************************************
    changed: [localhost] => (item=None)
    changed: [localhost]
    
    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost]
    
    TASK [Build an Ansible compatible image] ***************************************
    changed: [localhost] => (item=None)
    changed: [localhost]
    
    TASK [Create docker network(s)] ************************************************
    skipping: [localhost]
    
    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]
    
    TASK [Wait for instance(s) creation to complete] *******************************
    changed: [localhost] => (item=None)
    changed: [localhost]
    
    PLAY RECAP *********************************************************************
    localhost                  : ok=5    changed=4    unreachable=0    failed=0

Vamos a editar el fichero molecule.yml en molecule/default para generar dos contenedores uno con Debian y otro con Centos sobre los que probar nuestro rol. Eso se realiza añadiendo diferentes imágenes en la zona de platforms, quedando así el fichero:

---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: debian
    image: jrei/systemd-debian
    command: /lib/systemd/systemd
    privileged: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
  - name: ubuntu
    image: jrei/systemd-ubuntu
    command: /lib/systemd/systemd
    privileged: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
  - name: centos
    image: centos/systemd
    command: /sbin/init
    privileged: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
provisioner:
  name: ansible
scenario:
  name: default
  test_secuence:
    - destroy
    - create
    - converge
    - lint
    - verify
verifier:
  name: testinfra

Si ahora ejecutamos molecule create y hacemos docker ps podremos ver nuestros contenedores corriendo:

CONTAINER ID        IMAGE                                COMMAND                  CREATED             STATUS              PORTS               NAMES
f2532c299664        molecule_local/centos/systemd        "/sbin/init"             2 minutes ago       Up About a minute                       centos
4656b5096126        molecule_local/jrei/systemd-ubuntu   "/lib/systemd/systemd"   2 minutes ago       Up About a minute                       ubuntu
b7473fafb19a        molecule_local/jrei/systemd-debian   "/lib/systemd/systemd"   2 minutes ago       Up About a minute                       debian

Hemos usado contenedores privilegiados para correr los test y controlar systemd al estilo de una máquina virtual clásica.

Configuraciones extra

Nuestro siguiente paso es realizar molecule converge. Este paso desplegará nuestro rol en los contenedores que hemos creado y aplicará todas las tareas que hemos creado anteriormente.

Para no hacer el post demasiado largo, vamos a suponer que el despliegue de nuestro rol no falla. Sin embargo, antes de ejecutar converge necesitamos crear un entorno que simule lo que tenemos en nuestro servidor creando una ruta con ficheros sueltos en /var/www y una base de datos corriendo en ella.

Ahora creamos un fichero llamado ~/backupninja/molecule/default/prepare.yml. Este fichero contiene los pasos previos que utiliza molecule para dejar el entorno preparado para probar nuestros roles y vamos a añadir algunas acciones para simular nuestros servidores.

Como el fichero es muy largo, lo referencio aquí.

Al ejecutar molecule convergeprimero se ejecutará nuestro prepare.yml y después se ejecutará playbook.yml que lanzará nuestro rol propiamente dicho.

Si ahora nos metemos dentro de las instancias con molecule login –host $nombre podremos ver que nuestros backups se han creado, pero lo ideal es que Molecule lo compruebe por nosotros, así que vamos a editar el fichero backupninja/molecule/default/tests/test_default.py y a añadir el siguiente código.:

import os
import testinfra.utils.ansible_runner
import datetime

testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')


def test_backupninja_dump_exists(host):
    """Checking if the file generated in default test last step exists"""
    with host.sudo():
        bckdate = datetime.datetime.utcnow().strftime("%Y.%m.%d")
        bckpath = '/def_test/' + 'full-test' + '-' + bckdate + '.tgz'

        f = host.file(bckpath)
        assert f.exists

Estos test, escritos en Python y utilizando testinfra, nos permiten realizar ciertas comprobaciones de forma automática. Este código, simplemente comprueba si se ha generado nuestro backup al realizar molecule test, con el formato que hemos aplicado en el script de backupninja.

Aquí podremos ver el proceso entero utilizando molecule test (el gif dura cinco minutos así que… un poco de paciencia :D ):

molecule-gif

El gif muestra como se realiza la creación de las instancias, el despliegue del entorno de pruebas y nuestro rol en él y finalmente si éste es idempotente y si el test automático de Molecule ha pasado o no.

En el pasado introduje un error al crear el fichero playbook.yml en lugar de prepare.yml. Esto causaba problemas de idempotencia en los tests de molecule pero ya está solucionado.

Con esto, terminamos el post. En el siguiente añadiremos algo más de complejidad a los tests y explicaremos más en profundidad los pasos que realiza molecule.

Un saludo y espero que os haya resultado interesante.

Documentación

Revisado a 01/05/2023