Duckpiler

Proyecto para la asignatura Infraestructura Virtual (UGR). El propósito de este proyecto es desarrollar un microservicio que sirva como asistente en @libreim

View on GitHub

Proyecto_IV

Proyecto para la asignatura Infraestructura Virtual (UGR)

Duckpiler assistant

El proyecto a desarrollar será un servicio que proporcione un pdf que sea el resultado de una compilación de un fichero LaTeX o Markdown.

¿Por qué?

En numerosas ocasiones, trabajando en el repositorio de los apuntes de libreim se ha dado el caso de que algún compañero estaba intentando estudiar pero se encontraba desde algún dispositivo móvil, desde algún sistema operativo inapropiado o simplemente no tenía herramientas de compilación a mano. Por lo que se pensó hacer algún tipo de compilador automático que resuelva este problema.

Añadiendo Integración Continua

Herramientas utilizadas

Para llevar a cabo los tests se empezó usando (dada la simplicidad de los tests actuales) el módulo assert, que se encuentra incluído en node. Pero posteriormente se observó un beneficio por utilizar chai para realizar tests más complejos.

Para ejecutar los tests se ha usado Mocha, pues es un framework bastante extendido y cómodo que nos describe el resultado de la ejecución de los tests junto al tiempo transcurrido (lo cual puede ayudar a la hora de establecer algún benchmark para probar la aplicación).

Para gestionar la integración continua se ha utilizado travis, principalmente por su integración con github y su facilidad de uso.

Para gestionar el despliegue se ha realizado con una máquina virtual en azure, aprovechando el registro que hicimos en los ejercicios del tema introductorio y las suscripciones proporcionadas por el profesor de la asignatura (aunque también existen otras alternativas como Heroku, que también es popular en el uso de node.js o zeit)

La configuración que usará travis para realizar las pruebas de integración continua se debe indicar en el fichero .travis.yml. En el cual indicaremos qué lenguaje estamos usando, su versión, las dependencias necesarias a instalar y el script a usar para probar los tests:

language: node_js

node_js: 
  - "8.12"

before_install:
  - npm install --save-dev mocha
  - npm install mocha
  
script: npm test

Desplegando

Desplegando en Heroku

Se ha elegido Heroku como PaaS para desplegar la aplicación, principalmente por ser una de las mejores herramientas gratuitas que se integran bien con node.js. Proporciona otras facilidades como la sencillez con la que se pueden desplegar aplicaciones con simplemente hacer push al repositorio de Heroku (de hecho, con la configuración adecuada se puede gestionar el push automático con cada push al repositorio de github). Permite seleccionar la región del servidor en la que estará tu aplicación y permite “dormir” o desactivar las aplicaciones cuando estén un tiempo sin ser usadas para permitir ahorrar recursos.

Para crear la app primero debemos pedirle a heroku que la cree en nuestra región. En nuestro repositorio ejecutaríamos:

heroku apps:create --region eu

Esto nos dará una aplicación con un nombre aleatorio. Así que podemos renombrarlo al nombre que queremos.

heroku apps:rename --app nombre-aleatorio-69349 genuine-duckpiler

Y es importante destacar que esto implica que hay que cambiar la url del remote de heroku, pues en caso contrario intentaríamos publicar en un repo de heroku que ya no existe.

git remote set-url heroku https://git.heroku.com/genuine-duckpiler.git

Antes de publicarlo necesitamos añadir a nuestro proyecto un archivo Procfile que indique cómo se debe lanzar la aplicación. Este fichero simplemente tendrá web: sudo npm start. web: implica que es una aplicación web y sudo npm start indica cómo se ha de ejecutar esta aplicación web. Y para publicarlo tendríamos que hacer push a dicho repositorio.

git push heroku master

Si queremos desplegar la aplicación como un contenedor de docker tenemos que indicarlo en el archivo heroku.yml.

build:
  docker:
web: Dockerfile

Con esto tendríamos nuestra aplicación desplegada.

Despliegue aquí: https://genuine-duckpiler.herokuapp.com/

Despliegue automático desde Github

Cuando estamos trabajando es posible que queramos realizar “simultáneamente” el despliegue en Github y en Heroku. Así ahorrarnos tener que hacer push al repo de Github y de Heroku por separado.

Por suerte podemos encontrar un modo de automatizar este proceso, en nuestro caso configuramos desde Heroku el despliegue autorizando el acceso al repo de github. Primero tenemos que conectar la aplicación.

Y para confirmar simplemente activamos las opciones de despliegue automático, que es compatible con CI, para que solo se despliegue en caso de haber pasado los tests de Integración Continua.

Despliegue con Docker en Zeit

Hemos creado en DockerHub un repositorio para nuestro proyecto (al cual se publicará automáticamente al hacer push a Github con un webhook).

Y posteriormente lo hemos desplegado en Zeit con la orden now, que se ejecuta en función del contenido del archivo now.json.

Para el archivo now.json hemos tenido que configurar algunas cosas:

