Actualmente me encuentro con la oportunidad de ver nacer una startup. Más bien me toca ser parte de una startup, como responsable de toda decisión tecnológica sobre el proyecto, y participando activamente en su desarrollo como líder de un equipo de 1 integrante (yo :P estamos en proceso de reclutamiento de desarrolladores).

Es común que cuando alguien tiene experiencia con PHP, al momento de encarar un nuevo desarrollo Web vaya como burra al trigo a lo que ya conoce. PHP es una porquería pero al final de día logra hacer el trabajo, mal que mal. Sin embargo, en esta ocasión me zambullí a la pileta y aposté todo por Python. Creo que dada la potencia de Python, disponibilidad de módulos y muchas otras ventajas, vale la pena la apuesta a largo plazo. Además creo personalmente que, más a corto que largo plazo, me va a permitir desarrollar componentes mucho más rápido que PHP.

Ahora bien, para implementar aplicaciones Python en Internet es necesario contar con una interfaz al lenguaje, similar a CGI: WSGI. Y también es necesario un servidor HTTP frontend (para servir contenido estático e incluir soporte para TLS, mecanismos de caching, etc.): Nginx. ¿Por qué Nginx y no Apache? Porque es un servidor HTTP más flexible, eficiente y liviano.

Por último, es altamente recomendable contar con algún tipo de middleware que presente una capa de abstracción a la aplicación Python, para no tener que lidiar con los detalles de bajo nivel de WSGI (parseo de URLs, recuperación de parámetros GET/POST, cookies, headers HTTP, y toda la "magia" que ignoramos al momento de desarrollar en lenguaje PHP porque la hace por nosotros). Cabe destacar que WSGI es una interfaz de más bajo nivel que CGI y se desentiende de su implementación (puede ser multi-procesos o multi-hilada). Para este desarrollo decidí inclinarme por Flask, un microframework de desarrollo de aplicaciones Python basado en el middleware Werkzeug.

En este artículo voy a compartir mi experiencia montando un servidor Web Nginx con soporte para Python a través de uWSGI, con el objetivo de servir una aplicación desarrollada utilizando el microframework Flask.



Como es costumbre (al menos hasta ahora...) parto de un servidor Debian 7. El primer paso consiste en preparar el servidor para la batalla:

# apt-get update && apt-get upgrade

Instalación de Nginx

La instalación de Nginx es desde los fuentes, tal como expliqué anteriormente en el artículo Instalación y configuración de Nginx con PHP-FPM, para contar con la última versión estable.

Instalar las dependencias necesarias para compilar Nginx:

# apt-get install build-essential libpcre3-dev zlib1g-dev libssl-dev

Luego descargar, compilar e instalar Nginx:

# wget https://nginx.org/download/nginx-1.10.1.tar.gz
# tar xvf nginx-1.10.1.tar.gz
# cd nginx-1.10.1/
# ./configure --with-http_ssl_module
# make
# make install

Instalar el script de inicio System V para Nginx:

# cd
# git clone https://github.com/Fleshgrinder/nginx-sysvinit-script.git
# cd nginx-sysvinit-script/
# make
# ln -s /usr/local/nginx/sbin/nginx /sbin/

Luego configurar Nginx para que guarde su PID dentro del directorio /run:

# nano /usr/local/nginx/conf/nginx.conf

pid        /run/nginx.pid;

Iniciar Nginx ejecutando:

# service nginx start

Instalación de uWSGI

uWSGI es una implementación de WSGI capaz de servir aplicaciones Python, pero que además apunta a proveer la pila de software completa para implementar servicios de hosting. Aunque en esta configuración lo uso detrás de un frontend Nginx.

La instalación se puede realizar a través del gestor de paquetes de Python: pip.

Instalar pip:

# apt-get install python-pip

Luego es posible instalar uWSGI:

# apt-get install python2.7-dev
# pip install uwsgi

Al finalizar el proceso de instalación muestra la configuración de uWSGI:

    ################# uWSGI configuration #################
   
    pcre = True
    kernel = Linux
    malloc = libc
    execinfo = False
    ifaddrs = True
    ssl = True
    zlib = True
    locking = pthread_mutex
    plugin_dir = .
    timer = timerfd
    yaml = embedded
    json = False
    filemonitor = inotify
    routing = True
    debug = False
    ucontext = True
    capabilities = False
    xml = expat
    event = epoll
   
    ############## end of uWSGI configuration #############
    total build time: 37 seconds
    *** uWSGI is ready, launch it with /usr/local/bin/uwsgi ***
   
Successfully installed uwsgi
Cleaning up...

Instalación de Flask

Instalar el framework Flask con pip:

# pip install flask

En este momento es posible crear la primera aplicación desarrollada con Flask. Para alojar la aplicación creo un directorio, por ejemplo dentro de /usr/local:

# mkdir -p /usr/local/app
# cd /usr/local/app/
# nano app.py
from flask import Flask

application = Flask(__name__)

