Implementa tests en tus roles de Ansible con Molecule

Hoy en dia nadie concibe (o nadie debería concebir, mejor dicho) empezar un proyecto de programación sin sus correspondientes tests. Tranquilidad en el desarrollo, código más robusto, anticiparnos a los errores, etc. son algunas de las maravillas que nos ofrece el mundo del testing, así que si tiene tantas ventajas... ¿porqué no íbamos a aplicarlo también a otros ámbitos que no sean exclusivamente los de la programación?

Ansible es una de esas herramientas indispensables para cualquiera que administre servidores, automatizando la instalación de nuevos servicios, el despliegue de aplicaciones, configuraciones, etc. permitiendo que llevemos a cabo todas esas tareas de forma sencilla, rápida y automatizada ya sea en un servidor, en diez o en miles de ellos mediante la creación de roles.

En este artículo voy a explicar cómo implementar el framework Molecule en dichos roles para poder testear su sintaxis, su idempotencia (me encanta esta palabra) y su ejecución en diferentes instancias con diferentes sistemas operativos y configuraciones.

Me voy a extender bastante en este artículo ya que una vez que conozcas y entiendas Molecule verás que realmente es muy sencillo y su utilización no tiene demasiada complicación, pero si es la primera vez que te encuentras con este framework hay pequeñas (pero muchas) cosas que hay que entender para poder aprovecharlo al 100%.

Quiero dejar claro que éste artículo es sobre Molecule, no sobre Ansible... por lo que doy por hecho que ya conoces Ansible, sabes en qué consisten sus roles, cómo utilizarlos, etc.

Si este no es tu caso, seguramente este artículo no te será de mucha ayuda así que quizás deberías ponerte a enredar con Ansible primero (intentaré escribir un artículo sobre Ansible algún dia).

Aclarado este punto, comencemos:

  1. ¿Qué es Molecule?
  2. Instalación inicial del framework
  3. Configuración y ejecución de tests
  4. Referencias

¿Qué es Molecule?

Según lo definen en su propia página web, Molecule es un framework que facilita el desarrollo y los tests de los roles de Ansible. Permite la realización de tests con múltiples instancias, sistemas operativos, proveedores de virtualización, escenarios, etc.

Molecule se encarga de que los roles que hagamos estén bien escritos, sean mantenibles, fácilmente entendibles y de que el resultado de su ejecución sea el esperado.

Es decir, Molecule nos provee de la infraestructura necesaria para poder lanzar nuestros roles en diferentes escenarios, comprobar su funcionamiento, asegurarnos de que cumplen las reglas de estilo que queremos, etc.

Disponemos de varios drivers con los que ejecutar los tests. Uno de los más interesantes es el de Docker ya que a través de este driver Molecule puede levantar contenedores con cualquier sistema operativo o configuración y ejecutar en ellos nuestros roles de Ansible para asegurarnos de que funcionan correctamente en cualquier sistema operativo o versión.

Aparte de esto, Molecule cuenta también con otras funcionalidades que nos ayudan con el correcto desarrollo de nuestros roles de Ansible, como por ejemplo:

  • Analizador Linter: Molecule dispone de varios lints que se encargarán de comprobar que la sintaxis de nuestros roles sea correcta, que su código cumpla con los estándares de estilo que hayamos definido, etc.
  • Comprobación de idempotencia: Uno de los fundamentos básicos de Ansible es la idempotencia, es decir, que podamos ejecutar un role o tarea tantas veces como queramos y que el resultado de todas esas ejecuciones sea el mismo que si únicamente lo hubiéramos ejecutado una vez.
  • Ejecución de roles en diferentes entornos: Como os comentaba antes, según el driver que utilicemos Molecule puede ejecutar los roles en diferentes contenedores de Docker, en entornos Cloud, máquinas virtuales, etc.
  • Verificación de resultados: Molecule puede ejecutar diferentes scripts una vez terminada la ejecución de nuestros roles para comprobar que efectivamente han hecho lo que se supone que debían hacer. La forma más común de hacerlo es mediante Testinfra, una serie de scripts que podemos realizar para comprobar en qué estado está nuestro servidor después de la ejecución de un role de Ansible.

Ahora que ya sabemos más o menos de qué va la cosa, vamos a ver cómo se utiliza.

Instalación inicial del framework

Podemos instalar Molecule utilizando el gestor de paquetes de nuestra distribución (apt, yum, etc) pero (al igual que Ansible) Molecule está escrito en Python, por lo que personalmente recomiendo su instalación utilizando pip dentro de un virtualenv.

Tanto pip como virtualenv son dos aplicaciones completamente indispensables en el mundo de Python por lo que supongo que las conocerás bien, pero por si acaso:

  • pip: Se trata de un gestor de paquetes de Python. Podríamos decir que pip es a Python lo que apt es a Debian o lo que yum es a CentOS. Tienes toda la información sobre esta aplicación aquí: https://pip.pypa.io/en/stable/.
  • virtualenv: Python es un lenguaje genial, muy potente y versátil... pero debido a sus múltiples versiones, a sus incompatibilidades entre ellas, a su ingente cantidad de librerías, etc, etc. a veces es una locura disponer de todos los paquetes y librerías que necesitamos en nuestro proyecto. Para solucionar estos problemas nació virtualenv, una herramienta que permite gestionar entornos virtuales de Python. Dentro de estos entornos podemos instalar las librerías de Python que queramos, módulos, etc. y estarán disponibles y accesibles únicamente desde dicho entorno, por lo que cuando salgamos de él, todo lo que hubiéramos instalado dejará de estar disponible para el resto del sistema. De esta forma podemos crear un entorno virtual para nuestro proyecto e instalar en él las librerías necesarias sin que éstas afecten a las del resto del sistema (ni las del resto del sistema a nuestro entorno, por supuesto). Tienes toda la información sobre virtualenv aquí: https://virtualenv.pypa.io/en/stable/, pero no te preocupes si es la primera vez que lo ves y todavía no lo tienes claro, lo veremos más en detalle un poco más adelante.

