uWSGI on OpenWrt

以下の内容は OpenWrt 19.x の時点でのやや古いものであり当面の間は残しておくが、最近の 23.05 に基いた新しい文書を別途作成したので、通常は新しい方を参照して欲しい。

OpenWrt ルーターに Django を導入したいと思ったので、それにあたってはその下準備として、アプリケーションサーバーを整えておく必要がある。Python 系のアプリケーションサーバーとしては uWSGI が定番のようなので、まずここでは uWSGI 環境の構築について一通り行いたいと思う。

luci-ssl-nginx

OpenWrt では luci-ssl-nginx というパッケージがあり、これを導入するとデフォルトの uHTTPd 環境の LuCI に代えて、NGINX(かつ SSL)環境の LuCI が動くようになる。この環境において、Lua プログラムである LuCI に NGINX が CGI としてリレーするために、uWSGI が使われている。この場合は、uWSGI はあくまでも CGI を扱うためのアプリケーションサーバーの一種として使われているだけで、Python 固有の WSGI サーバーとして使われているわけではないが、それでも NGINX ⇔ uWSGI 間のプロトコルは uwsgi プロトコルが使われている点は特筆すべき点だろう(NGINX は uwsgi プロトコルにネイティブ対応している)。

NGINX が uwsgi プロトコルを使うということは、設定ファイル中で uwsgi_* プレフィックスを使った設定が行え、uwsgi_pass でソケットを通じて uWSGI にリレーできることを意味している。

例えば、OpenWrt の luci-ssl-nginx を入れた状態では uWSGI 関連の設定は次のようになっている。

/etc/nginx/luci_uwsgi.conf(/etc/nginx/nginx.conf からインクルードされている)

location /cgi-bin/luci {
  index  index.html;
  include uwsgi_params;
  uwsgi_param SERVER_ADDR $server_addr;
  uwsgi_modifier1 9;
  uwsgi_pass unix:////var/run/luci-webui.socket;
}
location ~ /cgi-bin/cgi-(backup|download|upload|exec) {
  include uwsgi_params;
  uwsgi_param SERVER_ADDR $server_addr;
  uwsgi_modifier1 9;
  uwsgi_pass unix:////var/run/luci-cgi_io.socket;
}

location /luci-static {
}

location /ubus {
        ubus_interpreter;
        ubus_socket_path /var/run/ubus.sock;
        ubus_parallel_req 2;
}

uWSGI と関係しているのは、/cgi-bin/luci と /cgi-bin/cgi-(backup|download|upload|exec) のブロック。いずれも uwsgi_pass で UNIX ソケットに繋いでいる。uwsgi_modifier1 を 9 にしているということは、これはプロトコル的に CGI モードである(👉 The uwsgi Protocol)。NGINX と uWSGI は本来、uwsgi プロトコルで直結できるのだが、現状の OpenWrt ではこのように汎用的な CGI としての設定にしてあるようである(👉 OpenWrt 公式ガイド)。

LuCI on nginx is currently supported by using uwsgi as plain-cgi interpreter.

以下、uWSGI 側の設定について見てみる。

/etc/uwsgi/emperor.ini

