OpenWrt で uWSGI 環境を整える
OpenWrt(23.05)に Web サーバーとして NGINX(SSL 版)を利用し、uWSGI をアプリケーションサーバーとして連携する方法について記す。
前提状況:USB フラッシュドライブ
WWW 用のデータを置く場所として USB フラッシュの外部ドライブを用意(👉 OpenWrt での USB フラッシュドライブ)し、さらに Extroot 化(👉 OpenWrt のストレージを Extroot 化する)していることを前提としている。
LuCI もろとも Web サーバー(HTTPd)を SSL 対応 NGINX 化する
以前の OpenWrt 18.x とは飛躍的に進歩して、19.07 以降では SSL 対応版の NGINX が opkg として用意されている(以前は SSL 対応にするためには自前で Linux ソースコードからモジュールをビルドする必要があった)のみならず、NGINX 版 LuCI がセットアップされている opkg すら用意されており、OpenWrt コミュニティの旺盛な活動を感じる(👉 LuCI on other web servers > LuCI on nginx)。
opkg update
opkg install luci-ssl-nginx
opkg remove luci-ssl
opkg remove luci
reboot
デフォルトでインストールされている LuCI/uHTTPd の方は不要になるのでアンインストールした。
OpenWrt ルーターで websocket サーバーを運用したいと思ったので、その下準備として、アプリケーションサーバーを整えておく必要がある。Python 系のアプリケーションサーバーとしては uWSGI が定番であり、最近のバージョンでは websocket にもデフォルトで対応しているようなので、まずここでは uWSGI 環境の構築について一通り行いたいと思う。
luci-ssl-nginx
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/conf.d/luci.locations(/etc/nginx/uci.conf.template から起動時の初期化処理においてインクルードされる)
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 {
error_log stderr crit;
}
location /ubus {
ubus_interpreter;
ubus_socket_path /var/run/ubus/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/ucode/luci/
buffer-size = 10000
reload-mercy = 8
max-requests = 2000
limit-as = 1000
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
; the regular expression leaves for successful de/activation only one line each:
log-route = luci ^(?!... Starting uWSGI |compiled with version: |os: Linux|nodename: |machine: |clock source: |pcre jit |detected number of CPU cores: |current working directory: |detected binary path: |uWSGI running as root, you can use |... WARNING: you are running uWSGI as root |chdir.. to |your processes number limit is |limiting address space of processes...|your process address space limit is |your memory page size is |detected max file descriptor number: |lock engine: |thunder lock: |uwsgi socket |your server socket listen backlog is limited to |your mercy for graceful operations on workers is |mapped .* bytes |... Operational MODE: |initialized CGI path: |... no app loaded. going in full dynamic mode ...|... uWSGI is running in multiple interpreter mode ...|spawned uWSGI worker |announcing my loyalty to the Emperor...|workers have been inactive for more than |SIGINT/SIGQUIT received...killing workers...|worker .* buried |goodbye to uWSGI.|...gracefully killing workers...|Gracefully killing worker|worker .* killed successfully)
disable-logging = true
req-logger = syslog:uwsgi-luci
log-format=%(method) %(uri) => return %(status) (%(rsize) bytes in %(msecs) ms)
threads = 1
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/ucode/luci/
buffer-size = 10000
reload-mercy = 8
max-requests = 2000
limit-as = 1000
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 側でも uwsgi_modifier1 によって CGI プロトコル(9)に設定してあるのだから、uWSGI 側でも CGI モードで設定するのは当然である。
Python プログラムを CGI として動かす
バッファーサイズ等の細かい調整は無視して、とりあえず CGI としてなら次のような設定で Python プログラムを動かすことができる。
/etc/uwsgi/vassals/python-cgi.ini
[uwsgi]
plugin = cgi
socket = /var/run/python-cgi.socket
; URL パスを使う場合の、ローカル側のパスのルートとなるプレフィックス。
cgi = /www
そして NGINX 側でも、この socket に対して CGI プロトコルで接続する設定を適当な場所に追加する。
/etc/nginx/conf.d/python.locations
location ~* \.py$ {
include uwsgi_params;
uwsgi_param SERVER_ADDR $server_addr;
uwsgi_modifier1 9;
uwsgi_pass unix:////var/run/python-cgi.socket;
}
Python プログラムを WSGI として動かす
正解に辿り着くまでに結構苦労した点が、Python プログラムを CGI としてではなく、本来の WSGI として動かす設定についてである。
今こうして一旦正解に辿り着いてしまえば、それほどのことでもないのだが、ポイントは、NGINX 側ではちゃんとプロトコルを uwsgi にすること(デフォルトなので uwsgi_modifier1 を記述しなければ 0 になるのだが、試行錯誤していた当初、LuCI 用の .ini の設定をコピーしたものを叩き台にしたため、9 に指定した状態のまま CGI プロトコルになってしまっていたために却ってうまく動かなかった)、wsgi.ini 側で plugin に python を指定することである。そうすれば、wsgi-file オプションで指定した Python プログラムが実行される(下の例では wsgi-file オプションではなくより複雑な mount オプションを使っている)。
とりあえず、最終的に動くようになった例が以下:
まずは uWSGI の Python プラグインをインストールしておく必要がある。
opkg install uwsgi-python3-plugin
その上で nginx と uwsgi を設定する:
/etc/nginx/conf.d/wsgi.locations
location /wsgi {
include uwsgi_params;
uwsgi_param SERVER_ADDR $server_addr;
uwsgi_modifier1 0;
uwsgi_pass unix:////var/run/wsgi.socket;
}
/etc/wsgi/vassals/wsgi.ini
[uwsgi]
plugin = python
socket = /var/run/wsgi.socket
chdir = /www/wsgi
mount = /wsgi/test=test.wsgi
/www/wsgi/test.wsgi
def application(env, start_response):
start_response('200 OK', [('Content-Type','text/plain')])
return [b'test ok!']
https://(IP)/wsgi/test にブラウザでアクセスした結果
test ok!
uWSGI で websocket アプリを運用する
結論から先に言うと、今回の環境(素の OpenWrt 23.05)でこれは不可能だった。
最近の uWSGI 自体は、デフォルトで websocket 対応している(“The uWSGI websockets implementation is compiled in by default.”)ようなのだが、OpenWrt で用意された uWSGI のバイナリーイメージは、websocket 非対応でコンパイルされている。この StackOverFlow の回答にもあるように、OpenWrt 上では uwsgi コマンドの help 表示のオプション一覧に、https 関係のオプションが表示されないので、https 対応状態でコンパイルされていないことが確認できる。
websocket プロトコルの規格では、非セキュア通信(ws://)の場合でも、ハンドシェイク確立時にはセキュア通信(https://)を必須としているので、https 対応状態でコンパイルされていることが必要となる。実際に、上述の uWSGI の公式ドキュメントのテスト用のシンプルなエコーサーバーの WSGI プログラムを実行してみると、uWSGI のログに次のようなエラーが記録されていた:
you need to build uWSGI with SSL support to use the websocket handshake api function !!! Traceback (most recent call last): File "echo.wsgi", line 5, in application uwsgi.websocket_handshake(env['HTTP_SEC_WEBSOCKET_KEY'], env.get('HTTP_ORIGIN', '')) OSError: unable to complete websocket handshake
ハンドシェイクに失敗しており、“you need to build uWSGI with SSL support to use the websocket handshake api function” と。
そこで、デフォルト状態でコンパイルされた uWSGI を使えるようにする方法を探ったが、その場合、OpenWrt とは直接関係無しに、pip から独立した Python モジュールとしての uWSGI をインストールする方法が紹介されていた。pip をインストールするだけでも、かなり FLASH 容量を消費するが、さらに pip から uWSGI をインストールしようとしたところ、その処理過程で gcc によるコンパイルが必要となってエラー。OpenWrt の gcc パッケージはさすがに巨大で、今回実験機として使っている WZR-HP-AG300H の残りの FLASH 容量ではとうてい無理である。
まあ、OpenWrt の uWSGI パッケージは、あくまでも、管理画面の LuCI のために、NGINX で Lua プログラムを CGI として動かすために用意されているので、uWSGI 単独で動かすことを想定されていないのだろう。LuCI は現状では、標準出力を通じて(つまり CGI モードで)NGINX と連携しており、WSGI 化すらされていない。そんな状況だから、当分は、OpenWrt の uWSGI パッケージが、websocket 対応でコンパイルされることは期待薄である。
最後に
最初のうち、理解しにくくややこしく感じたのが、サーバー(デーモンプログラム)同士である、uWSGI と NGINX の間は、uwsgi プロトコルで接続されていて、それは socket 設定行によって示されている(Web サーバー側が uwsgi プロトコル非対応の場合は socket 設定ではなく http-socket 設定になるだろうし、Web サーバーを別途置かずに uWSGI に Web サーバーも兼用させる場合は http 設定になる)という話と、Python プログラムと uWSGI の間は WSGI で接続する場合と従来の CGI で接続するという場合があるという話が、ごっちゃになっていた点かもしれない。試行錯誤の末に結果を出した、その後になってから見つけた情報なのだが、Django のための uWSGI の公式チュートリアルに、この辺りの関係性を下図のように分類してわかりやすく解説してあるものがあった(先に見付けていればもっと楽ができたと思うが、自分は Django は視野に入れていなかったせいもあって、スルーしていたのだと思う)。
the web client ↔ the web server ↔ the socket ↔ uwsgi ↔ Django the web client ↔ uWSGI ↔ Python the web client ↔ uWSGI ↔ Django the web client ↔ the web server the web client ↔ the web server ↔ the socket ↔ uWSGI ↔ Python
また、uWSGI に http プロトコルをしゃべらせれば、NGINX は不要なのだが、なぜあまりそういう使い方をしないのかという点も、すぐに納得行かず、一日モヤモヤしていた。だが、mount 設定でイチイチ REQUEST_URI と実際のパスの対応付けを予め列挙しておかなければならないのを見てもわかるように、uWSGI は本来、あまり静的コンテンツ処理向きのものではない。なので基本的には静的コンテンツは NGINX で捌きつつ、動的コンテンツのみピンポイントで uWSGI にリレーするという運用方法が標準的に想定されたものなのだろうと思う。また、OpenWrt デフォルトの uWSGI は、LuCI 用のみを想定していて、そのような NGINX をプロキシとして利用する設定となっているという面もある。
ともかく、websocket を実現するにあたっての uWSGI いじりは、WSGI サーバーまでは実現できたにもかかわらず、最後の websocket 化の段階で頓挫してしまったが、まだ、最終手段として、pip と gcc を使っての、uWSGI のインストールする方法が残されている。通常の状態では、FLASH 容量が足らないので、外部 USB メモリーの領域もシステム領域として使う Extroot という手段を使う方法が前提条件となる。これに成功すれば、容量的に gcc パッケージのインストールも可能になるので、pip からの uWSGI のインストールすら試みることができるようになるだろう。
コメント
コメントを投稿