matsukaz's blog

Agile, node.js, ruby, AWS, cocos2d-xなどなどいろいろやってます

PM2でNode.jsアプリケーションをCapistranoライクにデプロイ管理

qiita.com

この記事は Qiita Node.js Advent Calendar 2018 17日目 です。

Node.js アプリケーションの実行環境として PM2 を利用されている方も多いと思います。 そのまま使うだけでも十分便利ですが、本番運用時にはCapistranoライクなデプロイ管理方法がオススメです。

今回はそのための設定方法をご紹介します(公式に その方法 が載っていることを知らずに書き始めたため、公式のドキュメントの翻訳版のような形になってしまいました…すみません)。

PM2とは

PM2は、Node.jsアプリケーションの本番向け実行環境で、以下の特徴があります。

  • 設定を書くだけでマルチプロセス実行(通常はClusterモジュールを利用した実装が必要)
  • プロセス管理
    • プロセスを簡単に起動/停止
    • 不意に停止したプロセスの自動起動
    • ダウンタイム0でプロセスを再起動(アプリケーションの更新時など)
  • ログ管理やプロセスのメトリクスなど、Devopsに必要な様々な機能を提供

使わない手はないですよね。

サンプルアプリケーション

以下のサンプルアプリケーションを例に説明していきます。

$ tree -L 1
.
├── app.js
├── node_modules
├── package.json
└── pm2.json

$ cat package.json
{
  "main": "app.js",
  "dependencies": {
    "express": "^4.16.4"
  }
}

$ cat pm2.json
{
  "apps" : [{
    "script"    : "app.js",
    "instances" : "2",
    "exec_mode" : "cluster",
    "name"      : "app",
  }]
}

$ cat app.js
var express = require("express");
var app = express();

var server = app.listen(3000, function() {
    console.log("Listening: " + server.address().port);
});

app.get("/", function(req, res, next) {
    res.json({message: 'SUCCESS: version 1.'});
});

$ pm2 start pm2.json
[PM2][WARN] Applications app not running, starting...
[PM2] App [app] launched (2 instances)
┌──────────┬────┬─────────┬─────────┬───────┬────────┬─────────┬────────┬─────┬───────────┬──────────┬──────────┐
│ App name │ id │ version │ mode    │ pid   │ status │ restart │ uptime │ cpu │ mem       │ user     │ watching │
├──────────┼────┼─────────┼─────────┼───────┼────────┼─────────┼────────┼─────┼───────────┼──────────┼──────────┤
│ app      │ 0  │ 1.0.0   │ cluster │ 68006 │ online │ 0       │ 0s     │ 0%  │ 28.4 MB   │ matsukaz │ disabled │
│ app      │ 1  │ 1.0.0   │ cluster │ 68007 │ online │ 0       │ 0s     │ 0%  │ 20.5 MB   │ matsukaz │

$ curl -X GET http://localhost:3000/
{"message":"success 1"}

$ pm2 stop app
[PM2] Applying action stopProcessId on app [app](ids: 0,1)
[PM2] [app](0) ✓
[PM2] [app](1) ✓
┌──────────┬────┬─────────┬─────────┬─────┬─────────┬─────────┬────────┬─────┬────────┬──────────┬──────────┐
│ App name │ id │ version │ mode    │ pid │ status  │ restart │ uptime │ cpu │ mem    │ user     │ watching │
├──────────┼────┼─────────┼─────────┼─────┼─────────┼─────────┼────────┼─────┼────────┼──────────┼──────────┤
│ app      │ 0  │ 1.0.0   │ cluster │ 0   │ stopped │ 0       │ 0      │ 0%  │ 0 B    │ matsukaz │ disabled │
│ app      │ 1  │ 1.0.0   │ cluster │ 0   │ stopped │ 0       │ 0      │ 0%  │ 0 B    │ matsukaz │ disabled │
└──────────┴────┴─────────┴─────────┴─────┴─────────┴─────────┴────────┴─────┴────────┴──────────┴──────────┘

$ pm2 start app
[PM2] Applying action restartProcessId on app [app](ids: 0,1)
[PM2] [app](0) ✓
[PM2] [app](1) ✓
[PM2] Process successfully started
┌──────────┬────┬─────────┬─────────┬───────┬────────┬─────────┬────────┬─────┬───────────┬──────────┬──────────┐
│ App name │ id │ version │ mode    │ pid   │ status │ restart │ uptime │ cpu │ mem       │ user     │ watching │
├──────────┼────┼─────────┼─────────┼───────┼────────┼─────────┼────────┼─────┼───────────┼──────────┼──────────┤
│ app      │ 0  │ 1.0.0   │ cluster │ 68120 │ online │ 0       │ 0s     │ 0%  │ 28.4 MB   │ matsukaz │ disabled │
│ app      │ 1  │ 1.0.0   │ cluster │ 68121 │ online │ 0       │ 0s     │ 0%  │ 20.6 MB   │ matsukaz │ │ disabled │
└──────────┴────┴─────────┴─────────┴───────┴────────┴─────────┴────────┴─────┴───────────┴──────────┴──────────┘

