Hacer auto deploy con Bitbucket y GIT II

En el primer artículo de la serie vimos como crear un repositorio público (podríamos haberlo hecho privado perfectamente) en Bitbucket y registramos un servicio de POST para el mismo.

También comprobamos que el servicio estaba funcionando y que el objeto JSON enviado que contenía la información de nuestro repositorio y commit estaba bien formado y era correcto.

Hoy vamos a hacer una primera prueba sencilla de auto-deploy previa batería de tests para que el lector pueda ir haciéndose una ligera idea de lo que podemos hacer con muy poco esfuerzo.

Pero antes de nada, debemos escribir el servicio que va a encargarse de capturar ese POST enviado por Bitbucket y realizar las operaciones adecuadas. Este servicio se ejecutará en la máquina destino a la que tenemos acceso por SSH.

Servicio de Despliegue

El servicio de despliegue de nuestra aplicación está programado en Python utilizando las librerías Twisted (tal y como hemos hecho con el código de la aplicación en si) pero podría estar escrito en cualquier otro lenguaje.

Nuestro servicio es muy sencillo, escucha conexiones entrantes por el puerto 1936 y delega todo el trabajo a un módulo que se encarga de parsear la petición, ejecutar los tests y realizar el deploy, vamos a verlo:

from twisted.web import server, resource
from deployer import worker
class DeploymentResource(resource.Resource):
        """
        Process the POST petition and skip the GET ones
        I use twistd service to run, I'm a RPY Script
        """
    isLeaf = True
    def render_GET(self, request):       
        return "Nothing to see here!"   
    def render_POST(self, request):      
        deployer_worker = worker.Deployer(request)       
        return server.NOT_DONE_YET
resource = DeploymentResource()

El anterior archivo crea un objeto de tipo twisted.web.resource.Resource que procesa la petición HTTP proveniente de Bitbucket. No usamos un reactor de Twisted por que es un script de recursos de Twisted y se usa con la utilidad de línea de comandos twistd.

Como podéis apreciar, todo el trabajo es delegado a un worker que vamos a definir en su propio package (deployer) que va a encargarse de hacer el trabajo duro:

