読者です 読者をやめる 読者になる 読者になる

人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

H2Oとmrubyを使ってIPアドレスベースでアクセス制御しつつリバースプロキシとして動かしてWebサイトをHTTP/2化しよう

Webサーバ 運用 プログラミング

h2oをリバースプロキシで使えば、http://でのアクセスはHTTP/1.xでコンテンツを返しつつ、https://からのアクセスについてはHTTP/2+TLSに対応したブラウザであればHTTP/2、対応していなければHTTP/1.xでコンテンツを返す事ができます。

また、h2oがmrubyによる機能拡張をサポートし、かつ、先日アクセス元IPアドレスをmruby内で参照することができるようになったので、IPアドレスを用いたアクセス制御もh2o+mruby側で簡単に制御できるようになりました。

今日はその設定例を簡単に紹介したいと思います。

H2Oの設定

h2oの設定は大体以下のように書くと良いでしょう。このサンプルでは、本来muninが動いていたApacheを127.0.0.1:8000でListenするようにし、フロントにh2oを配置する状況を想定しています。

user: daemon
pid-file: /var/run/h2o.pid
access-log: /var/log/h2o/access_log
error-log: /var/log/h2o/error_log
hosts:
  "munin.example.jp":
    listen: 80
    listen:
      port: 443
      ssl:
        certificate-file: /etc/h2o/tls/server.crt
        key-file: /etc/h2o/tls/server.key
    paths:
      /:
        proxy.timeout.io: 300000
        proxy.reverse.url: http://127.0.0.1:8000/
        mruby.handler_path: /etc/h2o/mruby/h2o.rb
      /munin/:
        file.dir: /var/www/html/munin
        mruby.handler_path: /etc/h2o/mruby/h2o.rb

こんな感じで、staticなファイルが配置される/munin/はh2oが直接フロントで返し、動的コンテンツはバックエンドのApacheが実行するので、そのままリバースプロキシするようにしています。

また、それぞれのリクエストにおいてmruby.handler_pathにより、mrubyスクリプトh2o.rbをフックするようにしています。

mrubyでIPアドレスベースのアクセス制御を実装

続いて、h2o.confで呼ばれいてる/etc/h2o/mruby/h2o.rbを実装しましょう。この中で、IPアドレスベースのアクセス制御を実装します。

以下のようになります。

allow_seg_list = %w{

192.168.1
192.168.2
192.168.3
192.168.11
192.168.12
192.168.13

}

remote_seg = H2O::Connection.new.remote_ip.gsub(/\.\d{1,3}$/, "")

if allow_seg_list.include? remote_seg
  H2O.return H2O::DECLINED
else
  H2O.return 403, "Forbidden", "Forbidden"
end

例えば、allow_seg_listに書かれているセグメントからのみアクセスを許可する場合は上記のようにかけます。このmrubyでは正規表現を使うために、以下のようなbuild_config.rbを用いてmrubyをビルドしています。

MRuby::Build.new do |conf|

  toolchain :gcc

  conf.gembox 'full-core'

  conf.gem :github => 'iij/mruby-io'
  conf.gem :github => 'iij/mruby-env'
  conf.gem :github => 'iij/mruby-dir'
  conf.gem :github => 'iij/mruby-digest'
  conf.gem :github => 'iij/mruby-process'
  conf.gem :github => 'iij/mruby-pack'
  conf.gem :github => 'mattn/mruby-json'
  conf.gem :github => 'mattn/mruby-onig-regexp'
  conf.gem :github => 'matsumoto-r/mruby-sleep'
  conf.gem :github => 'matsumoto-r/mruby-userdata'
  conf.gem :github => 'matsumoto-r/mruby-uname'
  conf.gem :github => 'matsumoto-r/mruby-cache'
  conf.gem :github => 'matsumoto-r/mruby-mutex'

end

これをクローンしてきたmrubyリポジトリ内で以下のようにビルド環境にインストールします。

