Chez Sysnove, on adore Python. Parce qu'il est installé partout, parce qu'il n'a pas besoin d'IDE, parce qu'il est là depuis longtemps et pour encore longtemps, parce qu'il force la lisibilité, parce qu'il est hackable, parce qu'il y a une excellente doc, et surtout parce qu'il sert à tout, du petit script à la grosse application, du système au web et passant par le desktop et le traitement de données, du synchrone à l'asynchrone, etc. C'est le seul langage que je connaisse qui soit aussi complet et assez polyvalent pour se substituer efficacement à une bonne partie des DSL. Bref, quand on est administrateur système, c'est la clé du bonheur.

Mais rien n'est parfait, loin de là, et s'il y a un point sur lequel PHP n'a pas à rougir, c'est la simplicité d'héberger des applications web, que ce soit avec Apache ou derrière Nginx. En Python, c'est une tout autre histoire, mais ça a l'avantage de permettre de comprendre ce que l'on fait, et d'en profiter pour le faire bien.

Le serveur WSGI

Pour commencer, il est nécessaire de comprendre que tout ne fonctionne pas automagiquement comme en PHP, où il suffit de poser ses fichiers PHP sur à coté des fichiers HTML, et ceux-ci seront interprétés à la volée par le serveur web. Sur une application Python, on distingue plus clairement les différents services et protocoles qui entrent en jeux.

  1. Navigateur Web → HTTP → Serveur WSGI → WSGI → Application
  2. Application → WSGI → Serveur WSGI → HTTP → Navigateur Web

D'un coté, vous avez votre application Python, 0bin par exemple. Cette application va recevoir une requête HTTP par le protocole WSGI sous forme d'objet Python. Elle va la traiter, générer une réponse (une page web, par exemple), et envoyer cette réponse sous forme d'objet Python par le protocole WSGI.

De l'autre coté, vous avez le serveur (d'application), qui doit donc être capable de recevoir des requêtes HTTP envoyées par les navigateurs web des visiteurs, de les transformer en objets Python, de les transmettre par le protocole WSGI, d'attendre les réponses sous forme d'objet Python et de les renvoyer au client en HTTP par le réseau.

Les serveurs WSGI sont souvent livrés avec les framework web à des fins de test, typiquement Werkzeug (avec Flask), CherryPy, Bottle ou encore Django. La plupart d'entre eux ne sont pas recommandés en production (CherryPy fait exception, je crois). Pour la production, il existe des serveurs en daemons, tels que Apache (avec mod_wsgi) et Gunicorn.

Dans notre cas, nous sommes assez fans du serveur web Nginx pour de très nombreuses raisons, et notamment pour son efficacité et sa simplicité de configuration en temps que proxy. Malheureusement, il ne gère pas WSGI, et ne peut donc pas communiquer directement avec notre application. Il faut un intermédiaire. Après avoir en avoir testé plusieurs, notamment Gunicorn, nous avons opté pour uWSGI, qui est à la fois le plus rapide en cas de forte charge (Gunicorn se défend légèrement mieux sur des requêtes isolées) et celui qui a la plus faible empreinte mémoire. Il est aussi extrêmement simple à configurer. Enfin, même si ça ne fait pas l'objet de cet article, il s'interface aussi très bien avec d'autres langages que Python. Sous Debian, un simple aptitude search uwsgi-plugin permet de se faire une bonne idée.

Le proxy

Alors pourquoi pas uWSGI directement, sans Nginx ? Parce que Nginx est un excellent proxy qui, en plus de transférer les requêtes à uWSGI, va pouvoir délivrer directement les fichiers statiques (images, css, js…), va faire office de cache, va pouvoir communiquer en HTTPS, et va même compresser les réponses avec gzip. De plus, il sait très bien discuter en HTTP avec les navigateurs, c'est sa spécialité.

Nginx va donc se charger de parser les requêtes HTTP pour les transférer à uWSGI via un autre protocole : uwsgi (attention, uwsgi et WSGI, c'est pas pareil !).

En résumé, ça donne quelque chose comme ça :

  1. Navigateur web → HTTP(s) → Nginx → uwsgi → uWSGI → WSGI → Application
  2. Application → WSGI → uWSGI → uwsgi → Nginx → HTTP(s) → Navigateur web

Et ça s'installe comme ça :

sudo aptitude install python-dev uwsgi uwsgi-plugin-python nginx

Configuration

D'abord l'application…

Pour l'exemple, on va installer 0bin, le pastebin chiffré développé par Sam & Max (nsfw) :

sudo mkdir -p /srv/www/sysnove.net
sudo chown www-data:www-data /srv/www/sysnove.net
sudo -u www-data git clone https://github.com/sametmax/0bin.git /srv/www/sysnove.net/0bin

Reste à configurer la discussion entre le serveur et l'application. Généralement, celle-ci est fournie avec un fichier app.py, app.wsgi, ou tout autre fichier .py ou .wsgi déclarant un objet application. Tous les frameworks compatibles WSGI ont une fonction pour retourner cet objet.

Dans le cas de 0bin, manque de chance, le dépôt ne contient pas ce fichier, il faut donc le créer, dans zerobin/app.py par exemple.

import os, sys

# make sure the zerobin module is in the PYTHON PATH and importable
ZEROBIN_PARENT_DIR = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, ZEROBIN_PARENT_DIR)

