バックエンドを選ぶ
nginxは、proxy_passでリクエストをバックエンドへ渡します。mruby_setを使うと、Rubyのスクリプトがリクエストごとにバックエンドを決め、proxy_passはスクリプトが設定した変数を使います。その後、nginxがバックエンドに接続し、受け取ったレスポンスを中継します。スクリプトはリクエストごとに1回動き、文字列を返します。
スクリプトは、リクエストヘッダX-Tenantと、テナントとバックエンドのアドレスのテーブルから、バックエンドを選びます。ヘッダの無いリクエストと、テーブルに無いテナントのリクエストには、どちらもバックエンドaを選びます。スクリプトの最後の行の値が、$backendの値になります。
server { listen 18163;
location / { mruby_set $backend build/nginx/html/guide_routing.rb; proxy_pass http://$backend; }}
server { listen 18164; default_type text/plain; return 200 "backend a on port 18164\n";}
server { listen 18165; default_type text/plain; return 200 "backend b on port 18165\n";}backends = { "a" => "127.0.0.1:18164", "b" => "127.0.0.1:18165",}tenant = Nginx::Request.new.headers_in["X-Tenant"].to_sbackends.fetch(tenant, backends["a"])GET /orders HTTP/1.1Host: 127.0.0.1:18163X-Tenant: bConnection: closeHTTP/1.1 200 OKContent-Type: text/plain
backend b on port 18165CIが動くたびに、ケースのtest/t/cases/guides.rbがこのリクエストを送ります。ケースは、ステータス行、ここに載せたヘッダ、ボディ全体を比べます。5個のファイルを最後に変えたのは、2026-10-29のコミット32219e53です。
ケースはX-Tenantの無いリクエストも送り、ポート18164のバックエンドaのレスポンスを期待します。
同じ振り分け表を使い、別のポートとキャッシュ付きのRubyで試します。既存のnginx設定やバックエンドは不要です。見つからないキーはバックエンドaへ送ります。これは認証の判断ではありません。X-Tenantだけでテナントへのアクセスを許可しないでください。
前提と対象版
Section titled “前提と対象版”実行した版は開発コミット6d65a12ecです。その標準gem lockとnginx 1.31.8を使い、Ubuntu 22.04 aarch64・GCC 11.4.0で確認しました。next向けの例であり、v3の正式提供を示すものではありません。この手順をリリース済みのv2.7.2タグでは実行していません。
ngx_mrubyのHTTPモジュールを組み込んだnginxが必要です。インストールを参照し、実行ファイルの絶対パスをNGX_BINへ設定します。現在のチェックアウトのbuild/nginxへインストールした場合は、次の設定を使います。
export NGX_BIN="$PWD/build/nginx/sbin/nginx""$NGX_BIN" -Vシェルにはcurl、mktemp、cmpと標準的なPOSIXコマンドが必要です。nginxとcurlが使う同じネットワーク名前空間で、19480・19481・19482番ポートを空けてください。一般ユーザーか、隔離したテスト用コンテナで実行します。ホストへのポート公開は不要です。モジュールの無い配布版nginxでは動きません。動的ロードとLinux以外は未検証です。
作成から停止まで
Section titled “作成から停止まで”次のブロック全体をシェルへ貼り付けます。新しい一時ディレクトリに2つのファイルを作り、設定を検査して専用のnginxを起動します。5つの入力についてステータス、メディア型、本文全体を照合し、そのインスタンスだけを停止します。表示したディレクトリには、調査用のログとファイルを残します。heredoc内のnginx変数のバックスラッシュは残してください。シェルが展開するのはRUN_DIRです。
(set -eu: "${NGX_BIN:?Set NGX_BIN to the absolute path of nginx with ngx_mruby}"case "$NGX_BIN" in /*) ;; *) echo 'NGX_BIN must be absolute' >&2; exit 1 ;; esacRUN_DIR="$(mktemp -d)"mkdir -p "$RUN_DIR/logs"printf 'Runtime and logs: %s\n' "$RUN_DIR"started=0cleanup() { result=$? trap - 0 if [ "$started" = 1 ]; then "$NGX_BIN" -p "$RUN_DIR/" -c nginx.conf -s quit || result=1 tries=0 while [ -f "$RUN_DIR/logs/nginx.pid" ] && [ "$tries" -lt 50 ]; do sleep 0.1 tries=$((tries + 1)) done if [ -f "$RUN_DIR/logs/nginx.pid" ]; then echo 'Shutdown not confirmed; inspect the runtime logs' >&2 result=1 fi fi if [ "$result" != 0 ]; then tail -n 30 "$RUN_DIR/logs/error.log" >&2 || : fi exit "$result"}trap cleanup 0trap 'exit 130' INTtrap 'exit 143' TERMcat > "$RUN_DIR/route.rb" <<'RUBY'backends = { "a" => "127.0.0.1:19481", "b" => "127.0.0.1:19482",}tenant = Nginx::Request.new.headers_in["X-Tenant"].to_sbackends.fetch(tenant, backends["a"])RUBYcat > "$RUN_DIR/nginx.conf" <<CONFworker_processes 1;pid logs/nginx.pid;error_log logs/error.log notice;events { worker_connections 64; }http { access_log off; default_type text/plain; server { listen 127.0.0.1:19480; location / { mruby_set \$backend "$RUN_DIR/route.rb" cache; proxy_pass http://\$backend; } } server { listen 127.0.0.1:19481; return 200 "backend a: \$request_uri\n"; } server { listen 127.0.0.1:19482; return 200 "backend b: \$request_uri\n"; }}CONF"$NGX_BIN" -p "$RUN_DIR/" -c nginx.conf -t"$NGX_BIN" -p "$RUN_DIR/" -c nginx.confstarted=1check() { label=$1 backend=$2 shift 2 meta=$(curl --noproxy '*' --fail --silent --show-error --max-time 5 \ -D "$RUN_DIR/headers" -o "$RUN_DIR/body" \ -w '%{http_code} %{content_type}' "$@" \ 'http://127.0.0.1:19480/orders?limit=2') test "$meta" = '200 text/plain' printf 'backend %s: /orders?limit=2\n' "$backend" > "$RUN_DIR/expected" cmp "$RUN_DIR/expected" "$RUN_DIR/body" printf '%s -> %s; ' "$label" "$meta" cat "$RUN_DIR/body"}check tenant-a a -H 'X-Tenant: a'check tenant-b b -H 'X-Tenant: b'check absent acheck unknown a -H 'X-Tenant: other'check address-input a -H 'X-Tenant: 127.0.0.1:1')実行ディレクトリとnginxの設定検査メッセージに続き、成功した照合は次の5行を出力します。本文の末尾には改行が1つあり、cmpはそれも照合します。パスとクエリ文字列も選んだバックエンドへ届きます。
tenant-a -> 200 text/plain; backend a: /orders?limit=2tenant-b -> 200 text/plain; backend b: /orders?limit=2absent -> 200 text/plain; backend a: /orders?limit=2unknown -> 200 text/plain; backend a: /orders?limit=2address-input -> 200 text/plain; backend a: /orders?limit=2アドレス形式のヘッダ値も接続先ではなく表のキーなので、バックエンドaへ送られます。リクエストは順番に送り、aからbへ、その後aへ切り替わることを確認します。負荷試験や性能測定ではありません。Rubyは文字列を選び、接続とレスポンスの中継はnginxが行います。
失敗を調べる
Section titled “失敗を調べる”unknown directive "mruby_set"なら、NGX_BINと-Vの出力を確認します。Ruby処理系だけでなく、ngx_mrubyを組み込んだ実行ファイルが必要です。bind() ... address already in useなら、既存プロセスは停止しないでください。3つのポートを空けるか、設定、Rubyの表、curlのURLの対応するアドレスを全て変更してから再実行します。502やcurlの接続エラーなら、表示したディレクトリのlogs/error.logと3つの待受を確認します。この数値アドレスにDNS resolverは不要です。- 条件式や
cmpが失敗したら、同じディレクトリのheaders、body、expectedを見比べます。200だけでは正しい振り分けと判断できません。 - Rubyの読込みに失敗したら、生成された絶対パスとディレクトリの権限を確認します。
cacheを使うため、編集した内容は例を再起動して読み直します。
正常終了時は、一時インスタンスのPIDファイルが消えたことまで確認します。途中で失敗しても、起動済みならtrapがそのインスタンスへ正常終了を要求します。停止が未確認なら、記録したインスタンスを調べてからポートを再利用します。
ログが不要になったら、この例が表示したディレクトリだけを削除できます。既存の結合テストはtest/t/cases/guides.rbにあります。この例は既存テストを変えず、未知のキーとアドレス形式の入力も検査します。
気をつけること
Section titled “気をつけること”mruby_setは、locationのrewriteフェーズで、nginxがバックエンドに接続する前に動きます。フェーズの順はリクエストのフェーズにあります。スクリプトはNginx::Requestでリクエストを読みます。レスポンスは見えません。- 変数を使った
proxy_passは、アドレスをそのまま使います。値がupstreamブロックの名前なら、そのブロックを使います。それ以外のホスト名には、nginxのresolverディレクティブが要ります。 - リクエストヘッダはクライアントの入力です。スクリプトはヘッダをテーブルのキーと比べるので、クライアントはテーブルにあるバックエンドにしか届きません。詳しくは信頼境界にあります。
cacheが無いと、nginxはリクエストのたびにスクリプトのファイルを読みます。本番では、ディレクティブにcacheを付けるか、mruby_set_codeを使ってください。詳しくはコンパイルしたコードのキャッシュにあります。