はじめに
サーバにログインして作業していたら、こんな場面に出くわしました。
$ which ruby
ruby not found
$ ps aux | grep unicorn
deploy 12345 ... unicorn master -c /var/www/app/config/unicorn.rb
deploy 12346 ... unicorn worker[0] -c /var/www/app/config/unicorn.rb
ruby コマンドは無いのに、Ruby 製のアプリケーションサーバ Unicorn は元気に動いている。
「Ruby が無いのに Ruby のサーバが動く」というのは一見矛盾しています。この謎を入り口に、インタプリタとコンパイラの違いまで掘り下げてみます。
結論:Unicorn が動くなら Ruby は必ず存在する
先に答えを言ってしまうと、Unicorn が動いている時点で、Ruby はそのインスタンス内に必ず存在しています。
Unicorn は Ruby 製のアプリケーションサーバです。Ruby インタプリタなしには 1 プロセスも起動できません。
つまり「shell に ruby が無い」のは Ruby が存在しない のではなく、
あなたのシェルの
PATHから見えていないだけ
なのです。
なぜ shell から見えないのか
PATH から Ruby が見えなくなる理由は、主に次の 4 つです。
1. バージョンマネージャ(rbenv / rvm / asdf)配下にある
最も多いパターンです。Ruby は次のような場所にインストールされます。
~/.rbenv/versions/3.3.0/bin/ruby
~/.rvm/rubies/ruby-3.3.0/bin/ruby
~/.asdf/installs/ruby/3.3.0/bin/ruby
これらを PATH に通すのは .bashrc / .bash_profile の初期化スクリプトの仕事です。非対話シェル・別ユーザー・sh の直起動などでは shim が PATH に入らず、ruby: command not found になります。
2. Unicorn を起動したユーザー / 環境が違う
Unicorn は deploy や www-data など専用ユーザーで動いていることが多く、Ruby はそのユーザーの環境にだけ入っています。あなたのログインシェルとは別環境、というわけです。
3. systemd / init 経由でフルパス指定で起動されている
サービス定義に絶対パスが書かれていれば、PATH に依存せず起動できます。
ExecStart=/home/deploy/.rbenv/shims/bundle exec unicorn -c /var/www/app/config/unicorn.rb
4. コンテナ境界
Unicorn がコンテナ内で動いており、Ruby はそのコンテナイメージにだけ含まれている(ホストのシェルには無い)ケースです。
実際の Ruby を突き止める方法
「見えていないだけ」なら、実体を突き止められます。
# 1. Unicorn プロセスを特定
ps aux | grep -i unicorn
# 2. 動いているバイナリの実体を確認(Linux)
ls -l /proc/<PID>/exe
# 3. そのプロセスの PATH や環境変数を確認
cat /proc/<PID>/environ | tr '\0' '\n' | grep -E 'PATH|GEM|RBENV|RUBY'
# 4. 起動ユーザーの環境で which
sudo -u deploy -i which ruby
ps の出力には ruby や unicorn master のコマンドライン全体(多くはフルパス付き)が見えるので、そこからインストール先をたどれます。
補足:プロセス起動後に Ruby バイナリを削除しても、すでに動いている Unicorn は走り続けます(実行中プロセスが inode を掴んでいるため)。ただしこれは稀なケースで、通常は上記 1〜4 の「PATH の問題」と考えてまず間違いありません。
そもそも、なぜ Ruby が「存在し続ける」必要があるのか
ここが今回の本題です。なぜ Ruby 製サーバは、実行中ずっと Ruby を必要とするのか。
答えは「Ruby がインタプリタ型言語だから」です。ここでコンパイラとインタプリタの違いを整理しておきましょう。
コンパイラ(Compiler)
ソースコードを実行前にまとめて機械語(や中間形式)へ変換するプログラムです。
ソースコード ──[コンパイラ]──> 実行ファイル ──> 実行
(事前に1回) (何度でも)
- 変換(ビルド)と実行が別のフェーズに分かれる
- 一度変換すれば、生成された実行ファイルを何度でもそのまま実行できる
- 実行時はネイティブコードが直接 CPU で動くため高速
- 変換時にコード全体を解析するので、型エラーなどを実行前に検出できる
- ビルドという手間が増え、生成物は基本的に特定の OS / CPU に依存する
例:C, C++, Rust, Go
インタプリタ(Interpreter)
ソースコードを実行時に逐次読み取って、その場で解釈・実行するプログラムです。
ソースコード ──[インタプリタが読みながら実行]──> 結果
(実行のたびに毎回)
- 変換と実行が一体。事前のビルド工程がない
- ソースコードと、それを動かすインタプリタ本体が実行時に必要
- 1 行ずつ解釈しながら走るため、一般にコンパイラ型より低速
- すぐ動かせる、対話実行(REPL)ができる、プラットフォーム差をインタプリタが吸収するので移植しやすい
- 文法エラーやバグが実行して初めて発覚しやすい
例:Ruby, Python, JavaScript, PHP
C 製サーバと Ruby 製サーバの違い
この違いが、冒頭の謎にそのまま効いてきます。
| C 製サーバ(例:nginx) | Ruby 製サーバ(例:Unicorn) | |
|---|---|---|
| 配布されるもの | コンパイル済み実行ファイル | Ruby のソースコード(gem) |
| 実行時に必要なもの | 実行ファイルのみ | ruby インタプリタ一式 |
| コンパイラ | 実行時は不要 | (そもそもインタプリタ型) |
C で書かれたサーバは、コンパイル済み実行ファイルさえあればコンパイラは要りません。一方 Ruby はソースコードを解釈・実行する ruby インタプリタ本体が、インスタンス内に存在し続ける必要がある。だから「Unicorn が動く= Ruby が存在する」が必ず成り立つのです。
まとめ
- Unicorn(Ruby 製サーバ)が動いているなら、Ruby は必ずそのインスタンスに存在する
-
ruby: command not foundは「無い」のではなく「自分のシェルのPATHから見えていない」だけ - 原因はたいてい rbenv 等のバージョンマネージャ、起動ユーザーの違い、systemd のフルパス指定、コンテナ境界のいずれか
-
ps//proc/<PID>/exe//proc/<PID>/environで実体は突き止められる - Ruby が実行中ずっと必要なのは、インタプリタ型言語だから。C 製サーバのようにビルド済みバイナリだけでは動かない
「Ruby が無いのに Ruby のサーバが動く」という違和感は、インタプリタとコンパイラの違いを理解する良い入り口でした。