FreeBSDでgunicornのserviceスクリプトを作る


プライベート用途でFreeBSDで`Apache+mod_wsgi+flask`という構成でサーバを建てていたのだけど、gunicornを使ってみようか、と思い始めてきました。
gunicornであれば、アプリケーションの再起動もApacheを再起動しなくても済みますしね。まぁプライベートなんで、再起動しても問題ないんですが。

で、gunicornを使うには、gunicornをサービスプログラムとして起動させなきゃいけません。
ubuntuでも使っていたのだけど、その場合にはsystemdの情報がいくらでもあったので困らないのだけど、FreeBSDの場合、古い情報だったり、英語だったりして、意外に少ない。
あいや、英語だからダメ、じゃコンピュータの仕事をしている人間としてアレなんで、何とか動かすところまで頑張ってみました。
頑張ったので、ホントに動かしてみただけ、正しいやり方かどうかまでは知らないので、あしからず。

とりあえず、本家のまず見るべき資料。
* [Practical rc.d scripting in BSD](https://docs.freebsd.org/en/articles/rc-scripting/)

なお、以下は以下の環境で行いました。

```
FreeBSD test01.localdomain 12.2-RELEASE FreeBSD 12.2-RELEASE r366954 GENERIC  amd64
```

## テストアプリケーション

テストするwsgiアプリケーションを以下のように用意しました。

```python:/public/testweb/testweb.py
# coding: utf-8
from wsgiref.simple_server import make_server, demo_app as application
if __name__ == '__main__':
    with make_server('', 8080, application) as server:
        server.serve_forever()
```

pythonには、標準で`wsgiref.simple_server`という、テスト用のwsgiアプリケーションのモジュールが用意されています。さすが、Python様だねっ！

これを動かすために、python3をインストールします。

```
pkg install python3
```

まずは、テストアプリケーションのみを動かしてみます。

```
python3 /public/testweb/testweb.py
```

これで、ブラウザより8080ポートにアクセスすると、「`Hello world!`」のメッセージと、環境変数が表示されます。
特に、「`SERVER_SOFTWARE = 'WSGIServer/0.2'`」のところに注目。

## gunicornのインストール

gunicornをインストールします。
pkgでインストールしてもいいし、pipでインストールしてもいいし。この件に関しては、今回は置いときます。
今回は、pkgでインストールします。

```
pkg install py39-gunicorn
```

## serviceスクリプトの作成

さて、ここからが本題。

「`/usr/local/etc/rc.d`」ディレクトリ以下に、`testweb`という名前でスクリプトを作成します。

```sh:/usr/local/etc/rc.d/testweb
#!/bin/sh

# PROVIDE: testweb
# REQUIRE: NETWORKING

. /etc/rc.subr

name=testweb
rcvar=${name}_enable
testweb_user=katsuko

pidfile="/var/run/${name}/${name}.pid"

command="/usr/sbin/daemon"
command_args="-P ${pidfile} -r -f /usr/local/bin/gunicorn --bind '0.0.0.0:8080' --pythonpath /public/testweb testweb"

load_rc_config ${name}
run_rc_command "$1"
```

コメントにある「`PROVIDE`」は提供するサービスの名前、「`REQUIRE`」は依存する他のサービス名(rcスクリプト)だそうです。([ここ](https://taizooo.hatenadiary.org/entry/20051021/1129885051)より参照)
gunicornはwebサービスなので、`NETWORING`を指定しました。

デフォルトではrootユーザでサービスプログラムが実行されますが、できればrootで動かしたくはないので、その場合には「*サービス名*_user」に実行ユーザを指定します。
なお、「`pidfile`」に「`/var/run`」ディレクトリ以下にディレクトリを作って、そこにファイルを作るようにしているのは、「`/var/run`」ディレクトリはroot権限でしかファイルを作成できないので、ディレクトリを作って実行ユーザに権限を与えます。

さて、肝となるのは「`command`」と「`command_args`」。
これは、サービスの開始コマンドで、以下のように実行されます。

```
${command} ${command_args}
```

このコマンドにより実行されたプロセスは、

1. 実行中のサービスプログラムのプロセスIDを、「`pidfile`」で指定したファイルに書き込む。
2. コマンド自体は、即座に終了しなければならない。(つまり、サービスプログラム自体はforkして動かなければならない)

という必要条件があります。

`status`/`stop`サブコマンドを実行した際、サービスプログラムのプロセスを探すわけですが、それは **`pidfile`に書かれたプロセスIDで、かつプロセスコマンドが`command`のものかどうか(もしくは、`procname`が指定されていればそちらを参照する)** で判断されます。
(サービスプログラムが`command`での実行と違う場合には、`procname`を指定する必要があります)
この辺りが正しくないと、`start`は出来ても、`status`/`stop`は出来ず、散々悩むことになります。てか、悩みました。

さて、gunicornはプロセスIDをファイルに出力する設定はありますが、サービスをデーモンプロセスとして起動するやり方は見つけられませんでした。(~~もしあれば、失礼~~ [ちゃんとありますがな。](https://docs.gunicorn.org/en/stable/settings.html#daemon))
なので、`daemon`コマンドを使用します。

* [man daemon](https://www.freebsd.org/cgi/man.cgi?daemon(8))

`daemon`コマンドは、上記の条件を満たしてくれる、とってもありがたいコマンドです。

最後に、このスクリプトに実行権を与えます。

```
chmod a+x /usr/local/etc/rc.d/testweb
```

これで、後はいつものサービス通り、「`/etc/rc.conf`」に「`testweb_enable="YES"`」を書き込み、「`service testweb start`」でサービスを起動し、またブラウザでアクセスしてみます。
先程のように「`Hello world!`」と環境変数地が表示されますが、中には「`SERVER_SOFTWARE = 'gunicorn/20.1.0'`」という表示に変わっているはずです。
これで、gunicornが動いていることを確認できます。

## 終わりに

今回はシンプルな(と思われる)スクリプトの書き方をしましたが、その気になれば`start`コマンドなどを上書きしてしまうことも出来ます。
まぁそこまでやりたいのであれば、「[man rc.subr](https://www.freebsd.org/cgi/man.cgi?rc.subr(8))」を読んだり、「`/etc/rc.subr`」自体を読みましょう。
自分は疲れたので、ここまでにします。