話題の記事

Apacheによるバーチャルホスト構築レシピ mod_vhost_alias/mod_lua編

2013.12.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ども、大瀧です。
小規模なWebサイトの運用は、「お金はかけられないけど、ハードウェア障害などでサイトが落ちると困る」というように、運用コストが問題になりやすいITシステムではないでしょうか。そんなシステムへのソリューションとして、運用コストを削減できるクラウドの利用Webサーバー集約は案件として最近よく耳にします。

AWSでのWebサイト集約はAmazon S3のStatic Website Hostingが挙げられますが、BASIC認証に対応しておらず企業や会員向けサイトなどでは要件が満たせないことがあります。そこで今回は、EC2でApache httpd(以下Apache)を実行し、Webサーバーの集約としてバーチャルホストを構築するレシピをご紹介します。

Apacheのバーチャルホストのキホンと限界

Apacheのネームベースのバーチャルホストは、NameVirtualHostディレクティブ *1および<VirtualHost>ディレクティブで設定できますが、これらの情報はWebに溢れているため今回は敢えて書きません!これらの方法だと、バーチャルホストの数だけディレクティブを定義しなければならないため、運用面で以下の問題があります。

  • バーチャルホストの設定を追加する度に、Apacheの再起動が必要
  • バーチャルホストの設定がひとつでも間違っているとApache自体が起動しないため、全サイトが動かなくなる

サイト数が増えてくると、恐怖の運用が待っていることがイメージできると思います。そこで、再起動の必要がない方法を模索しました。

mod_vhost_aliasモジュール

mod_vhost_aliasモジュールは、Apache 2.2からサポートされている比較的新しいモジュールです。モジュールを有効にすると、VirtualDocumentRootディレクティブでバーチャルホストごとに異なるドキュメントルートを設定できます。ドキュメントルートのパスには変数を含めることができ、%0がドメイン名に置換されます。Amazon Linuxのhttpd24パッケージであれば、既定でmod_vhost_aliasモジュールが有効なので、例えば/etc/httpd/conf.d以下に設定ファイルを追加し構成できます。

$ sudo vim /etc/httpd/conf.d/vhost.conf
UseCanonicalName Off
VirtualDocumentRoot /var/www/vhosts/%0/html
$ sudo service httpd restart
Stopping httpd:                                            [  OK  ]
Starting httpd:                                            [  OK  ]
$

これにより、/var/www/vhost/<ドメイン名>/htmlがドメインごとのドキュメントルートとして動作します。適当なファイルを配置し、動作を確認してみます。

$ ls /var/www/vhosts/
example.com  example.jp  example.org
$ cat /var/www/vhosts/example.com/html/index.html
example.com
$

今回は手元のMBAで検証するため、/etc/hosts<EC2のPublic IP> example.comを追加し、ブラウザでアクセスしてみました。

apache01-3

ちゃんと表示されました!
これでコンテンツをドメインごとに分割できますが、mod_vhost_aliasモジュールではドキュメントルート以外にScriptAliasしか設定できないため、従来のバーチャルホスト機能と比較すると以下の問題点があります。

  • アクセスログ、エラーログがバーチャルホスト単位で分割できない。
  • バーチャルホストの対象となるドメインを絞り込めない。ドキュメントルートとなるディレクトリが存在しないドメインへのアクセスは、全て404 Not Foundになる。

あと一歩、かゆいところに手が届かないという印象です。ただ、設定自体はとても簡単ですし、Apacheの設定変更なしでドメインをどんどん増やしていけるので簡易バーチャルホストとしては強力な機能だと思います。

mod_luaモジュール

mod_luaは、mod_perlmod_phpと同様にApacheでLua言語を実行するモジュールですが、今回はCGIとしての実行ではなく、Apacheの動作をフック、カスタマイズする用途で利用します。

動作確認環境

  • OS : Fedora 20 64bit
  • Apache : 2.4.7(ソースからインストール、理由は後述)
  • Lua : 5.2.2(既定のRPMパッケージ)

