nginx + Python でウェブサービスを（CentOS編）

# はじめに

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

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

執筆時の「さくらのVPS」のCentOSのバージョンは7.5でした。

```console
# cat /etc/redhat-release
CentOS Linux release 7.5.1804 (Core)
```

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

nginxとPython、そして両者を繋ぐ役割を担うuwsgiをインストールします。

```console
# yum update

# yum -y install nginx

# yum search python3
# yum -y install python34-devel
# yum -y install python34-setuptools
# easy_install-3.4 pip

# pip3 install uwsgi
```

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

```console
# nginx -v
nginx version: nginx/1.12.2
# python3 --version
Python 3.4.8
# uwsgi --version
2.0.17
```

# uwsgi 設定

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

```console
# mkdir -p /etc/uwsgi/vassals
```

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

```/etc/uwsgi/emperor.ini
[uwsgi]
emperor = /etc/uwsgi/vassals
uid = nginx
gid = nginx
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`を消すのに有効でした。

続いてバーチャルホストごとの設定を `vassals/ホスト名.ini` に記述します。原理的にはこの設定ファイルをバーチャルホストの数だけ用意する必要があります。しかしuwsgiには「設定ファイル内で`%n`と記述することで自身のファイル名を参照できる」という便利な機能があるため、個々のファイル内に具体的なホスト名を記述する必要がなく、実質的にはテンプレートを１個だけ作り、これにホスト名のシンボリックリンクを張ることで設定ファイルを容易に量産できます。まずはそのテンプレートを`vhosts.ini`として作成します。

```/etc/uwsgi/vhosts.ini
[uwsgi]
chdir = /home/app/%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は先のリクエストを処理し終わるまで次のリクエストの処理を始めてくれません。

上記のテンプレートにホスト名のシンボリックリンクを作成します。こちらの例では`example.com`としています。これをバーチャルホストの数だけこれを作成します。

```console
# cd /etc/uwsgi/vassals
# ln -s ../vhosts.ini example.com.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 nginx:nginx /var/log/uwsgi
ExecStartPre=-/bin/mkdir -p /var/run/uwsgi
ExecStartPre=-/bin/chown -R nginx:nginx /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/uwsgi.log {
	compress
	daily
	missingok
	notifempty
	rotate 7
	sharedscripts
	size 1M
	postrotate
		touch /var/log/uwsgi/touch-logreopen
	endscript
}
```

# nginx 設定

バーチャルドメイン`example.com`の設定例です。

```nginx:/etc/nginx/conf.d/example.com.conf
server {
	listen 443 ssl;
	server_name example.com;
	root /home/app/example.com;
	location / {
		try_files /resource$uri @uwsgi;
	}
	location @uwsgi {
		include uwsgi_params;
		uwsgi_pass unix:///var/run/uwsgi/example.com.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/app/example.com/resource`内に存在すればこれを直接出力し、実在しなければ代わりにuwsgiのPythonへ処理を繋ぐようにする指示です。したがって例えば画像ファイルなどは`resource`に配置しておきます。

`uwsgi_connect_timeout`と`uwsgi_read_timeout`は`/var/log/nginx/error.log`に頻出していた`upstream timed out`を消すのに効果がありました。

### オレオレ証明書

前述の`example.com.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.com`のドキュメントルートとなるディレクトリを作成して、動作試験用のPythonスクリプトを配置します。

```console
# su app
$ chmod 711 /home/app
$ mkdir -p /home/app/example.com/resource
$ mkdir -p /home/app/example.com/wsgi
```

上の例ではユーザーappのホームにバーチャルホスト名のディレクトリを作成していますが、`/etc/nginx/conf.d/example.com.conf`の`root`と一致していれば、全く別のディレクトリでもかまいません。nginxとuwsgiからも参照できるようにパーミッションを変更します。

```python:/home/app/example.com/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/app/example.com/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://example.com/`へアクセスします。オレオレ証明書を使用しているためブラウザが警告を出しますが、ページの表示を強行します。wsgi.py が出力する環境変数の一覧がブラウザに表示されれば成功です。

結果が好ましくない場合は`/var/log/nginx/error.log`および`/var/log/uwsgi/uwsgi.log`に記されたエラーの内容から解決の糸口が得られるかもしれません。

# 終わりに

本稿は各項の詳細を深く理解して合理的に導いた結果ではなく、ネットに散在する情報の断片を元に試行錯誤を重ねて経験的に導いた現時点での解決策を紹介しています。誤りや改善点ご指摘いただくと助かります。

本稿と同様の内容の [ラズパイ編](https://qiita.com/waffle/items/dfd40f69d75b5be7afe7) もご覧ください。