cd mruby && make && sudo cp build/host/lib/libmruby.a build/host/lib/libmruby_core.a /usr/lib/. && sudo cp -r include/* /usr/include/.

現在のh2oとmrubyは、ビルドを連携して処理することができないため、上記のように様々なmrbgemをリンクした場合は、h2oのビルドに追加で必要なフラグをh2oのCMakeLists.txtに追記が必要になります。

今回は、mrubyリポジトリ内のbuild/host/lib/libmruby.flags.makを参考に、以下のようにCMakeLists.txtにフラグを追記しました。

(snip)
SET(CMAKE_C_FLAGS "-O2 -g -Wall -Wno-unused-function ${CMAKE_C_FLAGS} -DH2O_ROOT=\"\\\"${CMAKE_INSTALL_PREFIX}\\\"\" -L/home/vagrant/mruby/build/host/mrbgems/mruby-onig-regexp/onig-5.9.5/.libs")
(snip)
    LIST(INSERT EXTRA_LIBRARIES 0 ${MRUBY_LIBRARIES} "m" "onig")
(snip)

これを書いてから、以下のようにh2oをビルドすると、ちゃんとビルドできるはずです。

cmake28 -DWITH_MRUBY=ON .
make h2o

H2Oの起動スクリプトやその他設定

いざh2oをサーバでちゃんと使っていこうとすると、以下のようなファイルが必要になります。

  • 起動スクリプト
  • logrotateの設定
  • ディレクトリ構成

これらをざっと用意したので参考までに紹介します。

起動スクリプト

h2oはサーバユーティリティが同封されており、デーモンモードで動かす時はその中のstart_serverを使います。それらはh2oリポジトリの中のshare/h2oの中に入っています。

これらを使う事で、gracefulの処理も簡単に行う事ができるので、それを以下のように起動スクリプトとしてwrapしました。

#!/bin/bash
#chkconfig: 2345 85 15
#descpriction: h2o Web Server

# source function library
. /etc/rc.d/init.d/functions

RETVAL=0
SERVICE_NAME=`basename $0`

start() {
        echo -n $"Starting $SERVICE_NAME: "
        /usr/sbin/h2o -m daemon -c /etc/h2o/conf/h2o.conf
        RETVAL=$?
        if [ $RETVAL == 0 ]; then
          success
        else
          failure
        fi
        echo
}

stop() {
        echo -n $"Stopping $SERVICE_NAME: "
        kill -TERM `cat /var/run/h2o.pid`
        RETVAL=$?
        if [ $RETVAL == 0 ]; then
          success
        else
          failure
        fi
        echo
}

reload() {
        echo -n $"Graceful $SERVICE_NAME: "
        kill -HUP `cat /var/run/h2o.pid`
        RETVAL=$?
        if [ $RETVAL == 0 ]; then
          success
        else
          failure
        fi
        echo
}

case "$1" in
  start)
        start
        ;;
  stop)
        stop
        ;;
  reload|graceful)
        reload
        ;;
  restart)
        stop
        start
        ;;
  *)
        echo $"Usage: $0 {start|stop}"
        exit 1
esac

exit $RETVAL

これを/etc/rc.d/init.d以下において、

sudo chkconfig h2o on

などとしておきましょう。

logrotateの設定

上記の様に起動スクリプトを作り、かつ、h2oの設定通り/var/log/h2o/以下にログを保存するようにしているので、以下のようなlogrotateの設定を書きます。

/var/log/h2o/*log {
    missingok
    notifempty
    sharedscripts
    delaycompress
    postrotate
        /sbin/service h2o graceful > /dev/null 2>/dev/null || true
    endscript
}

これでうまくいけばログローテート時にgracefulが走るようになります。

ディレクトリ構成

そして、全体のディレクトリ構成を大体以下のようなシェルで用意します。

# h2oのバイナリと同封されているサーバユーティリティ群をコピー
cp -v h2o /usr/sbin/.
cp -rv share/h2o /usr/local/share/.

# h2oの設定
mkdir -pv /etc/h2o/conf
cp -v h2o.conf  /etc/h2o/conf/.

# HTTP/2をchorome等の有名ドコロブラウザから使おうとするとTLS必須なのでオレオレを準備
mkdir -pv /etc/h2o/tls
cp -v server.crt server.key /etc/h2o/tls/.

# IPベースのアクセス制御をmrubyで実装
mkdir -pv /etc/h2o/mruby
cp -v h2o.rb /etc/h2o/mruby/h2o.rb

# ログディレクトリを掘っとく
mkdir -pv /var/log/h2o/

# h2oの起動スクリプトをコピーしてパーミッションも適切なものに
cp -pv rc_script/h2o /etc/rc.d/init.d/.
chown root.root /etc/rc.d/init.d/h2o
chmod 755 /etc/rc.d/init.d/h2o

# ログローテートの配置
cp -v logrotate/h2o /etc/logrotate.d/.

イメージとしては、大体こんな感じで上記で作ったファイルを配置していきます。

その他気をつける事

HTTP/1側でアクセスする分には特に問題ありませんが、HTTP/2でアクセスするとバックエンドのApacheが動的に処理するCGIなどが、ブラウザのセッション数に依存しない数の同時接続数が発生します。

そのため、画像生成するCGIがApacheで動いていた場合に、その画像数が一つのページに数十個あった場合は、HTTP/1.xアクセスだと大体同時には6セッション分ぐらい、つまり6並列ぐらいでしかCGIが実行されていなかったのに対し、HTTP/2だと一気に数十個のCGIが同時多発的に処理されることになります。

つまり、バックエンドのチューニングを見なおして、最低でもCGIを同時に並行で処理してもサーバが問題ない程度のMaxClientにしておくと、HTTP/2アクセスが沢山発生してもサーバがフルにリソースを使って処理することができるようになり、より快適にHTTP/2を体験できるかと思います。h2oに同封のfastcgi機能などを使えばこの限りではありませんが、今回は既存のApacheを簡単に置き換える事を想定しているので、その点についての言及は省略します。

CGIの処理の重さにもよりますが、muninのように外部に公開していないものの、一つのCGI実行がCPUコア1つを占有しがちな処理の場合には、サーバのコア数の2〜3倍程度のMaxClinetsぐらいにしておくとそれなりにバランスよく処理できていました。CGIの処理が重たければ重たい程、コア数に近い数でも良いぐらいでしょうか。

その結果、

というような反応を頂いております。

まとめ

ということで、h2oとmrubyを使って既存のWebサイトをHTTP/2化しながらもmrubyで柔軟にアクセス制御する方法を紹介しました。また、その中でバックエンドのチューニングも、HTTP/2を考慮した場合には再設計し直す必要があるかもしれない、という点についても述べました。

是非このエントリを参考に、自身のブログやWebサイトをHTTP/2化してみてはどうでしょうか!