コンテンツにスキップ

バックエンドを選ぶ

nginxは、proxy_passでリクエストをバックエンドへ渡します。mruby_setを使うと、Rubyのスクリプトがリクエストごとにバックエンドを決め、proxy_passはスクリプトが設定した変数を使います。その後、nginxがバックエンドに接続し、受け取ったレスポンスを中継します。スクリプトはリクエストごとに1回動き、文字列を返します。

スクリプトは、リクエストヘッダX-Tenantと、テナントとバックエンドのアドレスのテーブルから、バックエンドを選びます。ヘッダの無いリクエストと、テーブルに無いテナントのリクエストには、どちらもバックエンドaを選びます。スクリプトの最後の行の値が、$backendの値になります。

test/conf/conf.d/73-guide-routing.conf
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";
}
test/html/guide_routing.rb
backends = {
"a" => "127.0.0.1:18164",
"b" => "127.0.0.1:18165",
}
tenant = Nginx::Request.new.headers_in["X-Tenant"].to_s
backends.fetch(tenant, backends["a"])
test/html/guide_routing.request:ケースが送るリクエスト
GET /orders HTTP/1.1
Host: 127.0.0.1:18163
X-Tenant: b
Connection: close
test/html/guide_routing.response:ケースが比べるレスポンス
HTTP/1.1 200 OK
Content-Type: text/plain
backend b on port 18165

CIが動くたびに、ケースのtest/t/cases/guides.rbがこのリクエストを送ります。ケースは、ステータス行、ここに載せたヘッダ、ボディ全体を比べます。5個のファイルを最後に変えたのは、2026-10-29のコミット32219e53です。

ケースはX-Tenantの無いリクエストも送り、ポート18164のバックエンドaのレスポンスを期待します。

同じ振り分け表を使い、別のポートとキャッシュ付きのRubyで試します。既存のnginx設定やバックエンドは不要です。見つからないキーはバックエンドaへ送ります。これは認証の判断ではありません。X-Tenantだけでテナントへのアクセスを許可しないでください。

実行した版は開発コミット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以外は未検証です。

次のブロック全体をシェルへ貼り付けます。新しい一時ディレクトリに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 ;; esac
RUN_DIR="$(mktemp -d)"
mkdir -p "$RUN_DIR/logs"
printf 'Runtime and logs: %s\n' "$RUN_DIR"
started=0
cleanup() {
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 0
trap 'exit 130' INT
trap 'exit 143' TERM
cat > "$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_s
backends.fetch(tenant, backends["a"])
RUBY
cat > "$RUN_DIR/nginx.conf" <<CONF
worker_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.conf
started=1
check() {
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 a
check 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=2
tenant-b -> 200 text/plain; backend b: /orders?limit=2
absent -> 200 text/plain; backend a: /orders?limit=2
unknown -> 200 text/plain; backend a: /orders?limit=2
address-input -> 200 text/plain; backend a: /orders?limit=2

アドレス形式のヘッダ値も接続先ではなく表のキーなので、バックエンドaへ送られます。リクエストは順番に送り、aからbへ、その後aへ切り替わることを確認します。負荷試験や性能測定ではありません。Rubyは文字列を選び、接続とレスポンスの中継はnginxが行います。

  • 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にあります。この例は既存テストを変えず、未知のキーとアドレス形式の入力も検査します。

  • mruby_setは、locationのrewriteフェーズで、nginxがバックエンドに接続する前に動きます。フェーズの順はリクエストのフェーズにあります。スクリプトはNginx::Requestでリクエストを読みます。レスポンスは見えません。
  • 変数を使ったproxy_passは、アドレスをそのまま使います。値がupstreamブロックの名前なら、そのブロックを使います。それ以外のホスト名には、nginxのresolverディレクティブが要ります。
  • リクエストヘッダはクライアントの入力です。スクリプトはヘッダをテーブルのキーと比べるので、クライアントはテーブルにあるバックエンドにしか届きません。詳しくは信頼境界にあります。
  • cacheが無いと、nginxはリクエストのたびにスクリプトのファイルを読みます。本番では、ディレクティブにcacheを付けるか、mruby_set_codeを使ってください。詳しくはコンパイルしたコードのキャッシュにあります。