このブログはNode.jsでGhost BlogをCentOSやWindows上で動かしているが、開発環境はともかく本番用やステージング用はサーバ起動時に一緒にデーモン起動していて欲しい。ところがコレには色々と考えるべきところがあるのだ。
デーモンプロセス化を助けるツール
Node.jsのデーモン化実装にはいくつかあるが、以下が代表的なものだろう。
- supervisor
- forever
- pm2
いずれもnpm install -g
で導入できる。おまけにLinuxでもWindowsでもそれなりに動く。それぞれに特徴がありセールスポイントがあり使い勝手も異なるが、一番リッチなのがpm2で、一番シンプルなのがforeverだろうか。
だがこれらはどういうワケかsuid/sugidをサポートしてないのである。いやNode.js自体にはPOSIX準拠のprocess.setuid
とprocess.setgid
が一応実装されているのだがそれをこれらで使っているのを見たことがない1。なので上記のこれらを普通にroot権限で起動するとそのままroot権限プロセスで動き続けることになり、実行権限を落として運用したい場合は別の工夫が必要になる。
手っ取り早くデーモンにしてしまう
WindowsにはWindowsの事情があるから置いておくとして、まずCentOS6ではどうするか。6のinitプロセスにはupstartとsysvinitの2種類2が使われており、特段の事情がなければ使い慣れた後者に従うのが普通だろう。とりあえずroot権限でGhost(に限らずNode.jsで実装した各種サービス)を手っ取り早く動かしてしまうには以下のようにする。なおここではforeverを使用する。また一連の作業はすべてrootユーザで行う。
まずyumでredhat-lsb-coreを導入する。そしてforeverとinitd-foreverをnpmで導入する。
$ sudo -s
# yum install redhat-lsb-core
# npm install forever -g
# npm install initd-forever -g
# initd-forever --help
Usage: initd-forever [options]
Options:
-h, --help output usage information
-V, --version output the version number
-a, --app [path] Path to node.js main file
-c, --command [value] Command to execute on main file
-e, --env [value] Export NODE_ENV with value
-l, --logfile [path] Logs the daemon output to LOGFILE
-n, --name [value] Application name
-p, --pidfile [path] The pid file
-m, --monit [value] Generate the monit script file with the listen port number
-f, --forever [value] The location of forever
次いでGhostディレクトリに移動してinitd-foreverでinitdスクリプトを生成する。最低限出力ファイル名=サービス名を決定する-n指定は必要だ。
# cd ~user/ghost
# initd-forever -n ghost
Script daemon file saved to ghost
出来上がったファイルに実行権限を付与して/etc/init.d
へ移動し、chkconfigで使用可能にしてserviceで起動する。
# chmod +x ghost
# mv ghost /etc/init.d
# chkconfig ghost --add
# chkconfig ghost on
# chkconfig ghost --list
ghost 0:off 1:off 2:on 3:on 4:on 5:on 6:off
# service ghost start
initd-foreverが生成したファイルは次のようなものだ。通常の用途ではまず修正するところはないだろう。
#!/bin/bash
### BEGIN INIT INFO
# Provides: /home/user/ghost/core/index
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: forever running /home/user/ghost/core/index
# Description: /home/user/ghost/core/index
### END INIT INFO
#
# initd a node app
# Based on a script posted by https://gist.github.com/jinze at https://gist.github.com/3748766
#
# Source function library.
. /lib/lsb/init-functions
pidFile="/var/run/ghost.pid"
logFile="/var/run/ghost.log"
command="node"
nodeApp="/home/user/ghost/core/index"
foreverApp="forever"
start() {
echo "Starting $nodeApp"
# Notice that we change the PATH because on reboot
# the PATH does not include the path to node.
# Launching forever with a full path
# does not work unless we set the PATH.
PATH=/usr/local/bin:$PATH
export NODE_ENV=production
#PORT=80
$foreverApp start --pidFile $pidFile -l $logFile -a -d -c "$command" $nodeApp
RETVAL=$?
}
restart() {
echo -n "Restarting $nodeApp"
$foreverApp restart $nodeApp
RETVAL=$?
}
stop() {
echo -n "Shutting down $nodeApp"
$foreverApp stop $nodeApp
RETVAL=$?
}
status() {
echo -n "Status $nodeApp"
$foreverApp list
RETVAL=$?
}
case "$1" in
start)
start
;;
stop)
stop
;;
status)
status
;;
restart)
restart
;;
*)
echo "Usage: $0 {start|stop|status|restart}"
exit 1
;;
esac
もう少し本格的に制御してみる
実行するNode.jsサービスがひとつやふたつでしかもrootオンリーなら上記の作業を必要なだけ繰り返せば良い。だがGhost Blogの場合これはあまりよろしくない。というのも;
- Ghost は記事に対して Draft と Publish の2モードを持つが Staging モードは持っていない。つまり投稿記事を一般公開前にレンダリングチェックすることができない。このために結局本番用とステージング用とでは個別のサービスインスタンスを立ち上げて使い分けたいという需要がある。
- Ghost はマルチユーザ(グループ)対応だが、ユーザ別にテーマを変更できるわけではなく全体でひとつのテーマに固定される。故にテーマやプラグインを開発するユースケースでもユーザ別インスタンスの需要がある。またこの作業は頻繁にインスタンスを再起動する必要が有るため、start/stopを行うのにいちいちrootへsuするのは現実的ではない。
- Ghost は自身のローカルディレクトリ内にアップロードされた画像ファイルと、記事DBファイルを格納3している。当然それらのファイル権限がそのままでは実効rootになってしまうから、非rootの開発ユーザがファイルメンテナンスをするうえで都合が悪い。
これらの諸々を助けるためにforeverdというinitdファイルを作成した。これは /etc/forver.d ディレクトリに配置した設定ファイルの数だけNode.jsのインスタンスを起動する。
NODE_DIR=/home/user/ghost
NODE_ENV=staging
NODE_APP=index.js
NODE_USER=user
OPTS="--silent"
NODE_DIR=/home/user/ghost
NODE_ENV=production
NODE_APP=index.js
NODE_USER=user
OPTS="--silent"
#!/bin/sh
#
# chkconfig: 2345 88 14
# description: Description of the Service
#
# Below is the source function library, leave it be
. /etc/init.d/functions
# result of whereis node_modules
export NODE_PATH=/usr/lib/node_modules
PATH=/sbin:/bin:/usr/sbin:/usr/bin
fordeamon(){
for conf in /etc/forever.d/*.conf; do
name=$(basename $conf .conf)
if [ -n "$2" ]; then
if [ "$name" != "$2" ]; then
continue
fi
fi
source $conf
: ${NODE_USER:=root}
: ${NODE_APP:=index.js}
: ${NODE_DIR:=~}
if [ $(whoami) != $NODE_USER ]; then
su -c "$0 $1 $name" $NODE_USER
else
echo $"Process id is $name"
case "$1" in
start)
NODE_ENV=$NODE_ENV \
forever $OPTS \
--sourceDir="$NODE_DIR" \
--workingDir="$NODE_DIR" \
--minUptime=1000 \
--spinSleepTime=1000 \
--uid=$name \
-a \
start $NODE_APP
;;
stop)
NODE_ENV=$NODE_ENV \
forever stop $name
;;
restart)
NODE_ENV=$NODE_ENV \
forever restart $name
;;
list)
NODE_ENV=$NODE_ENV \
forever list | grep /$name.log
;;
esac
fi
done
}
case "$1" in
start)
echo "Start service forever"
fordeamon start $2
;;
stop)
echo "Stop service forever"
fordeamon stop $2
;;
restart)
echo "Restart service forever"
fordeamon restart $2
;;
status)
fordeamon list $2
;;
list)
fordeamon list $2
;;
*)
echo "Usage: $0 {start|stop|restart|status|list} [name]"
exit 1
;;
esac
これを実行権限を付与してchkconfigでサービス登録すると、サーバ起動時に /etc/forver.d/*.conf に従ってforeverインスタンスを起動するが、その後は一般ユーザ側で普通に(serviceコマンドを通さずに)foreverコマンドでlist/stop/restart4することができるようになる。またroot側からもservice foreverd COMMAND NAME
でインスタンス個別にユーザ権限でプロセス操作ができる。
# service foreverd start
# service foreverd restart NAME
# service foreverd stop NAME
$ forever list
$ forever restart NAME
$ forever stop NAME
$ /etc/init.d/foreverd start NAME
$ forever restartall
Switch Userは27行目のsuコマンドが行っているが、これは自分自身(/etc/init.d/foreverd)をsuidして再帰的に呼び出すようにした。正攻法ならsudoコマンドでforeverを呼びたくなるところだが、エスケープを含むコマンドラインの組み立てが煩雑になるのを避けたいのと、CnetOS6の/etc/sudoersのデフォルト設定では非TTYでのsudo実行を禁止しており、そのままでは rcプロセス(当然非TTY)で動作しない5という事情による。
なお当然だが well-known port を掴むようなインスタンスはrootユーザで起動しなければ意味が無い。また個々のインスタンスが掴むポート番号を重複しないように調整するのもroot管理者の仕事である。
比較的新しい機能なので古いNode.jsに配慮するとまだ機能統合できないということか。ゆえにデーモンプロセスツールではなく個々のアプリケーションの中でやってくれという主張らしい。 ↩
CentOS7になるとsysvinitが非推奨となってsystemctlが標準となる。6/7両方で修正なく動くようにしたいならupstartで実装するのも手だ。 ↩
DBについてはPostgresql等外部へ逃すことはできるがアップロード画像ファイルはDBに入らないためどうにもならない。ゆえにsupervisorやpm2のファイル更新監視モードは無限再起動に陥るため使うことができず、かえってそれらを使うメリットがない。本項でforeverを主に扱っているのはこういう事情による。 ↩
ユーザ側でのstartは、root実行時と同じ起動パラメータを指定するようにしないと当然同じ動作にならない。故にサービス起動時はサンプルの4行目のように /etc/init.d/foreverdを直接実行する。 ↩
visudoで Defaults requiretty を Defaults:system_user !requiretty に修正すれば非TTYプロセス中でもsudoできるようになる。だが今回の場合はrootが非rootプロセスを起動したいのであり、非rootにはrootプロセスを起動させたくないのだから suのほうが理にかなう。 ↩