Node.jsのサービス実行

このブログはNode.jsでGhost BlogをCentOSやWindows上で動かしているが、開発環境はともかく本番用やステージング用はサーバ起動時に一緒にデーモン起動していて欲しい。ところがコレには色々と考えるべきところがあるのだ。


    デーモンプロセス化を助けるツール

    Node.jsのデーモン化実装にはいくつかあるが、以下が代表的なものだろう。

    • supervisor
    • forever
    • pm2

    いずれもnpm install -gで導入できる。おまけにLinuxでもWindowsでもそれなりに動く。それぞれに特徴がありセールスポイントがあり使い勝手も異なるが、一番リッチなのがpm2で、一番シンプルなのがforeverだろうか。

    だがこれらはどういうワケかsuid/sugidをサポートしてないのである。いやNode.js自体にはPOSIX準拠のprocess.setuidprocess.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管理者の仕事である。


    1. 比較的新しい機能なので古いNode.jsに配慮するとまだ機能統合できないということか。ゆえにデーモンプロセスツールではなく個々のアプリケーションの中でやってくれという主張らしい。

    2. CentOS7になるとsysvinitが非推奨となってsystemctlが標準となる。6/7両方で修正なく動くようにしたいならupstartで実装するのも手だ。

    3. DBについてはPostgresql等外部へ逃すことはできるがアップロード画像ファイルはDBに入らないためどうにもならない。ゆえにsupervisorやpm2のファイル更新監視モードは無限再起動に陥るため使うことができず、かえってそれらを使うメリットがない。本項でforeverを主に扱っているのはこういう事情による。

    4. ユーザ側でのstartは、root実行時と同じ起動パラメータを指定するようにしないと当然同じ動作にならない。故にサービス起動時はサンプルの4行目のように /etc/init.d/foreverdを直接実行する。

    5. visudoで Defaults requirettyDefaults:system_user !requiretty に修正すれば非TTYプロセス中でもsudoできるようになる。だが今回の場合はrootが非rootプロセスを起動したいのであり、非rootにはrootプロセスを起動させたくないのだから suのほうが理にかなう。

    RECENT LINKS