@application.route("/")
def index():
  return "<h1>Working like a charm!</h1>"

if __name__ == "__main__":
  application.run()

A diferencia de cómo los desarrolladores PHP estamos acostumbrados, cuando se desarrolla una aplicación WSGI existe un único punto de entrada a la misma. En cambio con PHP (y otros lenguajes), los puntos de entrada a una aplicación son todo archivo con extensión .php a partir de cierto directorio.

Que exista un único punto de entrada a la aplicación, significa que un único archivo .py consume todas las rutas que llegan a través de la URL. Este es un cambio que, al momento de desarrollar y pensar la aplicación, tal vez cueste un poco adaptarse.

Para verificar el funcionamiento es posible lanzar la aplicación (en background, o mandar luego a background con Ctrl+Z y bg):

root@debian:/usr/local/app# python app.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
^Z
[1]+  Stopped                 python app.py
root@debian:/usr/local/app# bg 1
[1]+ python app.py &

Verificar el funcionamiento utilizando la herramienta curl o wget:

root@debian:/usr/local/app# curl http://localhost:5000
127.0.0.1 - - [29/Sep/2016 12:44:57] "GET / HTTP/1.1" 200 -
<h1>Working like a charm!</h1>root@debian:/usr/local/app#

Se observa la respuesta enviada por la aplicación, tal como fue definida en la función index(). Detener la aplicación (Ctrl+C):

<h1>Working like a charm!</h1>root@debian:/usr/local/app# fg 1
python app.py
^Croot@debian:/usr/local/app#

Perfecto, la aplicación Flask funciona correctamente.

A continuación, es necesario crear el punto de entrada WGSI:

# nano wsgi.py
from app import application

if __name__ == "__main__":
  application.run()

Esto permite el acceso a la aplicación a través de WSGI, lo que posibilita verificar el funcionamiento del servidor uwsgi antes de integrarlo con Nginx.

Detener Nginx para que libere el puerto 80 y lanzar uwsgi:

root@debian:/usr/local/app# service nginx stop
root@debian:/usr/local/app# uwsgi --socket 0.0.0.0:80 --protocol=http -w wsgi
*** Starting uWSGI 2.0.13.1 (64bit) on [Thu Sep 29 13:06:23 2016] ***

Al acceder desde un navegador (también se puede utilizar una vez más curl o wget) se observa la respuesta de la aplicación:

Ahora se debe configurar el servidor uWSGI a través de un archivo .ini:

# nano wsgi.ini
[uwsgi]
uid = www-data
chdir = /usr/local/app
module = wsgi

master = true
processes = 5

socket = /usr/local/app/app.sock
chmod-socket = 600
vacuum = true

die-on-term = true

La configuración del servidor varía en cada instalación. Para este proyecto, y a modo de prueba, utilizo 5 procesos para servir solicitudes. Se observa que se ha configurado para que corra con el mismo usuario que Nginx (www-data) de esta forma se evita tener que utilizar un socket Unix con permisos demasiado abiertos. Notar que en la configuración cambia al directorio base de la aplicación para localizar adecuadamente al módulo wsgi.py. El socket será utilizado más adelante por Nginx para reenviar las peticiones al servidor uWSGI.

Como el servidor va a correr como el usuario www-data, es necesario cambiar los permisos en el directorio (para que pueda crear el socket):

# chown -R www-data:www-data /usr/local/app

Finalmente resta instalar un script de inicio System V para el servicio uwsgi. En mi caso decidí descargar el script de inicio de uwsgi-emperor para Debian y realizarle unas modificaciones menores:

# cd /etc/init.d/
# wget https://raw.githubusercontent.com/linuxitux/uwsgi/master/debian/uwsgi-emperor.init.d
# mv uwsgi-emperor.init.d uwsgi
# nano uwsgi

Luego de modificarlo, queda así:

#!/bin/sh
### BEGIN INIT INFO
# Provides:          uwsgi
# Required-Start:    $local_fs $remote_fs $network
# Required-Stop:     $local_fs $remote_fs $network
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Start/stop uWSGI server
# Description:       This script manages uWSGI server.
### END INIT INFO

# Author: Janos Guljas <janos@debian.org>

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
DESC="uWSGI server"
NAME="uwsgi"
DAEMON="/usr/local/bin/uwsgi"
PIDFILE=/run/uwsgi.pid
INIFILE=/usr/local/app/wsgi.ini
LOGFILE=/var/log/uwsgi.log
DAEMON_ARGS="--ini ${INIFILE} --pidfile ${PIDFILE} --daemonize ${LOGFILE}"
SCRIPTNAME="/etc/init.d/uwsgi"
ENABLED=yes

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions

#
# Function that starts the daemon/service
#
do_start()
{
  # Return
  #   0 if daemon has been started
  #   1 if daemon was already running
  #   2 if daemon could not be started
  if [ "$ENABLED" != yes ]; then
    [ "$VERBOSE" != no ] && log_progress_msg "(disabled)"
    return 2
  fi
  start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
    || return 1
  start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
    $DAEMON_ARGS 1> /dev/null 2>&1 \
    || return 2
}

