CloudFrontのジオターゲティングをngx_mruby&Redisで判別する

2014.12.14

ども、大瀧です。
このブログ記事はmod_mruby ngx_mruby Advent Calendar 2014の14日目です。

昨日は@matsumotoryさんの地理情報を使ってmod_mrubyとngx_mrubyでプログラマブルにアクセス制御でした。今日も続けての地理情報ネタです。

以前、Amazon CloudFrontとEC2(Nginx)で作る国・地域対応Webサイト構築という記事でCloudFrontが付与する地理情報をNginxで判別する設定を紹介しました。

cf-nginx01

この構成だと、国とリダイレクト先を追加/変更するたびにnginxの設定をリロードしなければならず、またMulti-AZとして複数のインスタンスを立てるときに設定を同期させなければなりません。

そこで今回は、Nginxの動的構成を実現するngx_mrubyを用いてAmazon ElastiCache for Redisのデータを読み出す形で国とリダイレクト先を対応させる処理を実装してみます。

Nginxとngx_mrubyの設定

Nginxの構成のうち、mrubyを呼び出す部分を抜粋しました。

nginx.conf

:
        location / {
            mruby_set $redirect /usr/local/nginx/hook/redirect.rb;
            rewrite ^ $redirect permanent;
        }
          :

今回はリダイレクト先を動的に変更すれば良いので、rewriteディレクティブで変数をリダイレクト先とし、mrubyスクリプトの戻り値を変数にセットするmruby_setディレクティブを利用しました。呼び出すmrubyスクリプトredirect.rbは以下です。

hook/redirect.rb

#location / {
#    mruby_set $redirect /usr/local/nginx/hook/redirect.rb;
#    rewrite ^ $redirect permanent;
#}
default_redirect_url = 'http://example.com/jp/'

request = Nginx::Request.new
country = request.headers_in['Cloudfront-Viewer-Country']

if !(country.nil? || country.empty?)
  redis = Redis.new "<REDIS_HOSTNAME>", 6379
  redirect_url = redis.get country
  if !redirect_url.nil?
    redirect_url
  else
    default_redirect_url
  end
else
  default_redirect_url
end

7,8行目でHTTPヘッダからCloudFrontが付与するCloudfront-Viewer-Countryを参照し、10行目でセットされているかを判別、11,12行目でヘッダの値からリダイレクト先をRedisから読み出しています。ヘッダが空だった場合やRedisに登録がなかった場合のデフォルト値として、http://example.com/jp/をリダイレクト先として返すようにしています。

動作確認

ngx_mrubyを最も簡単に動作させる方法は、Dockerコンテナでの実行です。今回はAmazon LinuxにDockerをインストールして試してみました。

$ sudo yum install -y docker
  : 
$ sudo service docker start
$ sudo chkconfig docker on
$ sudo docker version
Client version: 1.3.3
Client API version: 1.15
Go version (client): go1.3.3
Git commit (client): c78088f/1.3.3
OS/Arch (client): linux/amd64
Server version: 1.3.3
Server API version: 1.15
Go version (server): go1.3.3
Git commit (server): c78088f/1.3.3
$

OKですね。

Redis(ElastiCache)の準備

続いて、mrubyから接続するRedisノードをAmazon ElastiCache for Redis 1ノードを作成しました。redis-cliで接続し、データを登録しておきます。redis-cliもDockerコンテナで実行してみました。

$ sudo docker run -it --rm --link redis:redis dockerfile/redis bash -c 'redis-cli -h counties.XXXXXX.0001.apne1.cache.amazonaws.com'
counties.XXXXXX.0001.apne1.cache.amazonaws.com:6379> mset JP http://example.com/jp/ CN http://example.com/cn/  TW http://example.com/tw/

OK
counties.XXXXXX.0001.apne1.cache.amazonaws.com:6379> keys *
1) "JP"
2) "CN"
3) "TW"
4) "ElastiCacheMasterReplicationTimestamp"
counties.XXXXXX.0001.apne1.cache.amazonaws.com:6379>

ngx_mrubyの実行

ngx_mrubyの作者@matsumotoryさん作のmatsumotory/ngx-mrubyというDockerイメージがDocker Hubで公開されているので、こちらをベースに以下のDockerfileでアレンジしてみました。

FROM matsumotory/ngx-mruby

EXPOSE 80
EXPOSE 443

ADD hook /usr/local/nginx/hook
ADD nginx.conf /usr/local/nginx/conf/nginx.conf

CMD ["/usr/local/nginx/sbin/nginx"]

docker buildコマンドでDockerイメージを作成します。

$ docker build -t takipone/ngx-mruby .
Sending build context to Docker daemon  5.12 kB
Sending build context to Docker daemon
Step 0 : FROM matsumotory/ngx-mruby
 ---> cd4b597cfdb3
Step 1 : EXPOSE 80
 ---> Using cache
 ---> cfef66f7d472
Step 2 : EXPOSE 443
 ---> Using cache
 ---> 56dc98e500a5
Step 3 : ADD hook /usr/local/nginx/hook
 ---> 674c0b5e921a
Removing intermediate container 9c887712b5b6
Step 4 : ADD nginx.conf /usr/local/nginx/conf/nginx.conf
 ---> 47b34f8aa06b
Removing intermediate container 8460eb48abc8
Step 5 : CMD /usr/local/nginx/sbin/nginx
 ---> Running in 4e594377e9d5
 ---> 5e48ce97d4ae
Removing intermediate container 4e594377e9d5
Successfully built 5e48ce97d4ae
$

では、作成したDockerイメージからコンテナを実行します。

$ sudo docker run -d -p 80:80 takipone/ngx-mruby
eb00d07bc7d76d6787baf839e3ca8d806776c68081b7ba5d28eac9824efdcba4
$

CloudFrontが付与するCloudFront-Viewer-Countryヘッダをcurlコマンドで再現し、動作を確認します。

$ curl -I --header "CloudFront-Viewer-Country:JP" http://localhost/
HTTP/1.1 301 Moved Permanently
Server: nginx/1.7.7
Date: Sun, 14 Dec 2014 13:55:29 GMT
Content-Type: text/html
Content-Length: 184
Connection: keep-alive
Location: http://example.com/jp/

$

ちゃんと動いてますね!

まとめ

ngx_mrubyでNginxの動的処理としてRedisに接続し、リダイレクト先を判定する処理を実装しました。CloudFrontではジオターゲティング以外にも様々な情報をHTTPヘッダとしてリクエストに付与してくるので、mrubyの工夫次第でいろいろできそうですね!

なお、リクエストごとに毎回Redisに接続しにいくというのが気になる方は、mruby-userdataを検討しても良いでしょう。ただ、今回のケースではCloudFrontがレスポンスをキャッシュするためNginx自体へのアクセスは頻繁には起きないと見なしても良いと思います。

明日(といってもあと1時間くらいですが(^^;)は、qtakamitsuさんの「mod_mruby を使った Web アプリ」です。お楽しみに!