KitchenCI: Test unitarios de Ansible con Docker e Inspec

0
1328

Índice de contenidos

  1. Introducción
  2. Driver
    2.1 Funcionamiento
    2.2 Cambios comunes de configuración
  3. Provisioner
    3.1 Funcionamiento
    3.2 Cambios comunes de configuración
  4. Verifier
    3.1 Funcionamiento
    3.2 Cambios comunes de configuración
  5. Prueba práctica
  6. Conclusiones
  7. Referencias

1. Introducción

Ya hemos hablado previamente de Kitchen CI y de cómo funciona. Ahora vamos a ver un ejemplo práctico de su funcionamiento en el cual utilizaremos:

  • Docker como «driver», encargado de proporcionarnos la infraestructura en la que ejecutar las tareas a probar.
  • Ansible como «provisioner», encargado de ejecutar las tareas.
  • Inspec como «verifier», encargado de validar que las tareas se han ejecutado correctamente.

Para poder integrar estas herramientas con Kitchen CI ya existen varios plugins encargados de integrarse con el ciclo de vida de Kitchen, de forma que nos abstrae del proceso interno de cada uno de los componentes y nos permite trabajar con ellos de forma homogénea.

Eso quiere decir que el ciclo de vida que utilizaremos usando el driver de Vagrant y el de Docker serán iguales, siendo estos plugins los encargados de realizar los comandos correspondientes que sean necesarios en cada parte del ciclo de vida.

Vamos a ver qué herramientas nos ofrece el ecosistema de Kitchen CI, rematando con un pequeño ejemplo práctico.

2. Driver

Como ya hemos dicho antes necesitamos «un lugar» en el que probar que nuestras tareas funcionan. Este «lugar» depende del driver que utilizemos, pudiendo ser desde una máquina virtual (Vagrant), un contenedor (Docker), o una instancia en la nube (AWS).

En este ejemplo vamos a utilizar Docker como proveedor de la infraestructura en la que ejecutar los tests. Para ello utilizaremos el plugin Kitchen-docker.

2.1. Funcionamiento

Este plugin opera en los siguientes pasos del ciclo de vida de Kitchen:

  • Destroy: Elimina el contenedor de Docker.
  • Create: Crea el contenedor de Docker y permite que sea accesible desde la máquina local por SSH.
  • Login: Acceder por SSH al contenedor.

De todos los pasos, el más interesante es lo que ocurre en el create. Vamos a verlo en detalle.

Los pasos internos que sigue kitchen-docker por defecto son:

  • Creación de un Dockerfile temporal en el que:
    Elige qué imagen utilizar en base a la plataforma en la que se esté ejecutando.
    Instalación de curl y openssh para poder conectarnos por SSH al contenedor.
    Crea un par de clave publica/privada para dar acceso al sistema desde la máquina anfitriona (almacenado en el directorio .kitchen que se crea en el directorio en el que ejecutas el comando de Kitchen)
    Crea un usuario de acceso por defecto.
    Hacer sudoer al usuario creado por defecto.
    Añade la clave pública generada al authorized_keys del usuario creado previamente.
  • Construye la imagen a partir del Dockerfile temporal.
  • Arranca un contenedor a partir de la imagen, siendo el entrypoint de la misma el servicio de SSH.
  • Comprobar que se puede acceder por SSH al contenedor con el usuario especificado.

2.2. Cambios comunes de configuración

Es importante conocer cómo funciona el plugin para crear el Dockerfile temporal ya que puede dar muchos problemas de creación y acceso a la máquina a la hora de probar configuraciones específicas.

A continuación listo algunas de las preguntas que he tenido mientras configuraba la imagen de Docker y qué configuración del plugin ha sido necesaria para ello.

¿Si no le especifico ninguna imagen o Dockerfile en configuración al plugin de Kitchen Docker cuál utiliza?

El plugin comprueba qué plataforma (parámetro platform del fichero .kitchen.yml) estás utilizando y busca la imagen base adecuada para ello.

Si en nuestro .kitchen.yml tenemos definido:

driver:
  name: docker

provisioner
 ...

platforms:
  - name: "centos"

verifier:
 ...

En este caso, usará la imagen por defecto de Centos a la hora de ejecutar el create.
Para ver las plataformas disponibles, recomiendo leer la documentación del plugin en GitHub para tener una información actualizada.

¿Qué pasa si la plataforma que necesito no está disponible para mis tests?

Es posible que ninguna de las imágenes por defecto funcionen en tu caso, o simplemente quieras partir de una imagen de Docker personalizada.

En este caso, hay que especificar la imagen que quieres usar y kitchen-docker ignorará la plataforma para la elección de la imagen y realizará el resto de acciones que hemos listado anteriormente en el Dockerfile temporal sobre tu imagen.

