Fedora ServerへのNode.jsとPM2インストール:SELinuxとfirewalldを使ったJavaScript本番アプリのデプロイ

Fedora tutorial - IT technology blog
Fedora tutorial - IT technology blog

FedoraにNode.jsをデプロイすると、まずSELinuxが出迎える

Fedoraをメインの開発マシンとして2年間使ってきて、パッケージの更新速度にはかなり満足している。しかし、初めてNode.jsアプリをFedora Serverの本番環境にデプロイしたとき、立ちはだかったのはNode.jsでもPM2でもなく、SELinuxだった。事前にUbuntuでテストしていたときは問題なく動いていたのに、Fedoraに持っていくとcurl http://server-ip:3000が完全にタイムアウトし、ログには何も残らず、エラーメッセージも一切出なかった。

Fedora Serverを本番環境として構築するなら、SELinuxを無効にするのは避けてほしい。StackOverflowの「quick fix」の多くがそう勧めているとしても、だ。この記事では一つひとつのステップを丁寧に解説する:Node.jsとPM2のインストール、そしてSELinuxとfirewalldの正しい設定まで。SELinuxは見落とされがちな部分で、その理由はほとんどのチュートリアルがUbuntu/Debianで動かしているから——SELinuxがデフォルトで有効になっていない環境だ。

作業を始める前に押さえておくべき概念

PM2 — なぜnode app.jsを直接使わないのか?

node app.jsでも動くが、それは本番環境向けのセットアップではない。PM2はNode.js専用のプロセスマネージャーで、実際の現場で直面する3つの問題を解決する:

  • 自動再起動:アプリがクラッシュしても自動で再起動するため、深夜3時に画面を監視する必要がない
  • クラスターモード:4コアのサーバーでは、PM2が4つのインスタンスを並列実行する。Node.jsはシングルスレッドのため、シングルプロセスと比べてスループットが約3.5倍向上する
  • ログ管理:ログを一元管理し、自動ローテーションでディスク溢れを防ぐ

SELinuxコンテキストとポートラベリング

SELinuxはシステム内の各ファイル、プロセス、ポートにラベル(コンテキスト)を付与する。ポート3000はデフォルトのリストに含まれていないため、アプリが正しいアドレスでリッスンしていても、カーネルはすべての接続をブロックする。エラーは何も出ず、リクエストはただ静かにタイムアウトするだけだ。アプリのポートにhttp_port_tラベルを追加する必要がある。

firewalldのゾーン

Fedoraはiptablesではなくfirewalldを使用している。ポートを開くには、publicゾーンまたは使用中のインターフェースのゾーンに追加する必要があり、再起動後も設定が失われないよう--permanentフラグと--reloadを組み合わせて使う。

ステップごとの詳細な実践手順

ステップ1:Node.jsのインストール

FedoraのデフォルトリポジトリにはNode.jsが含まれているが、バージョンが古いことが多い。最新のLTS版を取得するにはNodeSourceリポジトリを使用しよう:

# Node.js 20 LTS用のNodeSourceリポジトリを追加
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -

sudo dnf install -y nodejs

# バージョン確認
node --version   # v20.x.x
npm --version

Fedoraエコシステム内に留まりたい場合はdnf moduleを使う方法もある:

# 利用可能なNode.jsのストリームを確認
sudo dnf module list nodejs

# ストリーム20を有効化してインストール
sudo dnf module enable nodejs:20 -y
sudo dnf install nodejs -y

ステップ2:PM2のインストール

sudo npm install -g pm2
pm2 --version

ステップ3:サンプルアプリケーションのデプロイ

全体の流れをテストするためのシンプルなアプリを作成する:

sudo mkdir -p /var/www/myapp
sudo chown $USER:$USER /var/www/myapp
cd /var/www/myapp
// /var/www/myapp/app.js
const http = require('http');
const PORT = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello from Fedora Server!\n');
});

server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
cd /var/www/myapp
pm2 start app.js --name "myapp"
pm2 status

PM2がonlineと表示していても、外部からcurl http://server-ip:3000を実行するとタイムアウトする——これがSELinuxとfirewalldを設定するタイミングだ。

ステップ4:SELinuxの設定

SELinuxがブロックしているかどうかを確認する:

