nginx + Python でウェブサービスを（ラズパイ編）

# はじめに

nginxにPythonが動作する環境を作ります。つまりPythonでウェブサービスを作る基盤を作ります。Django・Flask・Bottleといったフレームワークは使いません。

本稿はRaspbianを対象としています。CentOSをご利用の場合は [CentOS編](https://qiita.com/waffle/items/835625c2413ff6a387c6) もご覧ください。

執筆時のRaspbianのバージョンは9.4でした。

```console
# cat /etc/debian_version
9.4
```

# 必要なものをインストール

nginxとuwsgi-plugin-python3をインストールします。後者はnginxとPython3を繋ぐ役割を担います。

```console
# apt-get update
# apt-get -y install nginx
# apt-get -y install uwsgi-plugin-python3
```

各バージョンは次のようになりました。

```console
# 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に対応しています。

```sh:/root/bin/upnpc.sh
#!/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接続も転送できるようになります。

```console
# apt-get -y install miniupnpc
# chmod 700 /root/bin/upnpc.sh
# /root/bin/upnpc.sh
```

上記の例では手動で実行していますが、例えばルーターが再起動しても再びこの設定が有効になるように`crontab`に登録して定期的に実行されるようにします。

```:crontab
*/6 * * * * /root/bin/upnpc.sh >/dev/null 2>&1
```

# 事前設定 (2/2)

本題とは直接関係ありませんが、外から自宅のラズパイへアクセスするための事前設定（2/2）です。

例えば`https://example.ddns.net/`として自宅ラズパイへアクセスできるように、自宅のルーターに`example.ddns.net`という名前を与えます。これには外部の無料サービス [no-ip](https://www.noip.com/) を利用します。同サイトでアカウント登録して`*.ddns.net`といった自分専用のホスト名をひとつ取得してください。

ホスト名を取得したらddclientをインストールします。インストール過程で対話形式の質問がありますが、全てEnterでスキップしてかまいません。

```console
# apt-get -y install ddclient
```

対話形式の質問をスキップした代わりに、設定ファイル`ddclient.conf`の内容を自力で編集します。ログインIDとパスワードはもちろん、末行の`example.ddns.net`の部分も自身で取得したホスト名に置き換えてください。

```ini:/etc/ddclient.conf
ssl=yes

login='(ログインID)'
password='(パスワード)'
use=web

server=dynupdate.no-ip.com
protocol=dyndns2
example.ddns.net
```

ddclientを実行して、ホスト名`example.ddns.net`と自宅のIPアドレスを紐付けます。

```console
# chmod 600 /etc/ddclient.conf
# ddclient -daemon=0 -verbose
```

上記の例では手動で実行していますが、今後自宅のIPアドレスが変更されても新しいIPアドレスが自動的に紐付くように、ddclientをサービス化します。

```console
# systemctl start ddclient
# systemctl status ddclient
# systemctl enable ddclient
```

[no-ip](https://www.noip.com/) は、登録できるホスト名が最大３個で、１ヶ月間ログインがないと次のログインまで休止されるといった制約はあるものの、無料でも利用できるので便利です。

# uwsgi 設定

必要なディレクトリを作成します。

```console
# mkdir -p /etc/uwsgi/apps-available
# mkdir -p /etc/uwsgi/apps-enabled
```

uwsgiの設定を`emperor.ini`に記述します。

```ini:/etc/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`と記述することで自身のファイル名を参照できる」という便利な機能があるため、個々のファイル内に具体的なホスト名を記述する必要がなく、実質的にはテンプレートを１個だけ作り、これにホスト名のシンボリックリンクを張ることで設定ファイルを容易に量産できます。まずはそのテンプレートを`apps-available/vhosts.ini`として作成します。

```ini:/etc/uwsgi/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`としています。これをバーチャルホストの数だけこれを作成します。

```console
# cd /etc/uwsgi/apps-enabled/
# ln -s ../apps-available/vhosts.ini example.ddns.net.ini
```

### uwsgiのサービス化

uwsgiが常に動作し続けるようにsystemdでサービス化します。

```ini:/etc/systemd/system/uwsgi.service
[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`を指定します。

```console
# systemctl start uwsgi
# systemctl status uwsgi
# systemctl enable uwsgi
```

### uwsgiの循環ログ

uwsgiのログが際限なく巨大化してしまうことをlogrotateによって予防しておきます。

```/etc/logrotate.d/uwsgi
"/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`の設定例です。

```nginx:/etc/nginx/sites-available/example.ddns.net.conf
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/`に作ってこの設定を有効にします。

```console
# cd /etc/nginx/sites-enabled
# ln -s ../sites-available/example.ddns.net.conf
```

### オレオレ証明書

前述の`example.ddns.net.conf`に記述したSSLの証明書を作成します。本題から逸れる内容なので本稿ではオレオレ証明書で済ませますが、実際の運用に際してはLet's Encryptなどで証明書を得るのが良いでしょう。

```console
# 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スクリプトを配置します。

```console
# 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からも参照できるようにパーミッションを変更します。

```python:/home/pi/example.ddns.net/wsgi/wsgi.py
# -*- 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`を指定します。

```console
# systemctl start nginx
# systemctl status nginx
# systemctl enable nginx
```

# 動作確認

確認前に念のため nginx と uswgi を再起動します。

```console
# 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編](https://qiita.com/waffle/items/835625c2413ff6a387c6) もご覧ください。