Esto no quiere decir que la plataforma no sea importante ya que debe ser compatible. Si quieres probar en una imagen personalizada de Redhat, deberás poner que la plataforma es centos, ya que sino fallará tratando de instalar SSH y curl en la máquina porque no tendrá el gestor de paquetes que necesitas.

Para especificar la imagen, podemos hacerlo a nivel de platform y aplicará a todos los tests.

driver:
  name: docker

provisioner
 ...

platforms:
  - name: "centos"
    driver_config:
        image: "custom-image"

verifier:
 ...

¿Qué hago si no está la plataforma que soporte el gestor de paquetes de mi imagen?

Este puede ser uno de los motivos por el que queramos especificarle al plugin directamente un Dockerfile, en lugar de pasarle la imagen y dejar que lo genere dinámicamente.

Usar un Dockerfile personalizado tiene una serie de inconvenientes, ya que para que funcione como esperas debes encargarte en el propio Dockerfile de:

  • Crear el usuario acorde con el que tengas puesto en la configuración de Kitchen.
  • Instalar un servidor de SSH en el contenedor.
  • Creación de un usuario para acceder el contenedor.
  • Permitir que sea accesible por SSH desde el ciclo de vida de Docker, pasándole a kitchen-docker las rutas de las claves pública y privadas.
driver:
  name: docker

provisioner
 ...

platforms:
  - name: "centos"
    dockerfile: "ruta/al/dockerfile"
    public_key: "ruta/a/clave/pública/del/usuario/creado/dentro/del/dockerfile"
    private_key: "ruta/a/clave/privada/del/usuario/creado/dentro/del/dockerfile"

verifier:
 ...

El tener que gestionar este tipo de configuración hace que tu Dockerfile esté acoplado con Kitchen, de forma que si quieres cambiar el usuario con el que acceder al contenedor tienes que cambiar tu Dockerfile.

Además, la gran ventaja del plugin está en que te desentiendes de cómo lo crea y te da acceso al contenedor. Sin embargo tienes que ser consciente de cómo funciona internamente el plugin y cómo puede afectar a tu sistema la gestión interna que te facilita el plugin.

Por lo tanto, siempre que puedas, es recomendable crear tu imagen y hacer referencia a ella desde Kitchen, usando la plataforma que encaje con tu sistema gestor de paquetes. Esto debería ser más que suficiente en la mayoría de casos, y no te acopla la configuración de tu Dockerfile al plugin de Docker.

¿Cómo hago referencia a una imagen de Docker en un repositorio privado?

La referencia que hacemos a la imágen es la misma que en un Dockerfile, de forma que el formato de la misma es:

repository:port/image:version

De forma que nuestra configuración del .kitchen.yml quedaría parecida a:

driver:
  name: docker

provisioner
 ...

platforms:
  - name: "centos"
    image: "private_repo_url/custom_image:latest"

verifier:
 ...

Estas son las dudas principales que he tenido a la hora de configurar el plugin y con esto he sido capaz de configurar la base de la infraestructura de acuerdo a mis necesidades.

Espero que entender mejor cómo funciona este plugin haga que el hecho de tener configuraciones más especiales no te disuada de usar este framework. También, que no parezca magia negra la salida (que es bastante autoexplicativa) que sale por la terminal cuando escribes «kitchen create» 😀

Si quieres ver qué el resto de opciones a la hora de configurar kitchen-docker, puedes verlo aquí

3. Provisioner

Como decíamos antes, el apartado «provisioner» dentro del fichero .kitchen.yml define qué tareas vamos a ejecutar en la infraestructura creada por el «driver».

En este caso, la gestión de la configuración será llevada a cabo por Ansible, pero podríamos tener otras opciones cómo Chef o Puppet.

Muy por encima, Ansible es una herramienta de gestión de la configuración que funciona ejecutando scripts de Python en la máquina destino a la que se puede conectar de dos formas:

  • local: donde la máquina destino a ejecutar es la misma que ejecuta Ansible.
  • ssh: donde la máquina destino donde queremos ejecutar las tareas es diferente que la que ejecuta Ansible.

El plugin encargado de permitirnos usar Ansible como provisioner es Kitchen-ansible.

3.1. Funcionamiento

Este plugin opera en las siguientes fases del ciclo de vida de Kitchen:

  • Converge: donde realiza la ejecución de las tareas de Ansible.

Los pasos que sigue el plugin por defecto son:

  • Comprobar si Ansible está instalado en el contenedor.
  • Instalar (la última versión por omisión) Ansible en el contenedor.
  • Realizar el aprovisionamiento en el contenedor (con conexión local por defecto) con el usuario creado en el apartado del driver.