$ pm2 delete app
[PM2] Applying action deleteProcessId on app [app](ids: 0,1)
[PM2] [app](0) ✓
[PM2] [app](1) ✓
┌──────────┬────┬─────────┬──────┬─────┬────────┬─────────┬────────┬─────┬─────┬──────┬──────────┐
│ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │
└──────────┴────┴─────────┴──────┴─────┴────────┴─────────┴────────┴─────┴─────┴──────┴──────────┘

Capistranoライクにデプロイ管理

まずデプロイ対象となるアプリケーションを、以下のようなディレクトリ構造で管理します。 releases ディレクトリ配下にはアプリケーションをバージョンごとのディレクトリ(タイムスタンプでもなんでも構いません)で管理し、 currentシンボリックリンクで最新バージョンのディレクトリを参照させます。

$ tree -L 3
.
├── current -> releases/1
└── releases
    ├── 1
    │   ├── app.js
    │   ├── node_modules
    │   ├── package.json
    │   └── pm2.json
    └── 2
        ├── app.js
        ├── node_modules
        ├── package.json
        └── pm2.json

このようなディレクトリ構造を作成し、 pm2.jsoncwd プロパティに current ディレクトリまでの絶対パス指定を追加します。

$ cat pm2.json
{
  "apps" : [{
    "script"    : "app.js",
    "instances" : "2",
    "exec_mode" : "cluster",
    "cwd"       : "/<currentまでの絶対パス指定>/current",    // これ
    "name"      : "app",
  }]
}

これで準備完了です。

current1 のバージョンを指した状態でPM2でアプリケーションを開始してみます。

$ cd current

$ pm2 start pm2.json
[PM2][WARN] Applications app not running, starting...
[PM2] App [app] launched (2 instances)
┌──────────┬────┬─────────┬─────────┬───────┬────────┬─────────┬────────┬─────┬───────────┬──────────┬──────────┐
│ App name │ id │ version │ mode    │ pid   │ status │ restart │ uptime │ cpu │ mem       │ user     │ watching │
├──────────┼────┼─────────┼─────────┼───────┼────────┼─────────┼────────┼─────┼───────────┼──────────┼──────────┤
│ app      │ 0  │ 1.0.0   │ cluster │ 69058 │ online │ 0       │ 0s     │ 0%  │ 28.0 MB   │ matsukaz │ disabled │
│ app      │ 1  │ 1.0.0   │ cluster │ 69059 │ online │ 0       │ 0s     │ 0%  │ 20.2 MB   │ matsukaz │ disabled │
└──────────┴────┴─────────┴─────────┴───────┴────────┴─────────┴────────┴─────┴───────────┴──────────┴──────────┘

$ curl -X GET http://localhost:3000/
{"message":"SUCCESS: version 1."}

さっきと同様に起動しました。

次に currentシンボリックリンク2 のバージョンを指すように変更し、ダウンタイム0でプロセスを再起動する reload を実行します。

$ ln -snf releases/2 current

$ cd current

$ pm2 reload app
Use --update-env to update environment variables
[PM2] Applying action reloadProcessId on app [app](ids: 0,1)
[PM2] [app](0) ✓
[PM2] [app](1) ✓

$ curl -X GET http://localhost:3000/
{"message":"SUCCESS: version 2."}

アプリケーションが更新されました!簡単ですね。

reload は、アプリケーションのバージョン間で互換性がある場合に有効な再起動方法です。 瞬間的であっても、複数のバージョンのアプリケーションが同時起動するのが許容できない場合は restart を利用します(瞬断が発生します)。

まとめ

今回の方法を取らずに1つのディレクトリを更新し続けるのもアリですが、以下の点でオススメはできません。

  • バージョンの切り戻しがしづらい
  • 更新中に意図せずプロセスが再起動すると、その時点の更新内容が反映されてしまう(場合によってはバージョンの異なるプロセスが同時起動する)
  • マイグレーションなど、現行バージョンを動かしながら新しいバージョンの事前処理などが行いやすい

Capistranoライクなデプロイ管理、まだ利用されていない方は利用してみてはいかがでしょうか?

おまけ

pm2 を利用していてよく問題になるのが、プロセスを安全に再起動したいのになかなかプロセスが落ちてくれないケースです。 主な原因としては

  1. DBコネクションなどが終了時にcloseされていない
  2. New Relicなどのサードパーティーのサービスとの接続が切断されていない
  3. HTTPのkeep-aliveが有効となっていて、クライアントがコネクションを掴んでしまっている

特に 3 の場合は強制切断が必要になるのですが、その辺りの管理が意外と面倒だったりします。 以下のstoppableを使うと、強制切断までの時間を簡単に設定できるのでオススメです。

GitHub - hunterloftis/stoppable: Node's `server.close` the way you expected it to work.

それでは次の方へ〜!!!

Node.js超入門[第2版]

Node.js超入門[第2版]

www.wantedly.com