Por lo tanto, aunque podrías simplemente ejecutar apt-get install molecule desde tu flamante Debian, es mejor mantener el sistema limpio e instalar Molecule y el resto de paquetes necesarios únicamente dentro del entorno virtual de nuestro role.

Empecemos por el principio: supongamos que queremos escribir un role de Ansible que se encargue (por ejemplo) de instalar el editor Vim.

Llamaremos a nuestro role vim-installer.

Lo primero que necesitamos es crear la estructura de archivos y directorios típica de un role de Ansible, por lo que ejecutamos el comando ansible-galaxy de esta forma:

$ ansible-galaxy init --verbose vim-installer

Este comando creará un directorio llamado vim-installer que contendrá la siguiente estructura:

└─ vim-installer/
   ├─ defaults/
   │  └─ main.yml
   ├─ files/
   ├─ handlers/
   │  └─ main.yml
   ├─ meta/
   │  └─ main.yml
   ├─ tasks/
   │  └─ main.yml
   ├─ templates/
   ├─ tests/
   │  ├─ inventory
   │  └─ test.yml
   ├─ vars/
   │  └─ main.yml
   └─ README.md

Esta es la estructura típica que tienen todos los roles de Ansible.

Por simplificar el ejemplo, hemos acordado que nuestro role únicamente iba a encargarse de instalar Vim, por lo que editamos el archivo tasks/main.yml y creamos una tarea que instale dicho paquete:

tasks/main.ymldownload
---
# tasks file for vim-installer
- name: Instalando Vim
  apt:
    name: vim
    state: present

Como veis es un role muy sencillo en el que he usado a propósito el módulo apt para instalar el paquete para que cuando probemos el role en diferentes sistemas operativos podamos ver cómo funciona correctamente cuando lo hacemos en distribuciones basadas en Debian (en las que si se usa el comando apt) y cómo falla cuando lo probamos en otras distribuciones que no tienen dicho comando (como CentOS, Fedora, SuSE, etc).

Ya tenemos entonces nuestro role para instalar Vim usando Ansible, pero antes de continuar vamos a crear un virtualenv para poder instalar Molecule y sus dependencias.

Lo primero será instalar Virtualenv y Pip (si no los tenemos instalados ya). Para instalarlos, ahora si, podemos utilizar el gestor de paquetes de nuestra distribución:

Para los que uséis distribuciones de machotes, como por ejemplo Debian:

$ sudo apt-get install virtualenv python-pip

Para los que prefieran distribuciones de niñitas, como por ejemplo CentOS:

$ sudo yum install python-virtualenv python2-pip

Ahora que ya los tenemos instalados, creamos un nuevo entorno virtual con el comando virtualenv (dentro del directorio de nuestro proyecto) y lo activamos:

$ cd vim-installer
$ virtualenv venv
$ source venv/bin/activate

Esto creará un nuevo directorio llamado venv (el nombre que le hemos dado a nuestro entorno virtual) en el cual se guardarán las librerías y módulos de Python que instalemos de ahora en adelante y hasta que salgamos del virtualenv.

Os habréis dado cuenta de cuando hemos activado el virtualenv con el comando source venv/bin/activate nos aparece en el prompt el nombre de nuestro entorno virtual entre paréntesis, verdad? Eso nos indica que estamos dentro de dicho virtualenv, y aparecerá siempre hasta que salgamos del mismo (con el comando deactivate o al cerrar el terminal).

Ahora que estamos dentro de un virtualenv aislado del resto del sistema vamos a instalar los paquetes necesarios utilizando el comando pip.

Podríamos instalar los paquetes de uno en uno con pip install <nombre-paquete>, pero cuando tenemos varios proyectos diferentes al final se nos olvida qué paquetes son necesarios para cada uno de ellos, por lo que una buena práctica recomendada es la de tener un archivo llamado requirements.txt con la lista de paquetes necesarios para poder ejecutar nuestro proyecto.

Por lo tanto, vamos a crear dicho archivo de texto en la raíz de nuestro proyecto conteniendo el nombre y versión de los paquetes que necesitamos:

requirements.txtdownload
molecule>=2
docker-py>=1
testinfra>=1.7

Y ahora los instalamos todos a la vez (incluidas sus muchas dependencias) con el comando:

(venv) $ pip install -r requirements.txt

Ya tenemos Molecule instalado, pero para poder utilizarlo hacen falta varios archivos de configuración, directorios, etc. Sería una pérdida de tiempo tener que crear esas cosas de forma manual (aunque podríamos hacerlo si quisiéramos), por lo que vamos a decirle a Molecule que los genere automáticamente:

(venv) $ molecule init scenario --role-name vim-installer

