はじめに
nginxにPythonが動作する環境を作ります。つまりPythonでウェブサービスを作る基盤を作ります。Django・Flask・Bottleといったフレームワークは使いません。
本稿はRaspbianを対象としています。CentOSをご利用の場合は CentOS編 もご覧ください。
執筆時のRaspbianのバージョンは9.4でした。
# cat /etc/debian_version
9.4
必要なものをインストール
nginxとuwsgi-plugin-python3をインストールします。後者はnginxとPython3を繋ぐ役割を担います。
# apt-get update
# apt-get -y install nginx
# apt-get -y install uwsgi-plugin-python3
各バージョンは次のようになりました。
# nginx -v
nginx version: nginx/1.10.3
# python3 --version
Python 3.5.3
# uwsgi --version
2.0.14-debian
事前設定 (1/2)
本題とは直接関係ありませんが、外から自宅のラズパイへアクセスするための事前設定(1/2)です。
ラズパイが自宅のWiFiルーターに接続されていることを前提に、家の外からルーター越しにラズパイへアクセスできるようにUPnPの設定を行います。Apple製でない限りおよそ全ての家庭用ルーターはUPnPに対応しています。
#!/bin/sh
IP=`hostname -I | /usr/bin/cut -d' ' -f1`
upnpc -a ${IP} 443 443 tcp
upnpc -l
上記のupnpc.sh
は、外から家にhttps(port:443)によるアクセスがあった場合に、そのリクエストをラズパイへ転送してもらうように、自宅ルーターへ依頼するためのスクリプトです。例えばupnpc -a ${IP} 80 80 tcp
の行を追加すればhttpのリクエストも、また同様にupnpc -a ${IP} 22 22 tcp
とすればSSH接続も転送できるようになります。
# apt-get -y install miniupnpc
# chmod 700 /root/bin/upnpc.sh
# /root/bin/upnpc.sh
上記の例では手動で実行していますが、例えばルーターが再起動しても再びこの設定が有効になるようにcrontab
に登録して定期的に実行されるようにします。
*/6 * * * * /root/bin/upnpc.sh >/dev/null 2>&1
事前設定 (2/2)
本題とは直接関係ありませんが、外から自宅のラズパイへアクセスするための事前設定(2/2)です。
例えばhttps://example.ddns.net/
として自宅ラズパイへアクセスできるように、自宅のルーターにexample.ddns.net
という名前を与えます。これには外部の無料サービス no-ip を利用します。同サイトでアカウント登録して*.ddns.net
といった自分専用のホスト名をひとつ取得してください。
ホスト名を取得したらddclientをインストールします。インストール過程で対話形式の質問がありますが、全てEnterでスキップしてかまいません。
# apt-get -y install ddclient
対話形式の質問をスキップした代わりに、設定ファイルddclient.conf
の内容を自力で編集します。ログインIDとパスワードはもちろん、末行のexample.ddns.net
の部分も自身で取得したホスト名に置き換えてください。
ssl=yes
login='(ログインID)'
password='(パスワード)'
use=web
server=dynupdate.no-ip.com
protocol=dyndns2
example.ddns.net
ddclientを実行して、ホスト名example.ddns.net
と自宅のIPアドレスを紐付けます。
# chmod 600 /etc/ddclient.conf
# ddclient -daemon=0 -verbose
上記の例では手動で実行していますが、今後自宅のIPアドレスが変更されても新しいIPアドレスが自動的に紐付くように、ddclientをサービス化します。
# systemctl start ddclient
# systemctl status ddclient
# systemctl enable ddclient
no-ip は、登録できるホスト名が最大3個で、1ヶ月間ログインがないと次のログインまで休止されるといった制約はあるものの、無料でも利用できるので便利です。
uwsgi 設定
必要なディレクトリを作成します。
# mkdir -p /etc/uwsgi/apps-available
# mkdir -p /etc/uwsgi/apps-enabled
uwsgiの設定をemperor.ini
に記述します。
[uwsgi]
emperor = /etc/uwsgi/apps-enabled
uid = www-data
gid = www-data
logto = /var/log/uwsgi/uwsgi.log
touch-logreopen = /var/log/uwsgi/touch-logreopen
master = true
vacuum = true
ignore-sigpipe = true
ignore-write-errors = true
disable-write-exception = true
上記のignore-sigpipe
ignore-write-errors
disable-write-exception
は、/var/log/uwsgi/uwsgi.log
に頻繁に現れるOSError
を消すのに有効でした。
続いてバーチャルホストごとの設定を apps-enabled/ホスト名.ini
に記述します。原理的にはこの設定ファイルをバーチャルホストの数だけ用意する必要があります。しかしuwsgiには「設定ファイル内で%n
と記述することで自身のファイル名を参照できる」という便利な機能があるため、個々のファイル内に具体的なホスト名を記述する必要がなく、実質的にはテンプレートを1個だけ作り、これにホスト名のシンボリックリンクを張ることで設定ファイルを容易に量産できます。まずはそのテンプレートをapps-available/vhosts.ini
として作成します。
[uwsgi]
plugins = python3
chdir = /home/pi/%n/wsgi
module = wsgi:application
socket = /var/run/uwsgi/%n.sock
chmod-socket = 644
reload-mercy = 1
processes = %k
die-on-term = true
py-autoreload = 1
enable-threads = true
threads = 8
上記のpy-autoreload
は、Pythonスクリプトの内容を変更する度にuwsgiを再起動する必要を無くすための便利な設定です。またenable-threads
およびthreads
は、nginxの受信した複数のリクエストを同時に処理させるために必要な設定です。この設定がないとuwsgiは先のリクエストを処理し終わるまで次のリクエストの処理を始めてくれません。
上記のテンプレートにホスト名のシンボリックリンクをsite-enabled/
に作成します。こちらの例ではexample.ddns.net
としています。これをバーチャルホストの数だけこれを作成します。
# cd /etc/uwsgi/apps-enabled/
# ln -s ../apps-available/vhosts.ini example.ddns.net.ini
uwsgiのサービス化
uwsgiが常に動作し続けるようにsystemdでサービス化します。
[Unit]
Description=uWSGI Emperor
After=syslog.target
[Service]
ExecStartPre=-/bin/mkdir -p /var/log/uwsgi
ExecStartPre=-/bin/chown -R www-data:www-data /var/log/uwsgi
ExecStartPre=-/bin/mkdir -p /var/run/uwsgi
ExecStartPre=-/bin/chown -R www-data:www-data /var/run/uwsgi
ExecStart=/usr/bin/uwsgi --ini /etc/uwsgi/emperor.ini
RuntimeDirectory=uwsgi
Restart=always
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all
[Install]
WantedBy=multi-user.target
uwsgiを起動させます。start
で起動したことをstatus
で確認し、今後は自動的に起動されるようにenable
を指定します。
# systemctl start uwsgi
# systemctl status uwsgi
# systemctl enable uwsgi
uwsgiの循環ログ
uwsgiのログが際限なく巨大化してしまうことをlogrotateによって予防しておきます。
"/var/log/uwsgi/*.log" "/var/log/uwsgi/app/*.log" {
copytruncate
daily
rotate 5
compress
delaycompress
missingok
notifempty
postrotate
touch /var/log/uwsgi/touch-logreopen
endscript
}
nginx 設定
バーチャルドメインexample.ddns.net
の設定例です。
server {
listen 443 ssl;
server_name example.ddns.net;
root /home/pi/example.ddns.net;
location / {
try_files /resource/$uri @uwsgi;
}
location @uwsgi {
include uwsgi_params;
uwsgi_pass unix:///var/run/uwsgi/example.ddns.net.sock;
uwsgi_connect_timeout 600s;
uwsgi_read_timeout 600s;
}
ssl_certificate /etc/nginx/ssl/self_signed.crt;
ssl_certificate_key /etc/nginx/ssl/self_signed.key;
}
try_files
の行には、リクエストされたファイルが/home/pi/example.ddns.net/resource
内に存在すればこれを直接出力し、実在しなければ代わりにuwsgiのPythonへ処理を繋ぐようにする指示です。したがって例えば画像ファイルなどはresource
に配置しておきます。
uwsgi_connect_timeout
とuwsgi_read_timeout
は/var/log/nginx/error.log
に頻出していたupstream timed out
を消すのに効果がありました。
シンボリックリンクをsites-elabled/
に作ってこの設定を有効にします。
# cd /etc/nginx/sites-enabled
# ln -s ../sites-available/example.ddns.net.conf
オレオレ証明書
前述のexample.ddns.net.conf
に記述したSSLの証明書を作成します。本題から逸れる内容なので本稿ではオレオレ証明書で済ませますが、実際の運用に際してはLet's Encryptなどで証明書を得るのが良いでしょう。
# mkdir /etc/nginx/ssl
# chmod 700 /etc/nginx/ssl
# cd /etc/nginx/ssl
# openssl genrsa 2048 > self_signed.key
# openssl req -new -key self_signed.key > self_signed.csr
---- 必要情報を入力 ----
# openssl x509 -days 3650 -req -signkey self_signed.key < self_signed.csr > self_signed.crt
# chmod 600 /etc/nginx/ssl/*
Python配置
example.ddns.net
のドキュメントルートとなるディレクトリを作成して、動作試験用のPythonスクリプトを配置します。
# su pi
$ chmod 711 /home/pi
$ mkdir -p /home/pi/example.ddns.net/resource
$ mkdir -p /home/pi/example.ddns.net/wsgi
上の例ではユーザーpiのホームにバーチャルホスト名のディレクトリを作成していますが、/etc/nginx/sites-available/example.ddns.net.conff
のroot
と一致していれば、全く別のディレクトリでもかまいません。nginxとuwsgiからも参照できるようにパーミッションを変更します。
# -*- coding: utf-8 -*-
def application(environ, start_response):
try:
body = {k + ': ' + str(v) for k, v in environ.items()}
start_response('200 OK', [('Content-type', 'text/plain')])
return ['\n'.join(body).encode()]
except:
import traceback
start_response('500 Internal Server Error', [('Content-type', 'text/plain')])
return [traceback.format_exc().encode()]
このスクリプトの保存ディレクトリ/home/pi/example.ddns.net/wsgi/
と、ファイル名のwsgi
、および関数名のapplication
は、先のuwsgiの設定のvhosts.ini
の中で指定した文字列と一致させています。言い換えると、これらに一貫性のある変更を加えれば別の文字列にも変更できます。
nginx 起動
start
で起動したことをstatus
で確認し、今後は自動的に起動されるようにenable
を指定します。
# systemctl start nginx
# systemctl status nginx
# systemctl enable nginx
動作確認
確認前に念のため nginx と uswgi を再起動します。
# systemctl restart nginx
# systemctl restart uwsgi
家の中ならブラウザでhttps://raspberrypi.local/
にアクセスします。あるいは家の外ならhttps://example.ddns.net/
へアクセスします。家の中でもWiFiをOffにすればスマホを使って外部インターネットからの確認を行えます。
いずれもオレオレ証明書を使用しているためブラウザが警告を出しますが、ページの表示を強行します。wsgi.py が出力する環境変数の一覧がブラウザに表示されれば成功です。
結果が好ましくない場合は/var/log/nginx/error.log
および/var/log/uwsgi/uwsgi.log
に記されたエラーの内容から解決の糸口が得られるかもしれません。
終わりに
本稿は各項の詳細を深く理解して合理的に導いた結果ではなく、ネットに散在する情報の断片を元に試行錯誤を重ねて経験的に導いた現時点での解決策を紹介しています。誤りや改善点ご指摘いただくと助かります。
本稿と同様の内容の CentOS編 もご覧ください。