# create the wsgi application
from zerobin.routes import get_app
settings, application = get_app()

Pour une application Django, on aurait plutôt fait comme ça :

import os

os.environ["DJANGO_SETTINGS_MODULE"] = "project.settings"

# create the wsgi application
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

Dans ces deux exemples, l'unique objectif est de récupérer l'objet application. Avec 0bin, on utilise une fonction fournie dans zerobin/routes.py alors qu'avec Django on utilise une fonction fournie par le framework. Lorsque le serveur WSGI va charger ce module (nom donné aux fichiers Python), il aura alors accès à l'objet application et pourra lui transmettre les requêtes HTTP en WSGI sous forme d'objets Python, selon la norme WSGI.

…puis le serveur…

Pour le serveur WSGI, il suffit de créer le fichier de configuration de cette application dans uWSGI, ici /etc/uwsgi/app-available/0bin.ini :

[uwsgi]
plugins = python
chdir = /srv/www/sysnove.net/0bin
module = zerobin.app
callable = application

Les deux premières lignes sont évidentes. module correspond au path du fichier (module) Python (zerobin/app.py). uWSGI cherche des modules python, c'est pourquoi nous avons nommé le fichier app.py et non app.wsgi. Enfin, callable correspond à l'objet application (qui peut être nommé autrement, on trouve souvent app).

Enfin, on active le nouveau fichier de configuration à la manière d'Apache ou de Nginx, et on redémarre le serveur.

sudo ln -s /etc/uwsgi/apps-available/0bin.ini /etc/uwsgi/apps-enabled/0bin.ini
sudo service uwsgi restart

…et enfin le proxy

C'est bientôt fini. Il ne reste plus qu'à configurer Nginx pour faire office de proxy entre uWSGI et les navigateurs des visiteurs. On en profite pour lui faire servir directement les fichiers statiques, parce que c'est ce qu'il sait faire de mieux.

Exemple de fichier /etc/nginx/sites-available/0bin.sysnove.net minimaliste (sans ssl, sans compression gzip, sans cache…) :

server {
    listen 80;
    server_name 0bin.sysnove.net;

    access_log  /var/log/nginx/0bin.sysnove.net_access.log;
    error_log   /var/log/nginx/0bin.sysnove.net_error.log;

    location /favicon.ico {
        root /srv/www/sysnove.net/0bin/zerobin/static/img;
    }

    location /static/ {
        root /home/www/sysnove.net/0bin/zerobin;
    }

    location / {
        include uwsgi_params;
        uwsgi_pass unix:/var/run/uwsgi/app/0bin/socket;
    }
}

C'est assez trivial. Nginx va servir directement favicon.ico et tout le répertoire static/ (images, css et js). Pour tout le reste (/), il va transmettre la requête via le protocole uwsgi à la socket UNIX situé dans le chemin donné, socket écoutée par un processus uWSGI, qui va transmettre la requête en WSGI à l'application Python, vous connaissez déjà l'histoire :-)

Enfin, il ne reste plus qu'à activer le nouveau site et à recharger Nginx.

sudo ln -s /etc/nginx/sites-available/0bin.sysnove.net /etc/nginx/sites-enabled/0bin.sysnove.net
sudo service nginx reload

Ça y est, l'application est accessible.

À suivre

Il manque quelque chose à mon exemple. 0bin est livré avec toutes les bibliothèques nécessaires (dans le répertoire libs/). C'est rarement le cas, et beaucoup d'applications sont plutôt livrées avec un fichier requirements.txt contenant la liste des dépendances à installer avec pip. Or l'une des grandes forces de Python est de permettre d'installer simplement ces dépendances dans un environnement limité à l'application, pour que chaque application ait exactement la bonne version de chaque bibliothèque dont elle a besoin.

Au prochain épisode, nous verrons comment installer proprement ces dépendances et comment configurer uWSGI pour utiliser les virtualenv.

Oh, et bonne année à tous ! :-)

À propos de l'auteur



Guillaume est l'un des deux fondateurs et cogérants de Sysnove. Développeur Python et administrateur système passionné de logiciel libre et des technologies liées à Internet, il participe à divers projets, notamment le fournisseur d'accès Internet Aquilenet ainsi que la Fédération FDN.

Chez Sysnove, son rôle consiste à mettre en place et administrer les infrastructures nécessaires à l'hébergement des services fournis aux clients. En tant que directeur général, il gère aussi les aspects administratifs de l'entreprise.