Podríamos especificar el parámetro --driver <nombre_del_driver> para decirle a Molecule que utilice un driver u otro (Vagrant, Docker, Azure, EC2, OpenStack, etc), pero el que vamos a ver aquí es Docker (que es el driver por defecto en Molecule) por lo que no hace falta especificarlo.

Este comando añadirá 2 cosas a nuestro actual role:

  • .yamllint: Este archivo contiene la configuración y las reglas del analizador linter de archivos yaml (si no sabes qué es esto no te preocupes, lo explicaré un poco más adelante).
  • molecule/: En este directorio se guarda la configuración de Molecule para nuestro role, así como todos los demás archivos necesarios para la ejecución de los tests.

Nuestra estructura de archivos y directorios estará ahora de esta forma:

└─ vim-installer/
   ├─ defaults/
   │  └─ main.yml
   ├─ files/
   ├─ handlers/
   │  └─ main.yml
   ├─ meta/
   │  └─ main.yml
   ├─ molecule/    <--- nuevo subdirectorio (con el contenido de Molecule)
   │  └─ default/
   │     ├─ tests/
   │     │  ├─ test_default.py
   │     │  └─ test_default.pyc
   │     ├─ create.yml
   │     ├─ destroy.yml
   │     ├─ Dockerfile.j2
   │     ├─ INSTALL.rst
   │     ├─ molecule.yml
   │     ├─ playbook.yml
   │     └─ prepare.yml
   ├─ tasks/
   │  └─ main.yml
   ├─ templates/
   ├─ tests/
   │  ├─ inventory
   │  └─ test.yml
   ├─ vars/
   │  └─ main.yml
   ├─ README.md
   └─ .yamllint    <--- nuevo archivo (con las reglas del lint de YAML)

Antes de seguir y para evitar malentendidos no debemos confundir el directorio tests/ (que es el que crea Ansible al inicializar el role) con el directorio molecule/default/tests/ (que es el que crea Molecule con sus tests de Testinfra y que ya veremos más adelante). Por lo que ya que vamos a realizar los tests con Molecule podemos prescindir del directorio tests/ de Ansible, así que lo eliminamos y así evitamos posibles confusiones:

(venv) $ rm -rf tests

Con esto ya tendríamos un role de Ansible con el entorno de Molecule instalado. Vamos ahora a configurarlo...

Configuración y ejecución de tests

La configuración general de Molecule está en el archivo molecule/default/molecule.yml. Ahí es donde se configuran tanto los parámetros globales de los tests de Molecule como los parámetros específicos de cada uno de los tests individuales.

Más adelante veremos detenidamente qué son y para qué sirven cada uno de los archivos que se han generado en el paso anterior, pero antes hay algunas cosas que debemos saber sobre cómo funciona Molecule.

En primer lugar hay que saber que Molecule incluye varios tipos de comprobaciones. No se limita a lanzar los tests que hayamos programado, sino que hace varias comprobaciones diferentes, y ejecutar esos tests son solo una de ellas.

Aunque Molecule tiene varios drivers (como mencioné al principio) y en este artículo vamos a utilizar el driver Docker, cabe recordar igualmente que hay muchos otros como por ejemplo EC2 (para utilizar instancias de Amazon Web Services en lugar de contenedores Docker), o Azure, OpenStack, Vagrant, etc.

Estas son las acciones que podemos ejecutar en Molecule:

  • lint: Comprueba que los archivos .yml de nuestro role cumplen las reglas de estilo especificadas.
  • destroy: Destruye los contenedores Docker de una ejecución anterior (si los hubiera).
  • dependency: Instala las dependencias necesarias (en caso de haberlas) de los contenedores Docker donde vamos a probar nuestro role.
  • syntax: Comprueba que la sintaxis de los archivos .yml de nuestro role es correcta.
  • create: Construye e inicia todos los contenedores donde vayamos a probar nuestros roles.
  • prepare: Prepara (si es necesario) los contenedores antes de ejecutar los roles (por si hubiera que instalar algún paquete en ellos o hacer cualquier otra cosa).
  • converge: Ejecuta el role en todos los contenedores que previamente se han creado.
  • idempotence: Ejecuta de nuevo los roles (como en el paso anterior) para comprobar que el resultado es el mismo que la primera vez y asegurar de esta forma el principio de idempotencia necesario en cualquier role de Ansible.
  • side-effect: Esta comprobación viene desactivada por defecto, y se encarga de comprobar efectos colaterales de la ejecución del role en los contenedores (no la he usado nunca, así que poco puedo hablar de ella).
  • verify: Ejecuta tests (de Testinfra o Goss) para comprobar que el role ha hecho lo que se suponía que debía hacer en cada uno de los contenedores.
  • destroy: Una vez terminados los tests, destruye todos los contenedores y finaliza la ejecución.

Solo después de haber ejecutado una a una todas estas acciones y de no haber registrado ningún error en ellas se daría por buena la ejecución de Molecule y podríamos decir que nuestro role pasa los tests (OK). Si se detecta un error en cualquiera de ellas, se interrumpe la ejecución y se asume que nuestro role no pasa los tests (KO).

Un test completo incluiría todas esas estas acciones, pero también podemos ejecutar Molecule para que lleve a cabo solo una de ellas (o varias).

