LoginSignup
12
14

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-07-05

はじめに

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

/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接続も転送できるようになります。

# 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 を利用します。同サイトでアカウント登録して*.ddns.netといった自分専用のホスト名をひとつ取得してください。

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

# apt-get -y install ddclient

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

/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アドレスを紐付けます。

# 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に記述します。

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

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

uwsgiのサービス化

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

/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を指定します。

# 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の設定例です。

/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_timeoutuwsgi_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.conffrootと一致していれば、全く別のディレクトリでもかまいません。nginxとuwsgiからも参照できるようにパーミッションを変更します。

/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を指定します。

# 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編 もご覧ください。

12
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
14