Ten en cuenta que NO necesitas tener Ansible instalado en tu maquina local, ya que Ansible se va a ejecutar desde el propio contenedor, no desde la máquina real.

3.2. Cambios comunes de configuración

Es importante en este punto saber cómo vas a realizar el aprovisionamiento en tus entornos reales, de forma que no solo pruebes la ejecución de las tareas, sino también la conectividad y el tipo de acceso (ssh o local) a ese entorno.

A continuación listo algunas de las preguntas que he tenido mientras configuraba el plugin y cómo he cambiado la configuración.

¿Cómo puedo hacer una prueba sencilla para probar que las tareas funcionan?

En caso de que solo quieras probar tu tarea sin tener en cuenta el tipo de conexión, hay que configurar la conexión para que sea local y que la ejecución del comando se haga con privilegios elevados.

driver:
    ....

provisioner:
    name: "ansible_playbook"
    ansible_connection: "local"
    ansible_sudo: true

platforms:
    ....

verifier:
    ....

La ventaja que tiene esto es que puedes probar de forma rápida si tu tarea funciona en un entorno en el que la conexión y los permisos no son un problema. Un ejemplo de uso de este tipo de configuración es:

  • Probar en local el desarrollo de un rol.
  • Probar el aprovisionamiento local en una instancia de autoescalado.

¿Si quiero probar el rol sin tener acceso a un usuario privilegiado?

Puede que por restricciones del entorno no tengas acceso privilegiado para realizar las acciones, con un usuario poderoso. Para esto, debemos cambiar la configuración de nuestro plugin para que que se ejecute Ansible como el usuario con el que te conectas a la máquina.

driver:
    ....

provisioner:
    name: "ansible_playbook"
    ansible_connection: "local"
    ansible_sudo: false

platforms:
    ....

verifier:
    ....

Cuando trato de conectarme por SSH el playbook falla siempre. ¿Por qué no puedo conectar por SSH?

La conexión por SSH no está gestionada por el plugin, que está pensado para que las pruebas se hagan con:

  • Aprovisionamiento local autogestionado por las máquinas.
  • Privilegios para hacer lo que quieras.

Siendo este un caso ideal de autoaprovisionamiento de instancias inmutables donde un cambio en la configuración significa la creación de una máquina nueva.

Cuando las máquinas son fijas y/o mutables, y el aprovisionamiento se hace desde una máquina central que se conecte a estas por SSH, es conveniente que estos test se prueben con la misma conexión que vas a usar en tu máquina local para asegurar no solo que la tarea se puede llevar a cabo, sino también la conexión.

Ya que esto no está gestionado por el plugin, es necesario añadir la configuración para que el usuario con el que te conectas pueda ser capaz de hacer SSH al usuario que debería ejecutar el playbook de Ansible.

De forma que:

Usuario por defecto --SSH--> Usuario configurado de acceso para Ansible (puede ser el mismo)

¿Cómo podemos probar los playbooks reales?

Para probar los playbooks reales, podemos hacer referencias a los mismos directamente desde la configuración del fichero .kitchen.yml

driver:
    ....

provisioner:
    name: "ansible_playbook"
    playbook: "ruta/al/real-playbook.yml"

platforms:
    ....

verifier:
    ....

Aunque si necesitamos tener una configuración específica y previa de las pruebas y queremos seguir probando el playbook real sin tener que duplicarlo, es decir, evitar tener este caso:

# test-playbook.yml

- hosts: all
  roles:
    - setup_test_role

- hosts: all
  roles:
    - role_to_test
# real-playbook.yml

- hosts: all
  roles:
    - role_to_test

Haciendo referencia al playbook de verdad en tu playbook de prueba:

# test-playbook.yml

- hosts: all
  roles:
    - setup_test_role

- import_playbook: "real-playbook.yml"

El resto de opciones son bastante intuitivas y ya las exploraremos en detalle en el ejemplo práctico que ejecutaremos al final.

Podemos ver todas las opciones que nos ofrece el plugin de kitchen-ansible aquí

4. Verifier

El «verifier» es el encargado de comprobar que las tareas del «provisioner» han sido ejecutadas correctamente. Cada vez hay más frameworks de testing de infraestructura como serverspec o inspec. En este tutorial usaremos inspec ya que estoy mas habituado a trabajar con este.

Inspec es un framework de test de infraestructura hecho en Ruby que pretende ser el sucesor de Serverspec y cuya estructura es muy parecida a la de Ansible. En lugar de los módulos de Ansible tiene los recursos, que te facilitan probar diferentes componentes, desde los permisos de un fichero, hasta el estado de un determinado servicio. Pero ya hablaremos en detalle de Inspec en otro tutorial 😀

El plugin que nos permite realizar pruebas con inspec dentro del ciclo de vida es Kitchen-inspec

