desarrollo Linux Software

Deploy para Django sin downtime… a la antigua!

Lo de a la antigua tal vez sea prejuicioso. Quiero aclarar antes, que este tipo de deploy lo he implementado en ambientes donde en su mayoria solo se tiene un servidor. En este servidor se encuentra funcionando todo el sistema, de manera interna, generalmente por políticas de la empresa, seguridad, datos, etc..- No existe la posibilidad de llevar esto «a la nube» (en servidores de terceros), con varias instancias y balanceadores frontales todo separado -cada uno haciendo lo suyo-. En resumidas es tan solo un servidor y nada mas.

Tampoco está la idea de agregar la «magia» de los contenedores por la capa adicional y todo lo que esto incluye internamente en los recursos -totalmente discutible y daria para un largo tema-. Asi que todo está hecho a la antigua (bare metal)

No está demás indicar algunas consideraciones que debemos tener:

  1. Codigo versionado en un SCV (Sistema Control de Versiones). Podrías no tener esto, pero si estás desarrollando software y no tienes versionado el código en un sistema que se encargue de esto es casi un suicidio.
  2. Test suite del software. Importante, al menos debes considerar hacer tests automatizados a las partes mas críticas del sistema, repito, al menos. Si no existen, tienes la excusa perfecta para dar un punto de partida.

Como un gran resumen y a manera de spoiler, la técnica se denomina green blue. Solo lo aplicaremos a la parte del software y en este caso a un proyecto en Django. Esta técnica puede ser aplicada a lo que estimes conveniente, ya sea web server (varias instancias o internamente), base de datos u otro componente de tu sistema. Así que seria como la version green-blue recortada, pobre y a la antigua, todo esto a modo de ejemplo.

Llamaremos a nuestro proyecto payinv. En mi caso tengo un copia interna en Gitlab, donde automatizadamente corren los test en CI. Y si todo sale bien en la rama master se gatilla el deploy, (estos tests tambien podrían correr en el mismo servidor). Entre medio de los tests corren el linter y coverage pero esto no tiene mucho que ver con el tema central del deploy.

La idea general es tener dos copias de tu proyecto corriendo en el sistema, esto es:

  • /usr/local/apps/payinv-a
  • /usr/local/apps/payinv-b

Ambas están enganchadas mediante (upstream) a un GIT remoto. Todo lo relacionado con configuraciones a la base de datos, keys u otras están seteadas en variables de entorno.

En un archivo «llave» (/usr/local/apps/live) existe apuntada la edición que está funcionando -lease producción-. El contenido de ese archivo es solo a o b.

Usamos Supervisor + Apache -Nginx es una mejor opción, Apache2 es el ejemplo que tengo a mano ahora- y Gunicorn

En supervisor se generan dos configuraciones en /etc/supervisor/conf.d/payinv.conf

[program:payinv-a]
command=/usr/local/apps/payinv-a/env3/bin/gunicorn payinv.wsgi:application --bind 127.0.0.1:%(process_num)04d --pid /tmp/gunicorn-%(process_num)04d.pid 
directory=/usr/local/apps/payinv-a/src
numprocs=5
numprocs_start=8001
process_name=%(program_name)s_%(process_num)04d

[program:payinv-b]
command=/usr/local/apps/payinv-b/env3/bin/gunicorn payinv.wsgi:application --bind 127.0.0.1:%(process_num)04d --pid /tmp/gunicorn-%(process_num)04d.pid 
directory=/usr/local/apps/payinv-b/src

numprocs=5
numprocs_start=8100
process_name=%(program_name)s_%(process_num)04d

Como puedes ver existen dos grupos de procesos que se ejecutan, el payinv-a donde se levantan 5 desde el puerto 8001-8005 y el payinv-b del 8100:8104. También puedes notar que cada cópia del código tiene su propio virtualenv, el que he denominado env3 (por Python 3, si estás en la versión 2 por favor migra ya!)

En Apache, viven dos sites-availables, uno llamado payinv-a.conf y el payinv-b.conf. Un ejemplo recortado para payinv-a.conf es

