ひとつのポートで異なる通信プロトコルを nginx の nginScript で振り分ける

インダストリー事業本部のイチノです。リモート製品 (Optimal Remote, Optimal Second Sight, ポケットドクターなど遠隔地とコミュニケーションするための製品) で使われるコア技術をまとめた Communication SDK を担当しています。

本記事では、ひとつのポートで異なる通信プロトコルを、 nginx で振り分けたい場合に、役立つ方法を紹介します。

80 ポートや 443 ポートのみ許可しているセキュアなネットワーク環境へ、複数のプロトコルを持ったサービスを提供するといった場合に使える方法です。

nginx によるリバースプロキシの振り分け方法として、以下の6つが存在します。

  1. IP アドレスでの振り分け
  2. ポート番号での振り分け
  3. SSL/TLS の SNI による振り分け
  4. HTTP のパスによる振り分け
  5. HTTP の Host ヘッダーによる振り分け
  6. スクリプトによる振り分け

本記事では、 方法 6 のスクリプトによる振り分け方法を紹介します。

他の方法に対して方法 6 のメリットは、柔軟にルールを記述できることです。

デメリットは、他の方法と比べて遅いということです。 nginx が 2017 年に発表した資料によると、 nginScript を呼び出すだけで 10 %の低下。正規表現を使い振り分けを行った場合、30%ほどの低下。アクセス数が多いサービスへの導入には、他の方法も考慮したほうが良いです。

www.nginx.com

nginx で使用できるスクリプトとして Lua 等がありますが、本記事では、 nginScript での方法を紹介します。nginScript は、 nginx を設定するための JavaScript 実装です。他のスクリプトに対して nginScript を利用するメリットは次の通りです。

nginx のスクリプト以外に、 Go 言語で実現する方法もあります。本記事では、 nginx に限定します。

github.com

通信プロトコルごとにアクセスを振り分ける

例では、 TCP 80 番ポートへのアクセスに対して、クライアントから送られてきたデータを見て、HTTPか HTTP 以外の通信プロトコルかを判断させています。簡易的に GET メソッドのみ実装しています。

nginScript を利用するための nginx の設定。

load_module "modules/ngx_stream_js_module.so";

events {
    worker_connections  1024;
}

...

stream {
    js_include stream.js;

    js_set $server_url server_url;

    server {
        listen 80;

        js_preread preread;
        proxy_pass $server_url;
    }
}

js_include ディレクティブで、ロードさせたいスクリプトを指定します。

load_module ディレクティブは、 events ディレクティブよりも前に書きます。後に書いた場合は、エラーが発生します *1

nginx の設定中の js_set で宣言した変数の値は、 nginScript 内に書かれた関数の戻り値で、決定できます。 変数の値が決定されるタイミングは、 nginx のディレクティブに依存します。

続いて、 stream.js の中身。

var server_url_ = '';

var servers_ = {
  http: '127.0.0.1:8080',
  echo: '127.0.0.1:8081'
};

function preread(s) {
  s.on('upload', function(data, flags) {
    if (data.length === 0) {
      s.log('Continue to read.');
    } else if (data.startsWith('GET')) {
      s.log('HTTP protocol with GET method.')
      server_url_ = servers_.http;
      s.done();
    } else {
      s.log('ECHO protol.');
      server_url_ = servers_.echo;
      s.done();
    }
  });
}

function server_url(s) {
  return server_url_;
}

preread 関数で、振り分け先のサーバを決定しています。この後に server_url 関数が呼ばれ、戻り値として振り分け先のサーバを proxy_pass ディレクティブに渡します。

preread 関数で、クライアントから nginx への送信データを コールバック関数で受け取るようにしています。コールバック関数内で、データを見て、HTTP の GET メソッドか、それ以外のプロトコルとして分岐させています。なお、 if (data.length === 0) については、コールバック関数が最初に呼ばれると data が空文字になるので、ガード条件として設けています。

s.done() を呼ぶことで preread のフェーズを完了させ、次のフェーズに移行します。 nginx のフェーズについては、以下のドキュメントが参考になります。

ドキュメント

参考にしたドキュメントをまとめて列挙します。

おわりに

ひとつのポートで異なる通信プロトコルを振り分ける方法を紹介しました。 nginScript を使うことで、nginx の静的な設定ファイルでは実現できなかった、通信データから判断して振り分けることを確認できました。

オプティムでは、nginx に限らず様々なことに興味があるエンジニアを募集しています。