La mayoría de estos tests de Molecule se corresponden con una de estas acciones, pero hay algunos tests que conllevan varias de ellas. Por ejemplo, antes de ejecutar la acción de converge (que es la que ejecuta el role en los diferentes contenedores de Docker) tendremos que haber creado los contenedores mediante la acción create. Por lo tanto, estos serían los tests de los que disponemos en Molecule (y las acciones que conlleva cada uno de ellos):

  • Lint: Este test únicamente se corresponde con la acción lint.
  • Syntax: Este test únicamente se corresponde con la acción syntax.
  • Converge: Este test se corresponde con las acciones: create, prepare, converge y destroy.
  • Idempotence: Este test únicamente se corresponde con la acción idempotence.
  • Side-effect: Este test únicamente se corresponde con la acción side-effect.
  • Verify: Este test únicamente se corresponde con la acción verify.

En el archivo de configuración de Molecule (molecule/default/molecule.yml) podemos configurar la secuencia de acciones que tendrá asociado cada test. Esto se hace definiendo las secuencias dentro del bloque de configuración de scenario. En mi caso, suelo dejar ese bloque de esta forma:

scenario:
  name: default
  create_sequence:
    - create
    - prepare
  check_sequence:
    - destroy
    - create
    - prepare
    - converge
    - check
    - destroy
  converge_sequence:
    # - dependency
    - create
    - prepare
    - converge
  destroy_sequence:
    - destroy
  test_sequence:
    - lint
    - destroy
    # - dependency
    - syntax
    - create
    - prepare
    - converge
    - idempotence
    # - side_effect
    - verify
    - destroy

Es decir, cuando se invoque a create en realidad se estará ejecutando la acción create y luego la acción prepare. Cuando se invoque a check se estará ejecutando las acciones destroy, create, prepare, converge, check y destroy. En el caso de invocar un test completo, suelo deshabilitar (según el caso) las acciones dependency y side_effect.

Vamos a ver cada uno de los tests con un poco más de detenimiento...

Test lint

Se conoce como linter (o lint a secas) al programa que se encarga de examinar nuestro código en busca de partes incorrectas, sospechosas, incompatibles, etc.

En este caso, el test lint de Molecule se encarga de examinar los archivos .yml que forman nuestro role de Ansible para comprobar que cumple con una serie de reglas predefinidas. Estas reglas suelen ser reglas de formato, como por ejemplo que todos los archivos comiencen con tres guiones (los archivos .yml suelen empezar por ---), que no haya líneas cuya longitud supere un determinado número de caracteres (para que el código sea legible), etc.

Vamos a ejecutar este test, a ver qué nos dice Molecule:

(venv) $ molecule lint

El resultado de ese comando en nuestro role será algo parecido a esto:

--> Test matrix

└── default
    └── lint

--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /tmp/vim-installer/...
    /tmp/vim-installer/tasks/main.yml
      2:31      error    no new line character at the end of file  (new-line-at-end-of-file)

    /tmp/vim-installer/meta/main.yml
      1:1       warning  missing document start "---"  (document-start)
      30:4      warning  missing starting space in comment  (comments)
      48:5      warning  comment not indented like content  (comments-indentation)
      56:3      warning  comment not indented like content  (comments-indentation)
      57:42     error    no new line character at the end of file  (new-line-at-end-of-file)

    /tmp//vim-installer/vars/main.yml
      2:30      error    no new line character at the end of file  (new-line-at-end-of-file)

    /tmp/vim-installer/handlers/main.yml
      2:34      error    no new line character at the end of file  (new-line-at-end-of-file)

    /tmp/vim-installer/defaults/main.yml
      2:34      error    no new line character at the end of file  (new-line-at-end-of-file)

Que viene a decir que hay 5 archivos (de los generados por Ansible al inicializar el nuevo role) que no cumplen con las reglas de estilo predefinidas para los archivos .yml.

Es más que probable que aparezcan otro cientos de errores aparte de estos debido a que al estar dentro de un virtualenv tendremos un montón de archivos .yml en dicho directorio (los cuales no tiene porqué cumplir ninguna de nuestras reglas de estilo ya que son archivos de terceros sobre los que no tenemos ningún control) por lo que antes de nada, edita el archivo .yamllint y añade ignore: venv/ justo debajo de extends: default para que no se tenga en cuenta lo que hay en ese directorio. Después, ejecuta de nuevo el test lint para ver si ya no aparecen esos archivos en el informe de Molecule.

Dicho esto, si en lugar de inicializar un nuevo role (como he hecho yo para escribir este artículo) estuvieras implementando Molecule en un role que ya tuvieras hecho de antemano, obviamente la salida de este test podría ser diferente (podría haber otros errores diferentes o incluso no haber ninguno).

La mayoría de los errores que nos está mostrando Molecule ahora son debidos a que los archivos no terminan con un retorno de carro (que es una de las reglas de estilo que tiene el linter por defecto), por lo que si editamos los archivos tasks/main.yml, meta/main.yml, vars/main.yml, handlers/main.yml y defaults/main.yml, añadimos un retorno de carro al final y volvemos a ejecutar el test lint obtendríamos algo parecido a esto:

--> Test matrix

└── default
    └── lint

--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /tmp/vim-installer/...
    /tmp/vim-installer/meta/main.yml
      1:1       warning  missing document start "---"  (document-start)
      30:4      warning  missing starting space in comment  (comments)
      48:5      warning  comment not indented like content  (comments-indentation)
      56:3      warning  comment not indented like content  (comments-indentation)

