Programmableなproxyで試すOrchestration Layer



リクルートテクノロジーズアドカレ14日目です！先日[mod_mruby/ngx_mrubyのアドカレで記事][mruby]を書かせていただきましたが，その勢いでngx_mrubyを使って実験的な試みをしてみました．

ProxyでバックエンドAPIを結合する
---


![overview]


今年の前半ぐらいに盛り上がった話題で周回遅れ感があり若干の気恥ずかしさがあるのですが，サーバサイドでAPI実装が既に完了してしまっている/改修不能な状態で，クライアントアプリが要求するAPIの仕様がドラスティックに変わるというずれがあるケースで，クライアントサイドのエンジニアがサーバサイド側でAPIをある程度整形できる緩衝地帯的な仕組みがあるは面白いなと，kawasimaさんの[darzana]や[netflixの記事][netflix]や同僚の話を聞いて考えておりました.SOA,SOAPやWDSLのことが思い浮かばれますが,最近覚えたproxyのリクエスト制御の仕組みを使って同じように[Oracle Service bus](http://www.oracle.com/technetwork/jp/middleware/service-bus/overview/index.html)のようなものが作れるんじゃないか思いつきでちょっと作ってみました．


構成の概要
---------

nginx/ngx_mrubyでレスポンス制御を行いますが，制御のロジックをRedisに格納して，それをngx_mrubyのハンドラで取得してevalするという大胆な仕組みで実現しています．redisへのロジック格納を外部から楽に行えるようにするため，管理用のwebアプリも付けています．

**ハンドラの制御より抜粋**

```
r = Nginx::Request.new
#redisから取得したロジックをeval
orchestrate = lambda{eval(score)}
r.content_type = "application/json"
Nginx.rputs orchestrate.call
```


デモ
----------

プロトタイプを[公開][karajan]していて，一応デモを動かすことができます．一式構成できるDockerfileを用意したので，コンテナを立ち上げればすぐ動きます．

    git clone https://github.com/prevs-io/Karajan.git && cd Karajan
    docker build -t karajan .
    docker run -it --rm -p 49080:80 -p 49081:8080 -p 49082:3000 --name karajan karajan /data/scripts/start.sh

デモは次のようなケースを想定して作っています．

  - クライアント側アプリはある画面の描画のために `http://0.0.0.0:8080/api/contents` and `http://0.0.0.0:8080/api/badge`の2つのAPIからデータを取る必要がある.
  - しかしアプリ側はできることなら1回のhttpリクエストだけで必要な情報を取得したいので，2つのリクエストを1つのリクエストにまとめたい．

このケースにしたがって，2つのリクエストをまとめるロジックをngx_mrubyのフックスクリプトとして書くと次のようになります．

    api_endp_1 ||= WebAPI.new "http://0.0.0.0:8080"
    api_endp_2 ||= WebAPI.new "http://0.0.0.0:8080"

    contents = JSON.parse(api_endp_1.get('/api/contents').body)
    badges = JSON.parse(api_endp_2.get('/api/badges').body)

    JSON.generate(contents.merge(badges))

上記のロジックをngx_mrubyから使うために，管理アプリ経由でRedisに格納します．POSTリクエストでリクエストを受けるURIとbase64エンコードしたロジックの組を登録します．

    #!/usr/bin/env ruby
    require 'httparty'
    require 'json'
    require 'base64'

    path = File.dirname(__FILE__)
    score=File.open(File.join(path, 'score.rb')).readlines.join('')
    url = 'http://0.0.0.0:49082/write_score'
    #{<context_path> => <client logic base64 encoded>}
    body = {'/' =>  Base64.encode64(score)}.to_json
    # write logic
    puts HTTParty.post(url, :body=>body)


ロジックの格納が完了したら，動作を確認してみます．`/orchestrate`以下にロジックが登録されます．

    ➜  karajan git:(master) ✗ curl  http://0.0.0.0:49080/api/badges
    {"badges":[{"badge1":"badge1"},{"badge2":"badge2"},{"badge3":"badge3"},{"badge4":"badge4"},{"badge5":"badge5"},{"badge6":"badge6"}]}

    ➜  karajan git:(master) ✗ curl  http://0.0.0.0:49080/api/contents
    {"contents":[{"item1":"description1"},{"item2":"description2"},{"item3":"description3"}]}

	#2つのリクエストが1つにマージされた
    ➜  karajan git:(master) ✗ curl  http://0.0.0.0:49080/orchestrate/
    {"contents":[{"item1":"description1"},{"item2":"description2"},{"item3":"description3"}],"badges":[{"badge1":"badge1"},{"badge2":"badge2"},{"badge3":"badge3"},{"badge4":"badge4"},{"badge5":"badge5"},{"badge6":"badge6"}]}

まとめ
--------

前段の話に加え，AWS Lambdaの発表などもあってああいう仕組みを作ってみたいなぁと思っていたんですが，Lambdaもそうであるようにロジックはクライアント側が触るものなのでやっぱりjsにした方がいいのかなと思いつつ，そうなるとngx_mrubyでなくnodeで実装したほうがよいな．．．と迷いましたが気づいたら作り終わってました．Orchestration Layerに絡む問題を把握しきれていないこともあり，proxyレイヤでorchestrationすることに対して現状作ったコンセプトでメリットを出すことができませんでしたが，関心のある分野なので今後もウォッチしていきたいと思っています．

[overview]: https://dl.dropboxusercontent.com/u/10177896/karajan-overview.png
[mruby]: http://qiita.com/ainoya/items/656aff7df61554f68f87

[karajan]: https://github.com/prevs-io/karajan
[is]: https://github.com/prevs-io/karajan/issues
[darzana]: https://github.com/kawasima/darzana
[ngx_mruby]: https://github.com/matsumoto-r/ngx_mruby/
[overview]: https://dl.dropboxusercontent.com/u/10177896/karajan-overview.png
[netflix]: http://techblog.netflix.com/2012/07/embracing-differences-inside-netflix.html
[fk]: http://help.github.com/forking/