4.1 Funcionamiento

Este plugin opera en la siguiente fases del ciclo de vida de inspec:

  • Verify: donde realiza las validación de las tareas ejecutadas por nuestro provisioner.

Los pasos que sigue el plugin son:

  • Ejecución de los test de inspec con un target local, con el usuario creado en el paso del driver.

4.2 Cambios comunes de configuración

En cuanto al plugin de inspec no me ha sido necesario usar configuraciones específicas, y todos los problemas que he tenido es con cómo hacer tests específicos, así como la estructuración de los mismos.

5. Prueba práctica

Una vez vistos los plugins, vamos a ver una prueba práctica. Este es el motivo de que no hayamos dicho previamente cómo se instala y se configura un proyecto en el que queramos utilizar estas pruebas.

Para facilitar el proceso de instalación, y mantener actualizada la configuración, utilizaremos un repositorio que he creado a modo de plantilla, con la configuración que considero básica para poder empezar a ser productivo usando:

  • Kitchen CI como orquestador del ciclo de vida.
  • Ansible como ejecutor de las tareas.
  • Docker como creador de la infraestructura.
  • Inspec como validador de las tareas.

Para comenzar nos descargamos el repositorio donde tenemos la plantilla, ejecutando en el directorio de nuestra elección el comando:

$ git clone git@gitlab.com:JuananOc/kadi-template.git

Este repositorio está configurado para gestionar las dependencias de Ruby con RVM y las de Python con pipenv. Si quieres saber qué son estas herramientas y cómo mejoran la gestión de las versiones de tus proyectos que incluyan Ruby y Python, aquí tienes el tutorial que habla de RVM y aquí el que habla de Python.

Una vez dentro del directorio, RVM nos creará el Gemset del proyecto con la gema bundler instalada. Esto quiere decir que solo tenemos que ejecutar:

$ cd kadi-template
$ bundle

Y nos descargará las versiones específicas de las gemas correspondientes a Kitchen y los plugins para Docker, Ansible e Inspec.

Ahora accedemos a los test del rol de Nginx, que prueba que Nginx se ha instalado correctamente, habiendo tests para:

  • Conexión local con privilegios e imagen por defecto según plataforma.
  • Conexión local sin privilegios e imagen por defecto según plataforma.
  • Conexión local con privilegios con una imagen personalizada.
  • Conexión por SSH con privilegios.

Para ejecutar estos tests vamos a la ruta donde se encuentran los tests del rol Nginx y ejecutamos el ciclo de vida completo de Kitchen.

$ cd ansible/playbooks/roles/nginx/tests
$ kitchen test

Y ya está 😀

El fichero de configuración .kitchen.yml se encuentra dentro de ansible/playbooks/roles/nginx/tests/. Este fichero contiene más comentarios en detalle explicando las propiedades que he usado y la configuración que he encontrado más cómoda para trabajar con Kitchen.

6. Conclusiones

Ya no hay excusas para poder probar nuestra infraestructura y añadir no solo el código de nuestras aplicaciones sino también nuestra infraestructura a la integración continua.

Aquí vemos como el desarrollo de la infraestructura como código acerca de forma inevitable las buenas prácticas del mundo del desarrollo del software, facilitando la creación de infraestructuras replicables.

También vemos como Kitchen CI nos facilita la gestión de las herramientas, ya que no tenemos que usar ni aprendernos los comandos específicos de Ansible para ejecutar un playbook determinado, ni los comandos específicos de Inspec para ejecutar los tests, ya que todos estos se realizan bajo el ciclo de vida de Kitchen, por lo que si estamos desarrollando un nuevo rol, nuestro ciclo de vida de ejecución aplicando TDD podría ser:

  • Creación de la estructura del nuevo rol, con su configuración del .kitchen.yml
  • Creación de un test que pruebe la funcionalidad que va a usar tu rol.
  • Ejecutar kitchen create para crear nuestra máquina virtual.
  • Ejecutar kitchen converge para aplicar nuestros cambios, actualmente inexistentes.
  • Ejecutar kitchen verify para validar nuestros cambios. En este punto nos fallará ya que no hemos implementado nuestros cambios. RED
  • Implementamos nuestros cambios en el rol.
  • Iteramos entre kitchen converge y kitchen verify hasta que pase nuestro test de Inspec. GREEN
  • Refactorizamos nuestro rol. REFACTOR.
  • Probamos el ciclo completo desde el principio, para garantizar que funciona desde cero nuestro rol con kitchen test

Y ya tendríamos nuestro rol probado. Sin necesidad de tener que saber cómo ejecutar los comandos específicos de Ansible, Inspec o Docker para realizar sus respectivas tareas.

7. Referencias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad