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:
- 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.
- 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:
- Identificamos que versión esta corriendo según nuestro archivo llave /usr/local/apps/live
- Vamos a la versión del código que no está en producción (no live)
- Activamos el entorno virtual
- Instalamos las dependencias
- Ejecutamos las migraciones
- Generamos los archivos estaticos, mensajes u otro proceso para tu software
- Hacemos restart al proceso de supervisor de la versión no live
- Verificamos que corra el proceso de la versión no live
- Desabilitamos el sites-available de Apache la configuración que opera actualmente (live)
- Habilitamos en el sites-available de Apache la versión no live
- Cambiamos la key en el archivo llave
- 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.
- Primero, hacer el deploy donde quitas las referencias en el código a la columna a eliminar.
- Deploy con la migración que elimina dicha columna.
- Siguiendo el caso anterior, si vas a renombrar una columna
- 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.
- 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.