こちらは、Vagrant + VirtualBoxで検証しました。Apache 2.4.7が動く環境であれば、AWS EC2でも同様でしょう。まずは、mod_luaを有効にしApacheをインストールします。

$ sudo yum install apr apr-util apr-devel apr-util-devel lua-devel
$ wget http://ftp.riken.jp/net/apache//httpd/httpd-2.4.7.tar.bz2
$ tar jxf httpd-2.4.7.tar.bz2
$ cd httpd-2.4.7
$ ./configure --with-lua
$ make
$ sudo make install

続いて、mod_luaでApacheの動作をフックするハンドラを定義します。ハンドラ定義はApacheの動作のうちカスタマイズしたいタイミングによって、以下のディレクティブをApacheの設定ファイルに記述します。

ディレクティブ名 説明
LuaQuickHandler 最初に評価される
LuaHookTranslateName URIからファイル名を解釈するタイミング。mod_aliasmod_rewriteモジュール相当
LuaHookMapToStorage 物理、キャッシュ、プロキシなどストレージを解釈するタイミング。プロキシやキャッシュ処理に相当
LuaHookAccessChecker クライアントがリソースにアクセスできるかどうかを評価するタイミング。ユーザー認証の前に評価されることに注意
LuaHookCheckUserID ユーザーIDを評価するタイミング
LuaHookAuthChecker or LuaAuthzProvider ユーザーIDなどでユーザー権限を認証するタイミング
LuaHookFixups コンテンツを処理する前の最後のタイミング
LuaHookLog[Apache 2.4.7以降で動作] コンテンツ処理後のログ処理のタイミング。

今回はバーチャルホストの動作としてURIの解釈とログ処理をフックするために、LuaHookTranslateNameディレクティブ、LuaHookLogディレクティブでそれぞれハンドラを登録します。FedoraやUbuntu、Amazon LinuxなどにバンドルされるApacheは2013/12/30現在 バージョン2.4.6がほとんどだったため、LuaHookLogディレクティブを使うためにソースからインストールしました。

バーチャルホストのディレクトリレイアウトは、以下にしてみました。

  • ドキュメントルート : /usr/local/apache2/vhosts/<ドメイン名>/html/
  • ログディレクトリ : /usr/local/apache2/vhosts/<ドメイン名>/logs/

/usr/local/apache2/conf/httpd.comf(末尾に以下を追加)

Include conf/extra/httpd-lua.conf

/usr/local/apache2/conf/extra/httpd-lua.conf

LoadModule lua_module modules/mod_lua.so

# LuaHoot* <Luaスクリプトファイル名> <ハンドラ関数名>
LuaHookTranslateName /usr/local/apache2/conf/lua/vhost.lua     set_docroot
LuaHookLog           /usr/local/apache2/conf/lua/vhost-log.lua log_handler

<Directory "/usr/local/apache2/vhosts/*/html">
    Require all granted
</Directory>

Luaスクリプト側の実装はとてもシンプルで、上記で定義したハンドラに対応するハンドラ関数をスクリプトに記述するだけです。実装のポイントは、ハンドラ関数の引数になるrequest_rec構造体とapache2パッケージです。

request_rec構造体

ハンドラ関数で受け取る引数に代入される構造体です。クライアントのリクエスト情報など、Apacheのデータを参照するために利用します。多くのコードサンプルでは変数rで受け取り、r.hostname(リクエストのホスト名)やr.uri(リクエストのURI)として参照します。詳細はmod_luaガイドにありますが、未実装でエラーになるのものもあることに注意します。

apache2パッケージ