sudo ausearch -m avc -ts recent | grep node
# または
sudo tail -50 /var/log/audit/audit.log | grep denied

deniedの行が表示されれば正しい方向だ——原因が特定できるだけでも、闇雲に試すより遥かにましだ。修正方法は以下の通り:

# policycoreutils-python-utilsがなければインストール
sudo dnf install -y policycoreutils-python-utils

# HTTP許可済みのポートを確認
sudo semanage port -l | grep http_port_t

# ポート3000をhttp_port_tに追加
sudo semanage port -a -t http_port_t -p tcp 3000

# 再確認
sudo semanage port -l | grep 3000

アプリがファイルへの書き込みを必要とする場合(アップロード、キャッシュ、一時ファイルなど)、対象ディレクトリに書き込みを許可するSELinuxコンテキストが必要だ:

# uploadsディレクトリに書き込みコンテキストを設定
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/myapp/uploads(/.*)?"
sudo restorecon -Rv /var/www/myapp/uploads/

アプリが外部APIを呼び出したり、別ホストのデータベースに接続したりする場合は、このブール値を必ず有効にする必要がある:

# Node.jsがネットワーク経由で外部に接続することを許可
sudo setsebool -P httpd_can_network_connect 1

-Pフラグで再起動後も設定が維持される。これを忘れると、MongoDBへの接続やサードパーティAPIの呼び出しが何の警告もなく失敗する。

ステップ5:firewalldでポートを開く

# アクティブなゾーンを確認
sudo firewall-cmd --get-active-zones

# ポート3000を開く
sudo firewall-cmd --permanent --add-port=3000/tcp
sudo firewall-cmd --reload

# 確認
sudo firewall-cmd --list-ports

Nginxをリバースプロキシとして前段に配置する場合(本番環境では推奨)、80/443のみを開放してアプリのポートは内部に留める:

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

ステップ6:PM2をsystemdで自動起動する

このステップを忘れる人が多い——サーバーが再起動したとき、SSHでログインして手動で起動しなくてもアプリが自動で立ち上がる必要がある:

# Generate startup script
pm2 startup systemd

# 上記コマンドがsudoコマンドを出力するので、それをコピーして実行する。例:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u myuser --hp /home/myuser

# 現在のプロセスリストを保存
pm2 save

# サービスを確認
sudo systemctl status pm2-myuser

ステップ7:本番環境設定のためのエコシステムファイル

長いフラグを覚える代わりに、エコシステムファイルを使ってコードと一緒に設定をバージョン管理しよう:

// /var/www/myapp/ecosystem.config.js
module.exports = {
  apps: [{
    name: 'myapp',
    script: './app.js',
    instances: 'max',         // すべてのCPUコアを使用
    exec_mode: 'cluster',     // 高可用性のためのクラスターモード
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    error_file: '/var/log/myapp/error.log',
    out_file: '/var/log/myapp/out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss'
  }]
};
# エコシステムファイルで起動
pm2 start ecosystem.config.js

# ゼロダウンタイムリロード(処理中のリクエストを失わない)
pm2 reload myapp

# Monitor real-time
pm2 monit

まとめ

Node.jsとPM2のインストール自体は素早く終わる。時間がかかるのはSELinuxとfirewalldの設定だ——ほとんどのUbuntu/Debianチュートリアルでは触れられない部分だ。以下の3つのコマンドのどれかが欠けると、ローカルでは正常に動くのに外部からの接続を拒否する——あるいはさらに厄介なことに、HTTPは通るのにデータベース接続ができないという状況になる:

  • semanage port -a -t http_port_t -p tcp <port> — そのポートへの接続をSELinuxに許可させる
  • setsebool -P httpd_can_network_connect 1 — アプリが外部に接続することを許可する
  • firewall-cmd --permanent --add-port=<port>/tcp && firewall-cmd --reload — ファイアウォールのポートを開く

PM2については、pm2 startuppm2 saveは絶対に忘れてはいけないステップだ——サーバーが再起動してアプリが自動起動しないと、翌朝になって初めて気づくことになる。Nginxをリバースプロキシとして前段に置き、アプリのポートを内部に留めてインターネットに直接さらさないようにしよう。

Share: