異なるバージョンのPython(wsgi)アプリを一つの開発環境上で動かす

# はじめに

自分の開発用Linux環境には、OSのパッケージ管理コマンド経由でPython 2.7とDjango 1.4がインストールされています。今作っているアプリはこの環境下で動作するので問題はございませんでした。

あるとき、リリースが近づいているDjango 1.7と、ついでだからPython 3.4を組み合わせて、一体どういう困難が待ち受けているかを試そうと思い立ちました。

要は先行技術の調査を同じ環境下で行いつつ、既存の開発環境を破壊しない、という状況を達成したいわけです。

しかし、この「1つのOS,複数のPythonとDjango」は案外難しいのです。前回試したときには (http://qiita.com/amedama/items/79994598d9f4daa69d13 )、Apache2のmod_wsgiを複数種類持てないという問題で止まっていました。

今回は代替策として、独立したwsgiサーバを同じホスト上に立てて、おまけとしてApache2からリバースプロキシを設定することでこの問題に対応してみます。

具体的には

 * aptで入れたPython2.7 + django 1.4 環境
 * pyvenv と pip を駆使した Python3.4 + django 1.7 環境

の2環境を一つの開発環境上で同時並行に起動する方法を探ります。

更におまけとして、実際にDjango 1.7の互換性のない変更にぶちあたったりもしましたので、そちらもちょっと紹介します。

尚、記事の最後に実際にターミナル上で本記事の内容（の一部）を実践した動画7分を紹介してます。記事を読むか動画を見るか両方愉しむかは、読者の方の好きにどうぞ。

# 主な登場人物

 * Python2.7 ... 安定のPython2系
 * Python3.4 ... Python3期待の星
 * Apache2 ... フロントエンド。mod_wsgi経由でwsgiサーバにもなるが、mod_wsgi1つは1つのPython環境しか提供出来ないため、上に乗っかるwsgiアプリはPython2系のどれかかPython3系のどれか一種類でしか動かなくなる。Linux1環境にApacheバイナリを2つ入れるというのは若干狂気に見えたのでやってない。
 * django 1.4 ... 古い子
 * django 1.7 ... 生まれてないベータ
 * xbuild ... Python3.4のビルドを最初に行うときに使う
 * virtualenv ... Pythonの環境を作るもの
 * pyvenv ... Python3系で導入されたvirtualenvみたいなの
 * tornado ... 単体動作するwsgiサーバ
 * supervisor ... デーモンプロセスを束ねるデーモン。今回はwsgiサーバを束ねる
 * /opt ... 今回Djangoプロジェクト等はここに打ち込む

# djangoアプリの views.py

今回、views.pyの内容自体は本質的ではないので、以下のものを流用します。
Pythonのバージョンとdjangoのバージョンをプレーンテキストで返すだけです。

```py:views.py
from django.http import HttpResponse
import django
import sys

def home(request):
    version = ('Python: {}\nDjango: {}\n'
               .format(sys.version.replace('\n', ' '),
                       django.get_version()))
    return HttpResponse(version, content_type="text/plain")
```

# manage.py runserverまで動かす
## aptで入れたもの(Python2.7 + django 1.4)

tornadoやdjango (古い方) などがあらかじめ準備できているものとします。
sudo pipでも(Debianなら)apt-getでもいいと思います。

蛇足: pipだとバージョン指定なくば1.6系が入ります。今回の記事ではDebian wheezy (7.5) のapt-get install python-djangoが一番記事の結果に近くなります。Ubuntu 12.04LTSは筆者が知っている限りではDjango 1.3, Ubuntu 14.04LTSでは1.6が入ります。要は環境ごとに違います。重要なのはDjango 1.7は入らないということです。Python 3.4についてはUbuntu 14.04LTSではapt-getで入ってしまいます。

 * ```django-admin startproject test1```
 * settings.pyのsqlite3に関する設定をする。
     * db/db.sqlite3 をファイル保存先とし、www-dataから書き込めるようにする)
 * views.pyとurls.pyを変更
 * ```python mangae.py syncdb```
 * ```python manage.py runserver```
     * ブラウザで Python 2.7.3, Django 1.4.5 の文字列を確認

## pyvenvとpipを駆使したもの(Python3.4 + django 1.7)

(二度venv作ってるのですがベターな方法はないものでしょうか)

    $ xbuild/python-install 3.4.0 /opt/python3.4.0
    $ cd /opt
    $ /opt/python3.4.0/bin/pyvenv /tmp/venv
    $ source /tmp/venv/bin/activate
    (venv)$ pip install https://www.djangoproject.com/download/1.7b3/tarball/
    (rehash on zsh)
    (venv)$ python --version
    Python 3.4.0
    (venv)$ django-admin.py --version
    1.7b3
    (venv)$which pip
    /opt/test2/venv/bin/pip
    (venv)$ django-admin.py startproject test2
    (venv)$ cd test2
    (venv)$ deactivate
    (rehash on zsh)
    $ /opt/python3.4.0/bin/pyvenv venv
    $ source venv/bin/activate
    (venv) $ pip install https://www.djangoproject.com/download/1.7b3/tarball/
    (rehash on zsh)

 * settings.pyのsqlite3のdb保存先を```db/db.sqlite3```へ変える
 * パーミッションを変更
 * views.pyとurls.pyを変更
 * ```python mangae.py syncdb```
 * ```python manage.py runserver```
     * ブラウザで Python 3.4.0, Django 1.7b3 の文字列を確認