Lint completed successfully.
--> Executing Flake8 on files found in /tmp/vim-installer/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /tmp/vim-installer/molecule/default/playbook.yml...
Lint completed successfully.

La mayoría de los errores que teníamos antes han desaparecido. Y para solucionar el resto podemos o bien ir corrigiéndolos o bien configurar el linter para desactivar esa regla en concreto (en caso de que no queramos que se aplique en nuestro role por algún motivo).

Esta configuración se guarda en el archivo .yamllint, el cual fué creado de forma automática por Molecule cuando inicializamos el role y cuyo contenido es:

.yamllintdownload
extends: default

ignore: .venv/

rules:
  braces:
    max-spaces-inside: 1
    level: error
  brackets:
    max-spaces-inside: 1
    level: error
  line-length: disable

Por ejemplo, es una regla recomendable que en todos los archivos .yml los comentarios estén indentados igual que el resto de líneas que no son comentarios, por lo que el linter se quejará si detecta algún archivo que no cumpla esta regla (como acabamos de ver en nuestro ejemplo).

Pero si por cualquier motivo nosotros queremos permitir que estos comentarios no tengan que cumplir las reglas de indentación, podemos editar la configuración del lint y añadir comments-indentation: false. El archivo de configuración quedaría entonces de esta forma:

.yamllintdownload
extends: default

ignore: venv/

rules:
  comments-indentation: false 
  braces:
    max-spaces-inside: 1
    level: error
  brackets:
    max-spaces-inside: 1
    level: error
  line-length: disable

Si volvemos a ejecutar ahora el test lint el resultado será:

--> Test matrix

└── default
    └── lint

--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /tmp/vim-installer/...
    /tmp/vim-installer/meta/main.yml
      1:1       warning  missing document start "---"  (document-start)
      30:4      warning  missing starting space in comment  (comments)

Lint completed successfully.
--> Executing Flake8 on files found in /tmp/vim-installer/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /tmp/vim-installer/molecule/default/playbook.yml...
Lint completed successfully.

Como puedes ver, ya no aparecen los errores relativos a los comentarios que aparecían antes.

Aparte de la configuración de las reglas, hay algunas otras cosas que podemos configurar relativas al propio linter. Si examinas el contenido del archivo de configuración de Molecule (molecule/default/molecule.yml) verás algo parecido a esto:

molecule/default/molecule.ymldownload
---
dependency:
  name: galaxy
driver:
  name: docker
lint:
  name: yamllint
platforms:
  - name: instance
    image: centos:7
provisioner:
  name: ansible
  lint:
    name: ansible-lint
scenario:
  name: default
verifier:
  name: testinfra
  lint:
    name: flake8

Si modificamos el bloque del lint podemos configurar algunas cosas, como activar/desactivar este test, elegir qué linter queremos usar, etc.

Una cosa que a mi me gusta cambiar aquí es la ubicación del archivo de definición de reglas de estilo (.yamllint) ya que me gusta que esté dentro del directorio de Molecule, por lo que el bloque lint de mi configuración suele ser así:

lint:
  name: yamllint
  enabled: "True"
  options:
    config-file: "molecule/.yamllint"
    format: "standard"

Supongo que más o menos ha quedado claro como funciona el test lint, verdad? Toda la información sobre las posibles configuraciones de este test están en su documentación oficial.

Y si quieres profundizar más en relación a las reglas que puedes aplicar, instrucciones para integrar un linter en tu IDE, etc. puedes consultar esta documentación en la página web del proyecto yamllint.

Test syntax

Vamos con el siguiente test, que como habrás podido adivinar por el nombre, únicamente se encarga de comprobar la sintaxis de nuestro role.

Si ejecutamos este test, Molecule recorrerá de nuevo todos los archivos .yml de nuestro role en busca de errores sintácticos.

Vamos a probar el test:

(venv) $ molecule syntax

El resultado debería ser:

--> Test matrix

└── default
    └── syntax

--> Scenario: 'default'
--> Action: 'syntax'

    playbook: /tmp/vim-installer/molecule/default/playbook.yml

Lo que significa que el test ha pasado correctamente y que no hay ningún error de sintaxis.

Para probar que efectivamente el test funciona vamos editar uno de los archivos para introducir un error a propósito. Editamos, por ejemplo, el archivo tasks/main.yml y escribimos cualquier cosa incorrecta:

vars/main.ymldownload
---
# vars file for vim-installer
esto-falla-seguro

Aunque el archivo tiene un formato .yml válido y cumple todas las reglas de estilo por defecto (es decir, pasaría el test lint) eso de esto-falla-seguro no es ningún comando válido de Ansible, por lo que si volvemos a ejecutar el analizador sintáctico veremos algo así:

--> Test matrix

└── default
    └── syntax

--> Scenario: 'default'
--> Action: 'syntax'
ERROR! The vars/main.yml file for role 'vim-installer' must contain a dictionary of variables
ERROR:

Perfecto! Molecule se ha dado cuenta del error y nos avisa, por lo que volvemos a dejar el archivo como estaba y pasamos al siguiente test.

Test converge

En este test es donde vamos a aprovechar todo el potencial de Docker, ya que es el que nos permite ejecutar nuestro role en diferentes contenedores, cada uno de ellos con un sistema operativo diferente, versiones diferentes del mismo sistema operativo, distintas configuraciones o cualquier otra cosa que se nos ocurra.