#
# Function that stops the daemon/service
#
do_stop()
{
  # Return
  #   0 if daemon has been stopped
  #   1 if daemon was already stopped
  #   2 if daemon could not be stopped
  #   other if a failure occurred
  start-stop-daemon --stop --quiet --retry=QUIT/30/KILL/5 --pidfile $PIDFILE --name $NAME
  RETVAL="$?"
  [ "$RETVAL" = 2 ] && return 2

  start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
  [ "$?" = 2 ] && return 2

  rm -f $PIDFILE
  return "$RETVAL"
}

#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
  start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
  return 0
}

case "$1" in
  start)
  [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
  do_start
  case "$?" in
    0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
    2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
  esac
  ;;
  stop)
  [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
  do_stop
  case "$?" in
    0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
    2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
  esac
  ;;
  status)
  status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
  ;;
  reload|force-reload)
  log_daemon_msg "Reloading $DESC" "$NAME"
  do_reload
  log_end_msg $?
  ;;
  restart)
  log_daemon_msg "Restarting $DESC" "$NAME"
  do_stop
  case "$?" in
    0|1)
    do_start
    case "$?" in
      0) log_end_msg 0 ;;
      1) log_end_msg 1 ;; # Old process is still running
      *) log_end_msg 1 ;; # Failed to start
    esac
    ;;
    *)
    # Failed to stop
    log_end_msg 1
    ;;
  esac
  ;;
  *)
  echo "Usage: $SCRIPTNAME {start|stop|status|restart|reload|force-reload}" >&2
  exit 3
  ;;
esac

:

Configurar los permisos, instalar, y ejecutar el script de inicio:

# chmod +x uwsgi
# update-rc.d uwsgi defaults
# service uwsgi start

El servidor uWSGI inicia correctamente:

root@debian:/usr/local/app# ps aux | grep wsgi
www-data 13288  1.0  1.6  75808 16480 ?        S    10:03   0:00 /usr/local/bin/uwsgi --ini /usr/local/app/wsgi.ini --pidfile /run/uwsgi.pid --daemonize /var/log/uwsgi.log
www-data 13308  0.0  1.2  75808 13016 ?        S    10:03   0:00 /usr/local/bin/uwsgi --ini /usr/local/app/wsgi.ini --pidfile /run/uwsgi.pid --daemonize /var/log/uwsgi.log
www-data 13309  0.0  1.2  75808 13016 ?        S    10:03   0:00 /usr/local/bin/uwsgi --ini /usr/local/app/wsgi.ini --pidfile /run/uwsgi.pid --daemonize /var/log/uwsgi.log
www-data 13310  0.0  1.2  75808 13020 ?        S    10:03   0:00 /usr/local/bin/uwsgi --ini /usr/local/app/wsgi.ini --pidfile /run/uwsgi.pid --daemonize /var/log/uwsgi.log
www-data 13311  0.0  1.2  75808 13020 ?        S    10:03   0:00 /usr/local/bin/uwsgi --ini /usr/local/app/wsgi.ini --pidfile /run/uwsgi.pid --daemonize /var/log/uwsgi.log
www-data 13312  0.0  1.2  75808 13020 ?        S    10:03   0:00 /usr/local/bin/uwsgi --ini /usr/local/app/wsgi.ini --pidfile /run/uwsgi.pid --daemonize /var/log/uwsgi.log
root     13315  0.0  0.0   7840   876 pts/0    S+   10:03   0:00 grep wsgi
root@debian:/usr/local/app# netstat -a | grep app
unix  2      [ ACC ]     STREAM     LISTENING     48581    /usr/local/app/app.sock

Configuración de Nginx

El último paso consiste en conectar todo el circuito. Para ello se debe configurar Nginx para que reenvíe las peticiones al servidor uWSGI:

 
# nano /usr/local/nginx/conf/nginx.conf

Esta es una configuración básica (toda solicitud es procesada por la aplicación uWSGI) que luego deberá adaptarse para que el contenido estático sea servido directamente por Nginx en vez de uWSGI:

        location / {
                include uwsgi_params;
                uwsgi_pass unix:/usr/local/app/app.sock;
        }

Verificar el acceso nuevamente:

¡Éxito!

Referencias

Quickstart for Python/WSGI applications

PEP 333 -- Python Web Server Gateway Interface v1.0

Getting Started with WSGI

nginx+uWSGI vs Apache - why we switched. - PythonAnywhere News

How To Serve Flask Applications with uWSGI and Nginx on Ubuntu 14.04 - DigitalOcean

Serving Flask With Nginx - Vladikk

Designing a RESTful API with Python and Flask - miguelgrinberg.com

Implementing a RESTful Web API with Python & Flask - Luis Rei

RESTful Web Services With Python Flask - DZone Integration


Tal vez pueda interesarte


Compartí este artículo