{
    "type":"docker", // Indica que estamos desplegando un contenedor de Docker
    "version": 1,
    "features": { // Previene de la restricción de tamaño
        "cloud": "v1"
    }
}

Contenedor: https://duckpiler-gxwwizygwr.now.sh/

Para el contenedor hemos utilizado un fichero Dockerfile que nos ha permitido configurar cómo se construye dicho contenedor y qué comando hay que usar para ejecutar nuestra aplicación. Y para el despliegue en Zeit hemos proporcionado un archivo de configuración para que se pueda desplegar el contenedor de Docker.

Es destacable acerca del Dockerfile la siguiente configuración:

# Esta es la imagen desde la que queremos construir 
FROM node:8.15.0-jessie

# El directorio (arbitrario) sobre el cual trabajaremos
WORKDIR /usr/src/app

# La instalación de dependencias de npm
COPY package.json ./
RUN npm install

# La copia del código que queramos ejecutar en el servidor
COPY ./src ./src 

# La activación de la escucha en el puerto que de nuestro servicio
EXPOSE 80

# La instalación de los paquetes que se requieran
RUN apt-get update
RUN apt-get install pandoc -y

# El comando que se utilizará para lanzar el contenedor y desplegar la app
CMD [ "npm", "start" ]

Replicación del entorno

¿Quieres probar el servicio en un entorno testeado?

Bien sea para desarrollar o para probarlo por ti mismo puedes hacer uso de vagrant para desplegar una máquina debidamente provisionada con chef. Simplemente tienes que usar vagrant up ya puedes disponer de una máquina con todo lo necesario.

Provisionamiento con chef

Chef-solo es la herramienta que nos va a asegurar que la máquina que despleguemos va a tener todos los paquetes y herramientas necesarias para que nuestra aplicación funcione debidamente.

Chef se basa en instalar “recetas” (recipes). El ejemplo más simple de esto puede ser nuestro archivo git/default.rb, cuyo contenido es el siguiente:

package 'git'

Con lo cual instalará el paquete git allá donde se encuentre instalado chef-solo disponible para ser usado.

Las recetas se agrupan en torno a “cookbooks”. “Libros de cocina”, el directorio que recoge las distintas recetas de nuestro proyecto a cocinar. Y será necesario indicarle a nuestro archivo de configuración cómo se llamará este directorio si no tomará “cookbooks” por omisión (chef.cookbooks_path = "provision").

Además hay otro elemento importante en chef, y estos son los roles, una manera de definir patrones de trabajo o un conjunto de acciones a realizar. Esto nos permite proporcionarle a nuestra máquina una run list, el conjunto de recetas que tendrá que instalar. De este modo sólo tendríamos que proporcionarle en el vagrantfile la ruta y el role a añadir.

El role tiene esta estructura:

{
  "name": "vagrant",
  "chef_type": "role",
  "json_class": "Chef::Role",
  "description": "Vagrant instance, responsible for deploying a VM",
  "default_attributes": {},
  "run_list": [
      "recipe[git]",
      "recipe[npm]",
      "recipe[nodejs]"
  ]
}

Donde se encuentran los metadatos que describen el role y lo más relevante: La run list, que indica las recetas que se han de instalar.

Es necesario destacar que es posible tener más de una receta por programa. Es decir, que podríamos no solamente tener nuestro archivo git/default.rb que instale el paquete, sino que también podríamos tener otra receta para actualizarlo, a la que podríamos llamar git/update.rb. Si quisieramos lanzar esa receta en nuestra run list sería necesario indicar qué receta exactamente queremos instalar (Lo haríamos con recipe[git::update]. Por omisión toma recipe[receta::default].).

Configuración entorno con Azure

Esta configuración se ha hecho para desplegar una máquina virtual de azure con nuestra herramienta Vagrant. Para ello es necesario utilizar un archivo (programa escrito en ruby) Vagrantfile:

# En primer tenemos que elegir la configuración de Vagrant. Algunas
funcionalidades no están disponibles para otras versiones distinta a
la usada ("2")