Por defecto, cuando lanzamos este test, Molecule ejecuta las acciones dependency, create, prepare y converge. En mi caso, como ya puse un poco más arriba, suelo modificar esta secuencia para que no ejecute la acción dependency (porque no hay ninguna dependencia en el role que estamos probando).

Vamos a configurar Molecule para probar nuestro role en Linux Debian 8 (por poner un ejemplo) y luego ya probaremos en otros sistemas operativos diferentes.

Editamos el archivo de configuración (molecule/default/molecule.yml) y añadimos estas líneas en el bloque platforms:

platforms:
  - name: molecule_debian8
    image: debian:8-slim
    privileged: "True"

Con esta configuración estamos definiendo una instancia llamada molecule_debian8 (que se creará utilizando la imagen debian:8-slim). Yo he elegido esta imagen para probar el role, pero obviamente podéis usar cualquier imagen que haya en Docker Hub) o incluso crear vuestras propias imágenes.

Vamos a lanzar el test:

(venv) $ molecule converge

Tardará un poco (sobre todo la primera vez, porque tiene que descargar la imagen desde Docker Hub) pero una vez terminado deberíamos ver algo parecido a esto:

--> Test matrix

└── default
    ├── create
    ├── prepare
    └── converge

--> Scenario: 'default'
--> Action: 'create'

    PLAY [Create] ******************************************************************

    TASK [Create Dockerfiles from image names] *************************************
    changed: [localhost] => (item=None)

    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)

    TASK [Build an Ansible compatible image] ***************************************
    changed: [localhost] => (item=None)

    TASK [Create docker network(s)] ************************************************

    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=None)

    TASK [Wait for instance(s) creation to complete] *******************************
    changed: [localhost] => (item=None)

    PLAY RECAP *********************************************************************
    localhost                  : ok=5    changed=3    unreachable=0    failed=0


--> Scenario: 'default'
--> Action: 'prepare'

    PLAY [Prepare] *****************************************************************

    PLAY RECAP *********************************************************************


--> Scenario: 'default'
--> Action: 'converge'

    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [molecule_debian8]

    PLAY RECAP *********************************************************************
    molecule_debian8           : ok=1    changed=0    unreachable=0    failed=0

Podemos ver como se han ido ejecutando las 3 acciones: crear los contenedores (acción create), prepararlos (acción prepare) y después lanzar nuestro role en ellos (acción converge).

El role se ha ejecutado correctamente (como era de esperar) porque estamos probando en una distribución Debian, pero qué pasaría si probáramos también en un sistema operativo que no tenga el comando apt que estamos usando en el role? Vamos a editar de nuevo el archivo de configuración de Molecule (molecule/default/molecule.yml) y vamos a añadir una instancia CentOS 7 (por ejemplo):

platforms:
  - name: molecule_debian8
    image: debian:8-slim
    privileged: "True"
  - name: molecule_centos7
    image: centos:7
    privileged: "True"

Volvemos a lanzar el test converge, y el resultado esta vez es diferente:

--> Test matrix

└── default
    ├── create
    ├── prepare
    └── converge

--> Scenario: 'default'
--> Action: 'create'

    PLAY [Create] ******************************************************************

    TASK [Create Dockerfiles from image names] *************************************
    changed: [localhost] => (item=None)
    ok: [localhost] => (item=None)

    TASK [Discover local Docker images] ********************************************
    changed: [localhost] => (item=None)
    ok: [localhost] => (item=None)

    TASK [Build an Ansible compatible image] ***************************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)

    TASK [Create docker network(s)] ************************************************

    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)

    TASK [Wait for instance(s) creation to complete] *******************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)

    PLAY RECAP *********************************************************************
    localhost                  : ok=5    changed=3    unreachable=0    failed=0


--> Scenario: 'default'
--> Action: 'prepare'

    PLAY [Prepare] *****************************************************************

    PLAY RECAP *********************************************************************


--> Scenario: 'default'
--> Action: 'converge'

    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [molecule_centos7]
    ok: [molecule_debian8]

    TASK [vim-installer : Instalando Vim] ******************************************
    fatal: [molecule_centos7]: FAILED! => {"changed": false, "cmd": "apt-get update", "msg": "[Errno 2] No such file or directory", "rc": 2}
    changed: [molecule_debian8]

    PLAY RECAP *********************************************************************
    molecule_centos7           : ok=1    changed=0    unreachable=0    failed=1
    molecule_debian8           : ok=2    changed=1    unreachable=0    failed=0

ERROR:

Lógicamente la ejecución del role en la instancia CentOS ha fallado. Vamos a modificar un poco el role para asegurarnos de que se funciona correctamente tanto en Debian como en CentOS. Editamos el archivo tasks/main.yml y añadimos otra tarea específica para CentOS utilizando la cláusula when para ejecutar un modulo u otro dependiendo de la distribución en la que estemos ejecutando el role:

tasks/main.ymldownload
---
# tasks file for vim-installer
- name: Instalando Vim en Debian
  apt:
    name: vim
    state: present
  when: ansible_distribution == 'Debian'

- name: Instalando Vim en CentOS
  yum:
    name: vim
    state: present
  when: ansible_distribution == 'CentOS'

Lanzamos de nuevo el test converge:

