http-proxyモジュールを使って名前ベースのバーチャルホストを実現する

自前のサーバーで、多数のWebアプリを稼動させる場合、大抵1つしかIPアドレスを持たないので、不特定多数の人がアクセスするブログを80番、その他を80番以外のポートを割り当てて個別に立ち上げるか、Apacheなどのバーチャルホスト機能を使うしか、簡単な方法がなかった。

今筆者の環境では、全面的にApache上で稼動するWebアプリやブログをNode.jsプラットフォームに移植する作業を粛々と行っており、Apacheの名前ベースのバーチャルホストのような機能が実現できないか調べていると、http-proxyモジュールを使って実現するのが一般的のようなので、サンプルプログラムを作成して期待する結果になるか調査したときのメモを残す。

実現したい構成イメージ

  • サーバー端末、IPアドレス、ドメインは1つだけ
  • 複数のサーバーアプリケーションプロセスを稼動させる
  • 各アプリケーションにはサブドメイン形式のURLでアクセスできるようにする
  • socket.ioを使った通信処理もサポートしたい
  www.hoge.com => ブログアプリケーション  
  aaa.hoge.com => Aアプリケーション  
  bbb.hoge.com => Bアプリケーション  
  :

どうやって実現するか?

  • すべてのアプリケーションを80番ポート以外で稼動させる
  • http-proxyを使ったプロキシサーバーを80番で稼動させて、同一ドメインへのリクエストをすべてこのプロキシを経由させる。
  • リクエストのホスト名から判断して、トランスレート先のサーバーに処理を丸投げする
  • サブドメイン設定には、ワイルドカードを使う
proxy server (*.hoge.com:80) 
├ app1 (port:3000)  www.hoge.com
├ app2 (port:3001)  aaa.hoge.com
:

こうすれば、1つのアプリケーションが肥大化することもなく、アプリケーションの追加、削除がシンプルになりそう。

ドメイン名の名前解決に関して

筆者の場合、MyDNSが運営しているダイナミックDNSを使ってサーバーを運用している。Aレコードのホスト名にワイルドカード*を指定しておくだけで、すべてのサブドメインへのアクセスが同一IPアドレスにリクエストされるようになる。

プロキシサーバーの実装

http-proxyをインストールする

> npm install http-proxy --save

ポイントは、通常通りプロキシ用のhttpサーバーを80番ポートで実行する。飛んできたリクエストのヘッダを参照し、どんな名称のホスト名でアクセスしにきているかをチェックする。そして該当するアプリケーションサーバーへhttp-proxyモジュールのwebメソッドでトランスレートするだけである。(下記はhttpだがhttpsもサポートしている)

また、意図しないそれ以外のホスト名でアクセスしてきた場合、エラーでもデフォルトのホストへ接続するかは、各自で決めれば良い。

ちなみにproxy.ws()の処理は、www.hoge.com:3001websocketを使っている想定の処理であり、使ってなければ不要になる。

'use strict';
var port = 80;
var http = require('http');
var httpProxy = require('http-proxy');

var proxy = httpProxy.createProxyServer();

// http or https
var proxyServer = http.createServer(function(req, res){
    
    console.log( "[host] " + req.headers.host );
    var opt = {target:""};
    
    if( req.headers.host == "www.hoge.com" ){
        opt.target = "http://www.hoge.com:3000";
    }else if(req.headers.host == "test.hoge.com" ){
        opt.target = "http://www.hoge.com:3001";
    }else{
        res.write( "<p>Unknown host server</p>" );
        res.write( "<p>Host name : "+req.headers.host+"</p>" );
        res.end();
        return;
    }
    
    // translate
    proxy.web( req, res, opt, function(err){
            console.log(err);
    });
    
})

proxyServer.listen( port, function(){
    console.log("-- start reverse proxy server . listen "+ port + " port --");
});

// web socket
proxyServer.on( 'upgrade', function( req, socket, head ){
    proxy.ws( req, socket, head,{target:"http://www.hoge.com:3001"} );
});

下記はリクエストが飛んできたら、index.jadeから作成したhtmlを返すだけの3000番ポートで稼動するシンプルなhttpサーバになる

'use strict';

// -------------------------
// initialize
// -------------------------
var http = require( 'http' );
var path = require( 'path' );
var logger = require('morgan');
var express = require( 'express' );
var port = 3000;

var app = express();
app.use( logger( 'dev' ) );

app.use( express.static( path.join( __dirname, '/public' ) ) );

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// get
app.get( '/', function( request, response ) {
    response.render('index.jade');
});

// launch server
var server = http.createServer( app );
server.listen( port, function(){
    console.log("-- start http server . listen "+ port + " port --");
});

下記は3001番ポートで稼動するsocket.ioを使ったチャットアプリになる。


'use strict';

// -------------------------
// initialize
// -------------------------
var http = require( 'http' );
var path = require( 'path' );
var logger = require('morgan');// ログ出力
var express = require( 'express' );// Express
var port = 3001;

var app = express();
app.use( logger( 'dev' ) );

app.use( express.static( path.join( __dirname, '/public' ) ) );

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// get
app.get( '/', function( request, response ) {
    response.render('socket.jade');
});

// launch server
var server = http.createServer( app );
server.listen( port, function(){
    console.log("-- start socket io server . listen "+ port + " port --");
});

// socket.io
var socketio = require('socket.io');
var io = socketio.listen(server);

// connect socket server
io.sockets.on("connection", function(socket){
    // "sock_message"イベントを監視する
    socket.on("sock_message", function(data){
        // broadcast mesage
        socket.broadcast.emit("sock_message", data);
        // recieve message
        socket.emit("sock_message", data);
    });
});

socket.ioを使ったアプリの場合、クライアント側のjavascriptの実装も必要になる。従来どおりの実装だが、io.connect()メソッドで指定する接続先もサーバーに合わせる。

(function($){
    // socket.io
    var sock = io.connect("http://www.hoge.com:3001");
    sock.connect();
    
    // on message
    sock.on("sock_message",function(message){
        var date = new Date();
        date.getFullYear()
        $('#message').prepend(
            $('<li>').append(
                date.getFullYear()+"/"+(date.getMonth()+1)+"/"+date.getDate()+" "
                +date.getHours()+":"+date.getMinutes()+":"+date.getSeconds()+" "
                +message.value
            )  
        );
    });
        
    // send
    $('#btn_send').on('click',function(){
        var text = $('#in_text').val();
        $('#in_text').val("");
        // send message
        sock.emit("sock_message", {value:text});
    });
    
    // clear
    $('#btn_clear').on('click',function(){
        $('#message').empty();
    });
    
}(jQuery));