Vagrant.configure("2") do |config|

  ## Configuramos las máquinas virtuales de azure

  ### Elegimos el nombre de la vagrantbox localmente
  instalada/disponible en HasiCorp's Vagrant Cloud  que
  usaremos levantar la máquina
  config.vm.box = "azure"
  
  ### Proporcionaremos la ruta de nuestra clave privada para
  conectarnos via SSH con nuestra máquina
  config.ssh.private_key_path = '~/.ssh/id_rsa'

  ## A continuación se procede a la configuración exclusiva de azure
  config.vm.provider :azure do |azure, override|
    ### Elegimos el nombre de la máquina virtual, en lugar de utilizar
  uno aleatorio
    azure.vm_name = "duckpiler"

    ### Establecemos el nombre del grupo de recursos a usar. Nos puede
    evitar crear uno con un nombre distinto en cada despliegue, pero
    puede producir errores y demoras si se están realizando numerosos
    despliegues (se puede comentar esta línea si esto supone un problema)
    azure.resource_group_name = "duckpiler_resource_group"

    ### Podemos elegir una ubicación adecuada para nuestras VM para
    mejorar el rendimiento
    azure.location = "westeurope"

    ### Exponemos el puerto en el que nuestra aplicación escuchará
    azure.tcp_endpoints = "80"
    
    ### También es necesario para usar azure configurar un conjunto de
    claves e ID's cuya forma más segura de proporcionar es mediante
    variables de entorno
    azure.tenant_id = ENV['AZURE_TENANT_ID']
    azure.client_id = ENV['AZURE_CLIENT_ID']
    azure.client_secret = ENV['AZURE_CLIENT_SECRET']
    azure.subscription_id = ENV['AZURE_SUBSCRIPTION_ID']
  end


  ## Para el provisionamiento configuramos chef

  config.vm.provision "chef_solo" do |chef|
    ### Proporcionamos la ruta a los cookbooks
    chef.cookbooks_path = "provision"
    ### Ruta a los roles
    chef.roles_path = "provision/roles"
    ### Roles a añadir
    chef.add_role("vagrant")
end 

Replicación del entorno sin Azure

Si quieres conseguir una máquina virtual local que esté igualmente configurada puedes ignorar las especificaciones del Vagrantfile para Azure y usar otro genérico como este:

Vagrant.configure("2") do |config|
  config.vm.box = "nombre_maquina_virtual"
  
  # Redirigir el puerto del servicio a tu puerto 5000 (puede elegirse otro)
  config.vm.network "forwarded_port", guest: 5000, host: 80


  # Install with chef

  config.vm.provision "chef_solo" do |chef|
    chef.add_recipe "emacs"
    chef.add_recipe "git"
  end
end

Esto, tras descargar alguna vagrantbox y sustituir el valor de config.vm.box por el que corresponda permitiría preparar un entorno debidamente provisionado con vagrant up. Lo que dejaría lista la máquina para trabajar con ella con vagrant ssh (una conexión ssh “automatizada” a esta máquina).

Despliegue de la aplicación en entorno remoto

Para desplegar la aplicación en este nuevo entorno hemos usado flightplan (aprovechando que estamos usando nodejs).

Nuestros planes de vuelo

Con este conjunto de planes podemos:

Desplegar de cero la aplicación y tenerla de servicio

fly deployTo:azure --flightplan despliegue/flightplan.js

Detener con otro plan de vuelo la ejecución del servicio

fly stop:azure --flightplan despliegue/flightplan.js

Reanudar el servicio

fly run:azure --flightplan despliegue/flightplan.js

Borrar el repositorio

fly deleteAll:azure --flightplan despliegue/flightplan.js

En definitiva cualquiera puede coger este proyecto y desplegarlo con dos sencillas órdenes:

vagrant up

fly deployTo:azure --flightplan despliegue/flightplan.js

¿Cómo funcionan?

Flightplan, como se puede ver en su documentación es una biblioteca de nodejs, la cual nos permite ejecutar secuencias de comandos shell en hosts locales y remotos. Osea, que para usarlo tendremos que escribir un archivo en javascript. Por lo cual, para usarlo lo primero que tenemos que hacer es describir nuestro target:

plan.target('azure', {
    host: 'duckpiler.westeurope.cloudapp.azure.com',
    username: 'vagrant',
    agent: process.env.SSH_AUTH_SOCK
});

Donde nombramos nuestro target “azure”, e indicamos mediante un objeto javascript el resto de propiedades que nos interesen. En nuestro caso el host (DNS proporcionado por azure), el usuario (“vagrant” al estar por defecto usando vagrant) y el medio de autenticación.

A continuación procedemos a definir los planes de vuelo:

plan.remote(
    'deployTo',
    remote => {
        remote.log('Let\'s run Duckpiler');
        
        // Cloning repository
        remote.exec('git clone https://github.com/jojelupipa/Duckpiler.git');

        // Installing dependencies
        remote.exec('npm install --prefix Duckpiler');

        // Launching service
        remote.exec('sudo npm start --prefix Duckpiler');
    });

Con plan.remote(name, fallback_func); podemos ejecutar una secuencia de órdenes en nuestro host remoto. Entre las cuales se encuentran log(), para imprimir por pantalla, exec() para ejecutar órdenes (la más esencial), sudo() lo propio, pero como superusuario (Hay otras predefinidas como echo, ls, rm… que se usan en otros planes cuya funcionalidad es intuitiva y se pueden consultar en su documentación).

Si quisieramos realizar acciones en el host local se puede utilizar plan.local(fallback_func). Esto puede ser útil para realizar transferencias de archivos local.transfer(files, remote_path). En nuestro caso esto no ha sido necesario pues hemos provisionado las máquinas para que puedan descargarse el repositorio con git.

Normalmente, cuando está desplegada se encuentra la aplicación en este dominio.

Este dominio es proporcionado por azure a la hora de publicar una máquina virtual con un nombre en una localización determinada.