# -*- test-case-name: deployer.test_deployer -*-
import json
import subprocess
import os
class Deployer(object):
    def __init__(self, request):
        self._request = request
        self._data = json.loads(request.args['payload'][0])
        html = [
            '<!doctype html>',
            '<html>',
            '   <head>',
            '       <title>Genbetadev Auto Deployer</title>',
            '       <style type="text/css">',
            '           .error { color: red; }',
            '           .success { color: green; }',
            '       </style>',
            '   </head>',
            '   <body>'
        ]
        self._request.write("\n".join(html))     
        self._request.write('<br />Executing tests...')
        self._check()
    def _check(self):
        if self._check_tests():
            self._request.write('<br />%s' % self._out.replace("\n", "<br />"))
                        print self._out
            self._request.write('<br /><span class="cuccess">All tests passed... deploying</span>')
                        print 'All tests passed... deploying'
            self._deploy();
        else:
                        print self._out          
            self._request.write('<span class="error">Tests failed</span>:<br />%s<br /><br /><b class="error">Deploy Cancelled</b>' % self._out.replace("\n", "<br />"))      
                        print 'Tests failed... not deploying'
        self._request.write('    </body>');
        self._request.write('</html>');
        self._request.finish()   
    def _check_tests(self):       
        process = subprocess.Popen(['trial', 'deployer'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        self._out, self._error = process.communicate()                
        if process.returncode == 0:
            return True
        else:
            return False            
    def _deploy(self):       
        currpwd = os.getcwd()       
        os.chdir('/var/www/localhost/htdocs/deploytest')        
        process = subprocess.Popen(['git', 'pull'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        self._out, self._error = process.communicate()
                os.chdir(currpwd)
                print self._out
        self._request.write('<br /><br />%s' % self._out.replace("\n", "<br />"))
        if process.returncode == 0:
            process = subprocess.Popen([''])
            self._request.write('<br /><b class="error">Project Deployed</b>') 
        else:
            self._request.write('<br /><b class="error">Project not Deployed</b>') 

NOTA: Un copy and paste de este código seguramente no te funcione a no ser que mantengas las mismas rutas, esto no pretende ser un tutorial paso a paso sino una explicación de como realizar un auto deployment con Bitbucket.

Este archivo es algo más largo y se encarga de realizar todo el trabajo efectivo de testeo y deployment de nuestro código. Como podéis observar, nuestro worker ejecuta una batería de tests con trial y si se pasan todos los tests, entonces procede a realizar el despliegue.

Tests

Vamos a escribir el código de los tests, para ello debemos crear un nuevo package tests dentro de package deployer:

$ mkdir deployer/tests
$ touch deployer/tests/__init__.py

Dentro de ese package podemos crear nuestros archivos de tests que serán ejecutados por trial antes de desplegar nuestros commits, este va a llamarse test_worker.py:

from twisted.trial import unittest
class WorkerTest(unittest.TestCase):
    def test_worker(self):
        self.assertEqual(1, 2)

Este test siempre fallará ya que 1 nunca va a ser igual a 2, lo dejamos así para comprobar que nuestro worker trabaja como se espera de él.

KISS

Para facilitar las cosas, vamos a modificar el código al que llamamos webservice.py en el artículo anterior para convertirlo en un script de recursos de Twisted:

from twisted.web import server, resource
class HelloResource(resource.Resource):
    isLeaf = True
    numberRequests = 0    
    def render_GET(self, request):
        self.numberRequests += 1
        request.setHeader("content-type", "text/plain")
        return "I am request #" + str(self.numberRequests) + "\n"
resource = HelloResource()

Es necesario renombrar el archivo a webservice.rpy para que sea interpretado por twistd. Y no olvidemos subir los cambios al repositorio git, esto enviará otro POST a nuestro PostBin, agua que no has de beber...

Pues bien, ya tenemos todos los elementos para probar el servicio. Lo primero que tenemos que hacer es volver a las opciones de administración de nuestro repositorio y cambiar la URL a donde se debe dirigir el POST para que apunte a nuestro servicio. En mi caso sería algo como: http://xxx.xxx.xxx.xxx:1936/deployer.rpy

Vamos a clonar el repositorio git de nuestra aplicación en la máquina destino y a hacer nuestra prueba:

~ cd /var/www/localhost/htdocs
$ git clone https://bitbucket.org/damnwidget/deploytest.git

Hemos clonado el repositorio público por HTTP, también podíamos haber clonado un repositorio privado (y hacer deploy de él) utilizando una key SSH. Revisa la documentación de Bitbucket si quieres saber más sobre ello.

Levantamos los servicios con twisted:

$ cd /var/www/localhost/htdocs/deploytest
$ twistd web --path ./ --port 8080
cd /var/www/localhost/htdocs/deployer_service
$ twistd web --path ./ --port 1936

Esto levanta dos reactores con twisted uno con el servicio a desplegar que escucha en el puerto 8080 y el servicio que realiza los tests y el despliegue en el 1936, ya solo nos queda hacer un cambio al repositorio y ver si esto funciona.

Para ello vamos a ir a lo simple y vamos a crear un nuevo archivo vacío en el repositorio:

$ touch deleteme
git add deleteme
git commit -m "File 'deleteme' added to tree for testing purposes"
git push

En el log de nuestro servicio de despliegue obtenemos la siguiente petición:

2012-02-17 21:59:37+0100 [HTTPChannel,0,63.246.22.222] 63.246.22.222 - - [17/Feb/2012:20:59:36 +0000] "POST /deployer.rpy HTTP/1.1" 200 628 "-" "Bitbucket.org"
y un extenso log de como ha ido la ejecución de los tests.

Como podemos comprobar, han fallado puesto que uno nunca puede ser igual a dos y eso es lo que se está testeando. Si vuestro test ha pasado y no ha dado fail es debido a que en la configuración de vuestra instalación de Python no se añade el directorio actual en la ruta de Python, para corregir esto debéis levantar el servicio de despliegue de la siguiente manera:

$ PYTHONPAHT=$(pwd) twistd web --path ./ --port 1936

De esta manera nos aseguramos de que el comando trial encuentra el módulo deployer para realizar los tests.

Ahora que hemos comprobado que el servicio está funcionando correctamente, vamos a modificar el test para que trial lo pase y vamos a introducir un cambio en nuestro webservice.rpy para comprobar que efectivamente nuestra página web se actualiza a la última versión del repositorio.

Si apuntamos nuestro navegador a http://nuestra_url:8080/webservice.rpy nos encontraremos con una página que dice "I am request #1", vamos a hacer un ligero cambio y hacer un commit para comprobar que todo funciona. Primero modificamos el test en el servicio de despliegue para que pase, en la línea 7 de test_worker.py:

self.assertEqual(1, 1)

El test pasará ya que uno siempre es igual a uno, vamos a proceder pues a modificar nuestro webservice.rpy. Ahora el nuevo código de nuestra página:

from twisted.web import server, resource
class HelloResource(resource.Resource):
    isLeaf = True
    numberRequests = 0    
    def render_GET(self, request):
        self.numberRequests += 1
        request.setHeader("content-type", "text/plain")
        return "Hello, Genbetadev\n"
resource = HelloResource()

Antes de realizar el commit, es conveniente matar el proceso twistd de nuestro deployer y volverlo a levantar:

kill -9 $(cat twistd.pid) && PYTHONPATH=$(pwd) twistd web --path=./ --port=1936

Al hacer push de nuestro código esta vez el log reflejará lo siguiente:

2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] deployer.tests.test_deployer
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101]   WorkerTest
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101]     test_worker ...                                                        [OK]
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] 
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] -------------------------------------------------------------------------------
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] Ran 1 tests in 0.002s
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] 
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] PASSED (successes=1)
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] 
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] All tests passed... deploying
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] Updating e3d26b8..25619d1
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] Fast-forward
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] webservice.rpy | 12 ++++++++++++
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] 1 files changed, 12 insertions(+), 0 deletions(-)
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] create mode 100644 webservice.rpy

Si recargamos la página web obtendremos el siguiente mensaje:

Hello, Genbetadev

Espero que toda esta explicación se haya entendido porque creo que realmente es más sencillo hacerlo que explicarlo, es una forma muy sencilla y trivial de poder desplegar nuestras aplicaciones o páginas web en producción o testeo de forma sencilla.

El lector debe notar que esta no es la forma correcta de pasar un test, si nos fijamos, el test se lo estamos pasando al servicio de despliegue y no al servicio web, lo lógico en una aplicación real sería que el test se lo pasemos a aquello que vamos a desplegar, no al servicio de depliegue.

Tampoco hemos hecho nada útil con el JSON que nos da Bitbucket, en próximas entregas de la serie crearemos un auténtico servicio de auto despliegue con aplicación real.

Hasta entonces, happy hacking!

Más en Genbetadev | Hacer auto deploy con bitbucket y git (parte 1), Sistemas de Control de Versiones

Portada de Genbeta