Python CGI プログラマのための WSGI 移行記録

Apache2 MPM-ITK on Debian 9 Jessie を想定しています。

10 年近く書いてきた CGI を、昨今の WSGI に準拠したものへと移行するに当って大変苦労した記録です。技術的なことだけではなく、心情的なことも含んでいます。

まず最初に書いてしまうと、CGI も WSGI も呼び出しと結果の受け渡しが違うだけで、プログラムの内容や動作には殆ど関係ありません(細かく言うと実行の仕方も違うはずですが、そこはどちらかというとサーバの都合だと思うので今回は考慮しません)。なので単に今まで書いた CGI を WSGI に移行するだけなら、あまりメリットは感じないどころか環境構築が煩雑過ぎて疲れるだけです。

そもそもが CGI をどのような用途に使っているかによって、WSGI に移行すべきかどうかの判断が異なってきます。Raspberry pi やそれに近い組込み系の環境で単なる Web インターフェースとして使うのであれば、CGI の簡潔さは今なお有用です。ターミナル上で HTML の出力を確認しながらデバッグといったことも CGI なら簡単です。

CGI の代替となる技術が登場した背景には、CGI の実行に「無駄」が多いとされたことがあります。CGI へのアクセスがある度に Web サーバはプロセス(インタープリタ等)を起動します。アクセス数が多くなるとこの手順はサーバにとって負荷になります。インタープリタは常に同じものを使うことが多いので、当然ながらインタープリタは常時起動しておけばいいのではないかという考えに至ります。Web サーバと Web アプリケーション間のやり取りには様々なインターフェースが開発されましたが、それを統一したものの一つが WSGI という規格です。Python という言語の中から生れた WSGI はその後、他の言語にも派生しました。

さて、いよいよ Apache2 で WSGI を使って Python スクリプトを動かす準備に入ります。既に Apache2 で CGI が動いているなら実は変更することは 2 つです。

  • mod_wsgi のインストールと設定
  • AddHandler に wsgi-script を加える

mod_wsgi は Debian 環境で apt 一発インストールです。このとき、Python 2.x を使うのか Python 3.x を使うのかによってパッケージが異なるので注意しましょう。今回は Python 3.x を使う予定なので Python 3.x に対応した "libapache2-mod-wsgi-py3" をインストールします。パッケージがあるか確認してインストールします。

$ sudo apt search mod-wsgi
Sorting... Done
Full Text Search... Done
libapache2-mod-wsgi/stable,now 4.5.11-1 amd64
 Python WSGI adapter module for Apache

libapache2-mod-wsgi-py3/stable,now 4.5.11-1 amd64
 Python 3 WSGI adapter module for Apache

$ sudo apt install libapache2-mod-wsgi-py3

"sudo a2enmod wsgi" としなくても自動で有効化されていました。

Apache2 の設定ファイル(*.conf)に加えるのは次の一行だけ。<Directory /var/www> </Directory> の中などに書き加えましょう。

AddHandler wsgi-script .wsgi .py

WSGI はインターフェースの名前であって、スクリプト自体は Python だから拡張子を変えるのは間違っている気もしますが、それを言ったらどんな言語でも拡張子を .cgi とするのもおかしな話になるので、気にしないでおきましょう。

わかりにくいと思うので 000-default.conf を書き換えた例を示します。今回は実験用の独立したサーバなので全て許可した設定していますが、本番環境ではくれぐれも必要のないものまで許可しないようにしましょう。

...
DocumentRoot /var/www
<Directory /var/www>
    AllowOverride all
    Options ExecCGI
    AddHandler wsgi-script .wsgi .py
    Require all granted
</Directory>
...

設定を書き換えたら Apache2 を restart または reload します。

$ sudo systemctl reload apache2

または

$ sudo systemctl restart apache2

動作を確認するために簡単な WSGI スクリプトを書いてみます。基本の Hello, world. で。

def application(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')])
    yield '<p>Hello, world!</p>'.encode('utf-8')

これを /var/www/hello.wsgi として保存して http://[サーバのIPアドレス等]/hello.wsgi にアクセスして表示されたら完了です。CGI と違って chmod +x で実行権限を与える必要もありません。

ついでに調べた情報についても記しておきます。

def application() は呼び出し時にこの名前が呼ばれるので必須です。面倒だからと def app() にすると "404 Not Found" と怒られます。

引数の environ, start_response は別名にしても問題ありません。つまり、引数名使ったキーワード呼び出しは使わず、引数の順番しか定義されていません。これは仕様書でもはっきり次のように書かれています。

The application object must accept two positional arguments. For the sake of illustration, we have named them environ and start_response, but they are not required to have these names. A server or gateway must invoke the application object using positional (not keyword) arguments. (E.g. by calling result = application(environ, start_response) as shown above.)

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

なので、例えば次のように書いても良いわけです。殆どが environ, start_response で書かれているようなので、そんなところで要らない個性を発揮する必要はないと思いますが、さっと動作確認したいときには省略したくなりますからね。

def application(e, r):
    r('200 OK', [('Content-type', 'text/html; charset=utf-8')])
    yield '<p>Hello, world!</p>'.encode('utf-8')

application() を呼び出したときの戻り値は Iteratable である必要があるので、例えば複数の出力をしたい場合は次の 2 通りのパターンで行えます。結果はどちらも同じです。普通に return バイト列 とやると 500 Internal Server Error でハマります。

yield バイト列

def application(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')])
    yield '<title>Hello, WSGI!</title>'.encode('utf-8')
    yield '<p>This is a WSGI sample.</p>'.encode('utf-8')

return バイト列のリスト

def application(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')])
    return ['<title>Hello, WSGI!</title>'.encode('utf-8'), <p>Hello, world!</p>'.encode('utf-8')]

謝辞

この記事を書くに当って、れお氏(@reoreo125)には多大なる貢献を頂戴したこと、誠に感謝申し上げます。

参考