--> Test matrix

└── default
    ├── create
    ├── prepare
    └── converge

--> Scenario: 'default'
--> Action: 'create'
Skipping, instances already created.
--> Scenario: 'default'
--> Action: 'prepare'
Skipping, instances already prepared.
--> Scenario: 'default'
--> Action: 'converge'

    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [molecule_centos7]
    ok: [molecule_debian8]

    TASK [vim-installer : Instalando Vim en Debian] ********************************
    skipping: [molecule_centos7]
    ok: [molecule_debian8]

    TASK [vim-installer : Instalando Vim en CentOS] ********************************
    skipping: [molecule_debian8]
    changed: [molecule_centos7]

    PLAY RECAP *********************************************************************
    molecule_centos7           : ok=2    changed=1    unreachable=0    failed=0
    molecule_debian8           : ok=2    changed=0    unreachable=0    failed=0

Como verás, aparte de que las acciones create y prepare no se han ejecutado porque no ha habido cambios en ellas desde la última ejecución (para acelerar el proceso) el role ha funcionado tanto en la instancia Debian como en la CentOS.

Ya tenemos otro de los tests de Molecule funcionando! Vamos con el siguiente:

Test Idempotence

Uno de los requisitos fundamentales de todo role de Ansible es que cumpla el principio de idempotencia. Como ya expliqué antes, esto significa que da igual el número de veces que ejecutemos el role: siempre tiene que dar el mismo resultado.

Esto se hace para evitar que haya tareas que solo funcionan la primera vez que se ejecutan o por el contrario que únicamente funcionan a la segunda. En Ansible podemos ejecutar el mismo role 100 veces contra el mismo servidor, y este debería funcionar correctamente sin provocar ningún error las 100 veces.

Para comprobar esto, Molecule ejecuta el mismo role varias veces en las instancias que hayamos configurado en el test anterior (el test converge) y si en alguna de estas veces el role falla, el test nos lo hará saber.

Vamos a ver qué sucede al ejecutar este test con nuestro role actual:

(venv) $ molecule idempotence

Si el resultado es este:

--> Test matrix

└── default
    └── idempotence

--> Scenario: 'default'
--> Action: 'idempotence'
ERROR: Instances not converged.  Please converge instances first.

Significa que las instancias no están en ejecución y preparadas para este test, por lo que las levantamos con el comando molecule converge y ejecutamos el test de nuevo. El resultado debería ser parecido a esto:

--> Test matrix

└── default
    └── idempotence

--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.

El test ha funcionado, por lo que nuestro role cumple con el principio de idempotencia.

Vamos con el siguiente test:

Test Side-effect

Este test se utiliza para ver si la ejecución de un role ha tenido efectos colaterales que pudieran provocar errores, a pesar de que la ejecución del propio role haya sido correcta.

No se suele utilizar, por lo que incluso viene desactivado por defecto. Para habilitar este test hay que editar el archivo de configuración de Molecule y añadir la acción side_effect al bloque test_sequence dentro de scenario, o bien lanzar manualmente el test con el comando:

(venv) $ molecule side-effect

Pero antes tendríais que definir un playbook que se encargue de comprobar dichos efectos colaterales. Como ya os comenté, yo no he usado este test nunca por lo que poco puedo deciros mucho más sobre él.

Test Verify

Este test es uno de los más importantes, ya que es el que realmente se encarga de comprobar si lo que se supone que hace nuestro role se ha hecho realmente.

Molecule soporta 2 formas de ejecutar estas comprobaciones:

  • Testinfra: Esta herramienta es a su vez un plugin del framework Pytest, y permite ejecutar scripts escritos en Python para realizar las comprobaciones.
  • Goss: Es otra herramienta similar para definir comprobaciones pero que a diferencia de Testinfra utiliza archivos YAML en lugar de scripts en Python.

Por defecto, Molecule utiliza Testinfra para ejecutar los tests verify, pero ambos son perfectamente válidos. Personalmente utilizo Testinfra.

Editamos el archivo de configuración de Molecule (molecule/default/molecule.yml) e indicamos en el bloque verifier cual de los 2 queremos utilizar. En nuestro caso:

verifier:
   name: testinfra
   enabled: True
   directory: tests/
   lint:
     name: flake8

Como ves, aquí también podemos indicar el directorio donde estarán los scripts con las comprobaciones que queramos hacer (relativo a molecules/default/), si queremos deshabilitar el test o incluso un linter (no tiene nada que ver con el que usábamos antes en el test lint) para analizar el código de estos scripts (se usa flake8 porque como ya dije, los scripts de Testinfra se escriben en Python).

Todos los tests que vayamos a ejecutar deben estar en archivos cuyo nombre comience por test_, sino no se ejecutarán (puede parecer una tontería, pero lo recalco para evitar que os volváis locos por este motivo, como me pasó a mi xDD).

Vamos a echarle un vistazo al test que Molecule trae por defecto (molecule/default/tests/test_default.py):

molecule/default/tests/test_default.pydownload
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('/etc/hosts')

    assert f.exists
    assert f.user == 'root'
    assert f.group == 'root'

Aunque no seas un experto en Python seguro que puedes deducir sin problemas lo que hace este sencillo código:

  • Comprueba que exista el archivo /etc/hosts.
  • Comprueba que pertenezca al usuario root.
  • Comprueba que pertenezca también al grupo root.