# tornadoを用いてコマンドライン上からwsgiサーバを立ち上げる

なお、参考にしたのは以下の記事でした。

 * http://www.tornadoweb.org/en/stable/index.html
 * https://github.com/facebook/tornado
 * https://github.com/bdarnell/django-tornado-demo

今回は```tornado_main.py```をプロジェクトのルートにさらに次のファイルを配置することでお茶を濁します。

(内容は https://github.com/bdarnell/django-tornado-demo とほぼ同じですが、一点、ポート番号の変更をコマンドライン上から出来るように変更してあります。なお、今回と関係のないHelloHandlerは、単に消すのを忘れました)

```py:tornado_main.py
from tornado.options import options, define, parse_command_line
import django.core.handlers.wsgi
import tornado.httpserver
import tornado.ioloop
import tornado.web
import tornado.wsgi

define('port', type=int, default=18000)

class HelloHandler(tornado.web.RequestHandler):
  def get(self):
    self.write('Hello from tornado')


def main():
    parse_command_line()
    wsgi_app = tornado.wsgi.WSGIContainer(
        django.core.handlers.wsgi.WSGIHandler())
    tornado_app = tornado.web.Application(
        [('/hello-tornado', HelloHandler),
         ('.*', tornado.web.FallbackHandler, dict(fallback=wsgi_app)),
         ])

    server = tornado.httpserver.HTTPServer(tornado_app)
    server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()

if __name__ == '__main__':
  main()
```

(なお後述する通り、 django 1.7 で動かす時にはこのtornado_main.pyの実装では動作しません)


## apt-getで用意した方

    $ DJANGO_SETTINGS_MODULE=test1.settings python tornado_main.py

Pythonとdjangoのバージョンを確認

## pyvenvとpipの方

    (venv)$ pip install tornado
    (venv)$ PYTHONPATH=venv/lib/python3.4/site-packages DJANGO_SETTINGS_MODULE=test2.settings python tornado_main.py

Pythonとdjangoのバージョンを確認

# supervisorでデーモンとして両wsgiアプリを起動させる

今回、supervisorはaptで入れておきます。上記のように分離して管理するメリットは、supervisorについてはない気がします。

(なお、apt-getでそのまま入る3.0a8でもいいのですが、自分はunstableからソース (3.0r1.1) を取ってきてビルドしました。今回の記事には関係ないはずですが、参考まで)

aptでsupervisorをインストールした後、```/etc/supervisor/conf.d/```配下に次のような設定をぶち込みます。

```/etc/supervisor/conf.d/test.conf
[program:test1]
command=python tornado_main.py --port=18101
directory=/opt/test1
autostart=true
autorestart=true
user=www-data
environment = DJANGO_SETTINGS_MODULE="test1.settings"

[program:test2]
command=/opt/test2/venv/bin/python tornado_main.py --port=18102
directory=/opt/test2
autostart=true
autorestart=true
user=www-data
environment = PYTHONPATH="/opt/test2/venv/lib/python3.4/site-packages", DJANGO_SETTINGS_MODULE="test2.settings"
```

両方動きましためでたしめｄ

## App registory isn't ready yet.

2.7の方は良いのですが、3.4 の方がこれでは動作しません。

具体的には長大な例外を吐いて落ちます。/var/log/supervisor/supervisord.log を見つつ状況を確認します。

原因はdjango 1.7の仕様変更にありました。

https://docs.djangoproject.com/en/dev/releases/1.7/

    If you’re using Django in a plain Python script — rather than a management command — and you rely on the DJANGO_SETTINGS_MODULE environment variable, you must now explicitly initialize Django at the beginning of your script with:
    
    >>> import django
    >>> django.setup()

あ、はい。

```py:tornado_main2.py
from tornado.options import options, define, parse_command_line
import django
import django.core.handlers.wsgi
import tornado.httpserver
import tornado.ioloop
import tornado.web
import tornado.wsgi

define('port', type=int, default=18000)

class HelloHandler(tornado.web.RequestHandler):
  def get(self):
    self.write('Hello from tornado')


def main():
    // For django 1.7
    django.setup()
    parse_command_line()
    wsgi_app = tornado.wsgi.WSGIContainer(
        django.core.handlers.wsgi.WSGIHandler())
    tornado_app = tornado.web.Application(
        [('/hello-tornado', HelloHandler),
         ('.*', tornado.web.FallbackHandler, dict(fallback=wsgi_app)),
         ])

    server = tornado.httpserver.HTTPServer(tornado_app)
    server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()

if __name__ == '__main__':
  main()
```

これで両方同じ開発環境で動くようになりました。


# おまけ Apcahe2 でリバースプロキシ

ポート番号も覚えてられないのでリバースプロキシまでやっておくのは良い案かも知れません。
(ただ、supervisordとApacheで独立にポート番号を管理することになるので、後々めんどくさくなるかもしれません)

mod_proxy等を有効にし、```ProxyPass```や```ProxyPassReverse```を設定します。省略

 * http://localhost/test1
 * http://localhost/test2

などとやってPythonとdjangoのバージョンが異なることを確認します。

# 参考

微妙に最後までいけてませんが、実際にターミナル上で実行してみました

https://www.youtube.com/watch?v=l9gvX1oMOF4