実行しているApacheの情報が取れるパッケージです。現状はバージョン番号と、Apacheの動作をコントロールする以下の定数(これ重要)を参照できます。定数は、主に以下をハンドラ関数のreturn文に指定して使用します。

  • apache2.OK : リクエスト処理の終了を示す。luaスクリプトで直接レスポンスを生成する場合に指定する
  • apache2.DONE : LuaHookLogディレクティブでログ処理をフックする際に、OKの代わりに指定する
  • apache2.DECLINED : Apacheの既定の動作を示す。"luaスクリプトではApacheの動作設定などを変更し、レスポンスはApacheのコンテンツを返す"というような場合に指定する

apache2パッケージの詳細はApacheドキュメントで参照できますが、そのページにあるサンプルスクリプトBuggyで動作しないものもある(バーチャルホストのサンプルはエラーになりました)ので、注意してください。

バーチャルホストの実装例

ではコードを示してみます。まずは、ドキュメントルートを変更するvhost.luaから。

/usr/local/apache2/conf/lua/vhost.lua

local vhosts = {
    "example.com",
    "example.jp"
}

-- 対応するハンドラ関数set_docrootの定義、request_rec構造体を変数rで受け取る
function set_docroot(r)
    -- 配列vhostsのループ
    for key, domain in ipairs(vhosts) do
        -- r.hostnameにドメインが含まれる場合
        if r.strcmp_match(r.hostname, domain) then
            local docroot = "/usr/local/apache2/vhosts/" .. domain .. "/html/"
            -- ドキュメントルートの変更
            r:set_document_root(docroot)
        end
    end
    -- コンテンツはApacheで対応
    return apache2.DECLINED
end

続いて、ログをフックするvhost-log.luaを。

/usr/local/apache2/conf/lua/vhost-log.lua

local vhosts = {
    "example.com",
    "example.jp"
}

-- 対応するハンドラ関数log_handlerの定義
function log_handler(r)
    for key, domain in ipairs(vhosts) do
        -- バーチャルホストのドメインの場合、専用のアクセスログに出力
        if r.strcmp_match(r.hostname, domain) then
            local f = io.open("/usr/local/apache2/vhosts/" .. domain .. "/logs/access_log", "a")
            if not r.user then
                r.user = '-'
            end
            if not r.headers_in['Referer'] then
                r.headers_in['Referer'] = '-'
            end
            -- Apache Combinedに似せたフォーマットで出力
            f:write(r.useragent_ip .. " - " .. r.user .. " [" .. os.date("%d/%b/%Y:%H:%M:%S %z") .. "] \"" .. r.the_request .. "\" " .. r.status .. " - \"" .. r.headers_in['Referer'] .. "\" \"" .. r.headers_in['User-Agent'] .. "\"\n")
            f:close()
            -- アクセスログを出力したので、既定のアクセスログは出力しない
            return apache2.DONE
        end
    end
    -- 他のドメインは、既定のアクセスログに出力
    return apache2.DECLINED
end

アクセスログは、request_rec構造体からリクエストの情報を取り出してApacheのcombinedログに整形しています。レスポンスのバイトサイズは見当たらなかったので未実装としました。-を仮の値として入れています。それからエラーログも見つからなかったため、実装できていません。

$ cat /usr/local/apache2/vhosts/example.com/logs/access_log
10.0.X.X - - [30/Dec/2013:14:29:52 +0900] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36"
10.0.X.X - - [30/Dec/2013:14:29:52 +0900] "GET /favicon.ico HTTP/1.1" 404 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36"
$

きっちりアクセスログとして書き出されていますね!

まとめ

mod_luaモジュールはステータスがまだexperimentalで、ドキュメントに記載があっても動作しない機能があったり発展途上な感もありますが、Apacheの動作をカスタマイズする手段として期待できる印象が持てました。今後もウォッチしていきたいと思います。

また、Apacheの拡張として最近ホットなmod_mrubyも、そのうち検証してみたいです。そういえば、Nginxだと標準機能で今回のmod_lua相当のバーチャルホストが構成できるようなので、そちらも試してみないと。

脚注

  1. Apacheバージョン2.4以降では定義不要になり、<VirtualHost>ディレクティブのみで動作します。