Las 3 aserciones son ciertas en cualquier distribución Linux, por lo que si lanzamos este test debería ejecutarse correctamente:

(venv) $ molecule verify

Y el resultado:

--> Test matrix

└── default
    └── verify

--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /tmp/vim-installer/molecule/default/tests/...
    ============================= test session starts ==============================
    platform linux2 -- Python 2.7.14+, pytest-3.4.2, py-1.5.2, pluggy-0.6.0
    rootdir: /tmp/vim-installer/molecule/default, inifile:
    plugins: testinfra-1.11.1
collected 2 items

    tests/test_default.py ..                                                 [100%]

    ========================== 2 passed in 10.71 seconds ===========================
Verifier completed successfully.

Vale, el test se ejecuta correctamente... pero lo que se está comprobando en él no tiene nada que ver con nuestro role, por lo que vamos a modificarlo un poco para que compruebe algo que de verdad nos asegure que nuestro role funciona.

Como nuestro role únicamente instala Vim, vamos a modificar el test para comprobar que el paquete Vim esté instalado. Aprovechando la potencia que nos da el framework de Testinfra, modificamos el test para que compruebe si el paquete vim está instalado (y aprovechamos para quitar los import y demás cosas que no usamos):

molecule/default/tests/test_default.pydownload
def test_vim_is_installed(host):
    vim = host.package("vim")

    assert vim.is_installed

Lanzamos de nuevo el test y comprobamos el resultado:

--> Test matrix

└── default
    └── verify

--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /tmp/vim-installer/molecule/default/tests/...
    ============================= test session starts ==============================
    platform linux2 -- Python 2.7.14+, pytest-3.4.2, py-1.5.2, pluggy-0.6.0
    rootdir: /tmp/vim-installer/molecule/default, inifile:
    plugins: testinfra-1.11.1
collected 2 items

    tests/test_default.py F.                                                 [100%]

    =================================== FAILURES ===================================
    ______________ test_vim_is_installed[ansible://molecule_centos7] _______________

    host = <testinfra.host.Host object at 0x7f6c912ed1d0>

        def test_vim_is_installed(host):
            vim = host.package("vim")

    >       assert vim.is_installed
    E       assert False
    E        +  where False = <package vim>.is_installed

    tests/test_default.py:12: AssertionError
    ====================== 1 failed, 1 passed in 9.15 seconds ======================

El test falla! ¿Qué es lo que está pasando? El role ha fallado y no se ha instalado Vim? No, no es eso... el role funciona perfectamente, el problema es que en Debian cuando instalas el editor Vim se instalan los paquetes vim y vim-common, pero CentOS únicamente se instala vim-common, por lo que el test falla al buscar el paquete vim en esta distribución.

Editamos de nuevo el test y cambiamos el nombre del paquete de vim a vim-common:

molecule/default/tests/test_default.pydownload
def test_vim_is_installed(host):
    vim = host.package("vim-common")

    assert vim.is_installed

Y ahora si, al ejecutar el test vemos que el role ha funcionado perfectamente:

--> Test matrix

└── default
    └── verify

--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /tmp/vim-installer/molecule/default/tests/...
    ============================= test session starts ==============================
    platform linux2 -- Python 2.7.14+, pytest-3.4.2, py-1.5.2, pluggy-0.6.0
    rootdir: /tmp/vim-installer/molecule/default, inifile:
    plugins: testinfra-1.11.1
collected 2 items

    tests/test_default.py ..                                                 [100%]

    =========================== 2 passed in 9.98 seconds ===========================
Verifier completed successfully.

Perfecto! Ya tenemos todos los tests de Molecule configurados.

Si recuerdas cuando estuvimos definiendo las secuencias en el archivo de configuración de Molecule seguramente te fijarías en que había una secuencia llamada test que se encargaba de llamar uno a uno a todos los tests, verdad? Me refiero a este código dentro del bloque scenario del archivo molecule/default/molecule.yml:

test_sequence:
  - lint
  - destroy
  # - dependency
  - syntax
  - create
  - prepare
  - converge
  - idempotence
  # - side_effect
  # - verify
  - destroy

Si ejecutamos ese test se lanzarán una a una todas acciones que hayamos configurado en esa secuencia, por lo que cuando queramos pasar todos los tests del role o estemos configurando el proyecto para utilizar un sistema de Integración Contínua (como el de Gitlab-CI que ya comenté en este otro artículo) no hace falta que los ejecutes uno a uno, puede ejecutarlos todos de forma automática con el comando:

(venv) $ molecule test

Y ahora si, creo que con esto tienes más que suficiente para implementar este genial framework de tests que es Molecule. Me temo que ya no hay excusa cuando al cambiar algo en un role se te rompa otra cosa accidentalmente.

Cubre todas las tareas que hagas con tests y dormirás más tranquilo!

Ah, se me olvidaba! Si tienes problemas con la configuración de alguno de los tests, puedes añadir el parámetro --debug a Molecule para que muestre mucha más información de lo que va haciendo en cada una de las acciones:

(venv) $ molecule --debug test

Como siempre, espero que el artículo te haya resultado de utilidad! Y por supuesto no dudes en distribuirlo y compartirlo con todo el mundo, pero por favor: cita siempre la fuente original de éste artículo.

Alaaaaaaaaaaaaaaaaaaaaaaa!

Referencias