[uwsgi]
strict = true
pidfile = /var/run/uwsgi.pid
emperor = /etc/uwsgi/vassals/*.ini
early-emperor = true
vacuum = true
emperor-on-demand-directory = /var/run/
emperor-required-heartbeat = 99
vassal-set = die-on-idle=true

uWSGI は emperor.ini が子プロセスの起動などを行っているようだが、具体的な個別の設定はこの emperor.ini にインクルードされている vassals/*.ini にある。LuCI の WebUI 用の .ini と、LuCI が呼び出す入出力用の CGI のための .ini の 2 種が定義されている。

/etc/uwsgi/vassals/luci-webui.ini

[uwsgi]
strict = true
if-not-env = UWSGI_EMPEROR_FD
socket = /var/run/luci-webui.socket
chmod-socket = 666
cheap = true
end-if =
plugin = cgi
cgi-mode = true
cgi = /www/
chdir = /usr/lib/lua/luci/
buffer-size = 10000
reload-mercy = 8
max-requests = 2000
limit-as = 200
reload-on-as = 256
reload-on-rss = 192
enable-threads = true
post-buffering = 8192
socket-timeout = 120
thunder-lock = true
plugin = syslog
logger = luci syslog:uwsgi-luci
log-route = luci luci:
disable-logging = true
req-logger = syslog:uwsgi-luci
log-format=%(method) %(uri) => return %(status) (%(rsize) bytes in %(msecs) ms)
threads = 3
processes = 3
cheaper-algo = spare
cheaper = 1
cheaper-initial = 1
cheaper-step = 1
master = true
idle = 360
/etc/uwsgi/vassals/luci-cgi_io.ini

[uwsgi]
strict = true
if-not-env = UWSGI_EMPEROR_FD
socket = /var/run/luci-cgi_io.socket
chmod-socket = 666
cheap = true
end-if =
plugin = cgi
cgi-mode = true
cgi = /www/
chdir = /usr/lib/lua/luci/
buffer-size = 10000
reload-mercy = 8
max-requests = 2000
limit-as = 200
reload-on-as = 256
reload-on-rss = 192
no-orphans = true
post-buffering = 8192
socket-timeout = 120
thunder-lock = true
plugin = syslog
disable-logging = true
req-logger = syslog:uwsgi-luci-cgi_io
log-format=%(method) %(uri) => return %(status) (%(rsize) bytes in %(msecs) ms)
chmod-socket = 666
cgi-safe = /usr/libexec/cgi-io
cgi-dontresolve = true
cgi-close-stdin-on-eof = true
cheap = true
idle = 360

ログファイルの設定など色々と細かい設定が並んでいるが、特記すべきなのは、socket を定義している点と、plugin として cgi を使い、その cgi のルートパスを設定している点だろう。socket は NGINX 側のものと対応しているのがわかるし、NGINX 側でも CGI プロトコルに設定してあるのだから、uWSGI 側でも CGI モードで設定するのは当然である。

Python プログラムを CGI として動かす

バッファーサイズ等の細かい調整は無視して、とりあえず CGI としてなら次のような設定で Python プログラムを動かすことができる。

/etc/uwsgi/vassals/cgi.ini

[uwsgi]
socket = /var/run/wsgi.socket
plugin = cgi
cgi = /mnt/data/www/default

そして NGINX 側でも、この socket に対して CGI プロトコルで接続する設定を適当な場所に追加する。

/etc/nginx/nginx.conf(抜粋)

    server {
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name _;
        return 301 https://$host$request_uri;
    }

    server {
        listen 443 ssl default_server;
        listen [::]:443 ssl default_server;
        server_name  localhost;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        ssl_ciphers "EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:DHE+AESGCM:DHE:!RSA!aNULL:!eNULL:!LOW:!RC4:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!CAMELLIA:!SEED";
        ssl_session_tickets off;

        ssl_certificate /etc/nginx/nginx.cer;
        ssl_certificate_key /etc/nginx/nginx.key;

        location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
            expires 365d;
        }

        include luci_uwsgi.conf;
+
+       location ~* \.py$ {
+            include uwsgi_params;
+            uwsgi_param SERVER_ADDR $server_addr;
+            uwsgi_modifier1 9;
+            uwsgi_pass unix:/var/run/wsgi.socket;
+       }

        root /mnt/data/www/default;
    }

Python プログラムを WSGI として動かす

結構正解に辿り着くまで苦労したのが、Python プログラムを CGI としてではなく、本来の WSGI として動かす設定についてである。

正解に辿り着いてしまえば、それほどのことでもないのだが、ポイントは、NGINX 側ではちゃんとプロトコルを uwsgi にすること(デフォルトなので uwsgi_modifier1 を記述しなければ 0 になるのだが、当初 LuCI 用の .ini の設定をコピーしたため、9 に指定のまま CGI プロトコルになってしまっていたために却ってうまく動かなかった)、wsgi.ini 側で plugin に python を指定することである。そうすれば、wsgi-file で指定した Python プログラムが実行される(下の例では wsgi-file ではなくより複雑な mount 設定を使っている)。

とりあえず、最終的に動くようになった例が以下:


opkg install uwsgi-python3-plugin

まずは uWSGI の Python プラグインをインストールしておく必要がある。

/etc/nginx/nginx.conf(抜粋)

    server {
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name _;
        return 301 https://$host$request_uri;
    }

    server {
        listen 443 ssl default_server;
        listen [::]:443 ssl default_server;
        server_name  localhost;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        ssl_ciphers "EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:DHE+AESGCM:DHE:!RSA!aNULL:!eNULL:!LOW:!RC4:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!CAMELLIA:!SEED";
        ssl_session_tickets off;

        ssl_certificate /etc/nginx/nginx.cer;
        ssl_certificate_key /etc/nginx/nginx.key;

        location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
            expires 365d;
        }

        include luci_uwsgi.conf;
+
+       location ~* \.py$ {
+            include uwsgi_params;
+            uwsgi_param SERVER_ADDR $server_addr;
+            #uwsgi_modifier1 0; # wsgi: 0; psgi: 5; cgi: 9
+            uwsgi_pass unix:/var/run/wsgi.socket;
+       }

        root /mnt/data/www/default;
    }
/etc/uwsgi/vassals/wsgi.ini

[uwsgi]
#protocol = http
plugin = python
socket = /var/run/wsgi.socket
chdir = /mnt/data/www/default
mount = /env.py=env.py
#stats = 192.168.10.1:9191
/mnt/data/www/default/env.py

def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/plain')])
    query_string = env['QUERY_STRING']
    request_method = env['REQUEST_METHOD']
    content_type = env['CONTENT_TYPE']
    content_length = env['CONTENT_LENGTH']
    request_uri = env['REQUEST_URI']
    document_path = env['PATH_INFO']
    document_root = env['DOCUMENT_ROOT']
    server_protocol = env['SERVER_PROTOCOL']
    request_scheme = env['REQUEST_SCHEME']
    https = env['HTTPS']
    remote_addr = env['REMOTE_ADDR']
    remote_port= env['REMOTE_PORT']
    server_addr = env['SERVER_ADDR']
    server_port = env['SERVER_PORT']
    server_name = env['SERVER_NAME']
    content = f'''\
QUERY_STRING:\t{query_string}
REQUEST_METHOD:\t{request_method}
CONTENT_TYPE:\t{content_type}
CONTENT_LENGTH:\t{content_length}

REQUEST_URI:\t{request_uri}
PATH_INFO:\t{document_path}
DOCUMENT_ROOT:\t{document_root}
SERVER_PROTOCOL:\t{server_protocol}
REQUEST_SCHEME:\t{request_scheme}
HTTPS:\t{https}

REMOTE_ADDR:\t{remote_addr}
REMOTE_PORT:\t{remote_port}
SERVER_ADDR:\t{server_addr}
SERVER_PORT:\t{server_port}
SERVER_NAME:\t{server_name}
'''
    return [content.encode()]
https://(IP)/env.py にブラウザでアクセスした結果
QUERY_STRING:   
REQUEST_METHOD: GET
CONTENT_TYPE:   
CONTENT_LENGTH: 

REQUEST_URI:    /env.py
PATH_INFO:  /env.py
DOCUMENT_ROOT:  /mnt/data/www/default
SERVER_PROTOCOL:    HTTP/1.1
REQUEST_SCHEME: https
HTTPS:  on

REMOTE_ADDR:    192.168.10.109
REMOTE_PORT:    53328
SERVER_ADDR:    192.168.10.1
SERVER_PORT:    443
SERVER_NAME:    localhost

最後に

最初のうち、理解しにくくややこしいく感じたのが、サーバー(デーモンプログラム)同士である、uWSGI と NGINX の間は、uwsgi プロトコルで接続されていて、それは socket 設定行によって示されている(Web サーバー側が uwsgi プロトコル非対応の場合は socket 設定ではなく http-socket 設定になるだろうし、Web サーバーを別途置かずに uWSGI に Web サーバーも兼用させる場合は http 設定になる)という話と、Python プログラムと uWSGI の間は WSGI で接続する場合と従来の CGI で接続するという場合があるという話が、ごっちゃになっていた点かもしれない。

uWSGI に http プロトコルをしゃべらせれば、NGINX は不要なのだが、なぜあまりそういう使い方をしないのかという点も、すぐに納得行かず、一日モヤモヤしていた。だが、mount 設定でイチイチ REQUEST_URI と実際のパスの対応付けを予め列挙しておかなければならないのを見てもわかるように、uWSGI は本来、あまり静的コンテンツ処理向きのものではない。なので基本的には静的コンテンツは NGINX で捌きつつ、動的コンテンツのみピンポイントで uWSGI にリレーするという運用方法が標準的に想定されたものなのだろうと思う。

まあともかく、Django を導入するにあたっての下準備としての uWSGI いじりはこのくらいまでやっておけば十分だと思うので、ここまでで一応終わりにすることにする。

コメント

このブログの人気の投稿

清水俊史『上座部仏教における聖典論の研究』

シークエンスパパともの先見の明

シークエンスパパとも 本物の霊能力