<IfModule mod_ssl.c>
    <VirtualHost *:443>
        # Custom config here     
        # ...

        <Proxy balancer://payinvs>        
            BalancerMember http://127.0.0.1:8001
            BalancerMember http://127.0.0.1:8002
            BalancerMember http://127.0.0.1:8003
            BalancerMember http://127.0.0.1:8004
            BalancerMember http://127.0.0.1:8005    
        </Proxy>    

        Alias /static /usr/local/apps/payinv-a/src/static    
        <Directory  /usr/local/apps/payinv-a/src/static>
            Require all granted    
        </Directory>

        ProxyPass /static !
        ProxyPreserveHost On
        ProxyPass / balancer://payinvs/        
        ProxyPassReverse / balancer://payinvs/    
        # Certs here    
        # ...
    </VirtualHost>
</IfModule>

En el ejemplo anterior he quitado todo lo relacionado a certificados, configuraciones dominio, logs y otros casos que no son relevantes para el ejemplo. Para el archivo payinv-b.conf debes ajustar los puertos para los BalancerMember y la ruta del código.

¿Comó opera todo esto?. En cada deploy se ejecutan los siguientes pasos:

  1. Identificamos que versión esta corriendo según nuestro archivo llave /usr/local/apps/live
  2. Vamos a la versión del código que no está en producción (no live)
  3. Activamos el entorno virtual
  4. Instalamos las dependencias
  5. Ejecutamos las migraciones
  6. Generamos los archivos estaticos, mensajes u otro proceso para tu software
  7. Hacemos restart al proceso de supervisor de la versión no live
  8. Verificamos que corra el proceso de la versión no live
  9. Desabilitamos el sites-available de Apache la configuración que opera actualmente (live)
  10. Habilitamos en el sites-available de Apache la versión no live
  11. Cambiamos la key en el archivo llave
  12. Hacemos un reload a Apache2

Los puntos anteriores los tengo en un archivo que llamo deploy.sh que esta contenido en el mismo proyecto. Este tiene el siguiente aspecto

#!/bin/bash
LIVE=/usr/local/apps/live


if [[ "$(cat $LIVE)" == "a" ]]; then
    RUNNING="a"
    NORUNNING="b"
else
    RUNNING="b"
    NORUNNING="a"
fi


source env3/bin/activate
pip install -r requirements.txt

python src/manage.py migrate
python src/manage.py check --deploy
python src/manage.py collectstatic --no-input
python src/manage.py compilemessages -l es


supervisorctl restart "payinv-$NORUNNING:*"

echo $NORUNNING > $LIVE
a2dissite "payinv-$RUNNING.conf"
a2ensite "payinv-$NORUNNING.conf"
systemctl reload apache2

El hook que gatilla dicho deploy.sh tiene esta forma

#!/bin/bash
LIVE=/usr/local/apps/live
CODEPATH=/usr/local/apps/payinv-

KEY="a"
if [[ "$(cat $LIVE)" == "a" ]]; then
    KEY="b"
fi

# Go not running enviroment
cd "$CODEPATH$KEY"
git pull
bash deploy.sh

Temas a considerar a todo lo anterior:

  • Los archivos estaticos son versionados y se comparten en entre a y b, los cuales se mantienen por un tiempo. Esto permite que una versión cacheada en el browser no sea pierda. Para esto uso whitenoise
  • Si existen cambios de migraciones estas debe ser compatibles hacia atrás (backward compatibility)
  • Si hubiera algo que se rompe con una migración, por ejemplo, eliminación de una columna, debes hacer esto en dos pasos de deploy.
    1. Primero, hacer el deploy donde quitas las referencias en el código a la columna a eliminar.
    2. Deploy con la migración que elimina dicha columna.
  • Siguiendo el caso anterior, si vas a renombrar una columna
    1. Agregar una nueva columna y copias el contenido de la antigua mediante una migracion. En el código usas la referencia a la nueva columna.
    2. Borras la columna antigua mediante una migración.
  • Podrias mejorar esto, la copia no en ejecución (no live) podria estar detenida en el supervisor.
  • El código de tu proyecto no podria estar enganchado a GIT, si no como un export, pero tendrías que aplicar tus ajustes necesarios a lo anterior.

Nota: Si llegaste hasta aqui puede que interesen los temas que escribo. Puedes seguirme en Twitter o suscribirte a los nuevos artículos

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.