モバイルアプリ開発で面倒なサーバAPIバックエンドの切り替えを動的に行う


これはリクルートテクノロジーズアドベントカレンダー18日目の記事です．


![](https://dl.dropboxusercontent.com/u/10177896/Logical-switching-proxy.png)

*[14日目][day14]に出た図にそっくりですね*

APIエンドポイントを固定したままバックエンドを切り替える
----------------------------------------------

モバイルアプリの開発時，サーバ側APIの開発も同時並行で実施している場合，皆さんは開発中のアプリと開発中のサーバの対応をどのようにとっているでしょうか？

- 開発のたびにアプリ内のサーバAPIのエンドポイント設定値を変更してアプリをビルドし直す
- 開発版のアプリはconfig配布サーバから設定を取得し，設定値に記載されたエンドポイントと通信する

など，上記のような方法が考えられますが，クライアントアプリ側に開発デバッグ用の機能を作りこんでいくのは面倒で，なるべく少なくしたいのではないかと思います．そこで今回は，モバイルアプリで設定するエンドポイントURLは固定にしたまま，サーバ側でモバイルアプリからのリクエストに応じてバックエンドを動的に切り替える仕組みを紹介いたします．[14日目][day14]と同じく，今回もnginx/ngx_mrubyを利用します．


開発用バックエンド切り替えの仕組み概要
--------------------------------

構成は[14日目][day14]とほとんど同じです．構成要素は次のようになっていて，

- Nginx/ngx_mruby: バックエンドを動的に切り替えるproxy
- Redis: ngx_mrubyによる動的切り替えのためのロジックを格納
- Sinatra app: Redisに切り替えロジックを埋め込むための管理用webアプリ

開発者がproxyに切り替え用のロジックを書き込めるようになっています．切り替えロジックには，具体的にはモバイルアプリからのリクエストに含まれる何らかのメタデータをもとに任意の開発用バックエンドにルーティングするような仕組みを想定しています．

[conf/nginx.conf]:

nginxの設定ファイルで，`mruby_set`ディレクティブを使って`proxy_pass`に変数を渡します．

```
location / {
	mruby_set $backend /usr/local/nginx/hook/proxy.rb;
	proxy_pass http://$backend;
}
```

[hook/proxy.rb]: 

redisから取得した切り替えロジックを元に，参照先のバックエンドを決定します

```
eval("def backend_logic r;#{target_score};end")
r.var.set "backend", backend_logic(r)
```


今回はデモとして，モバイルアプリがAPIリクエストに付与するリクエストヘッダのID値をもとにバックエンドを切り替える例を紹介します．


リクエストヘッダのID値で参照先バックエンドAPIを切り替える
------------------------------------------------

モバイルアプリからのAPIリクエストには，HTTPリクエストヘッダに`X-Switching-Id: <ID値>`を含ませるようにし，proxyはそのヘッダ値をもとにバックエンドを切り替えたいとします．開発中のapiサーバは3つ，内部でポート8080,9000,9001でlistenしているものとして，開発者はそれらのサーバ参照を切り替えることでapiレスポンスに含まれるパラメータの算出方法を変更しながらモバイルアプリのテストをしている，というユースケースです．

APIのレスポンス:

```
{"rarities"=>{"character_1"=>" 0.9", "character_2"=>" 0.7", "character_3"=>" 0.5"}}
```

[ainoya/logical-switching-proxy][repo]にデモを用意しています．
まず，構成一式が入ったDockerコンテナを動かしてみましょう．

	git clone https://github.com/ainoya/logical-switching-proxy.git && cd logical-switching-proxy
	docker build -t lsp .
	docker run -it --rm -p 49080:80 -p 49081:8080 -p 49082:3000 --name lsp lsp /data/scripts/start.sh

コンテナを立ち上げたら，まず切り替え用のロジックをproxyにデプロイします．切り替え用のロジックスクリプトを書いてみます．ロジックは返り値にバックエンドの指定として`http://<$backend>`の`$backend`部分を記述します．


[takt/spec/system/score.rb]:

```
# Score must returns the variable "$backend".
# $backend variable is used by parameter as http://$backend in nginx.conf
#

case r.headers_in["X-Switching-Id"]
when 'test-001'
  backend='0.0.0.0:8080'
when 'test-002'
  backend='0.0.0.0:9000'
when 'test-003'
  backend='0.0.0.0:9001'
else
  backend='0.0.0.0:8080'
end

return backend
```

管理用のsinatraアプリを通じて書き込み用ロジックをデプロイします．それと一緒にデプロイ後の動作確認を行うスクリプトを下記に用意したので，dockerコンテナの外からスクリプトを動かしてみます．

[takt/spec/system/write_score.rb]:

```
#!/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'

body = {'.*' =>  Base64.encode64(score)}.to_json

HTTParty.post(url, :body=>body)

['test-001', 'test-002', 'test-003'].each do |id|
  puts HTTParty.get('http://0.0.0.0:49080/api/rarities', :headers => {'X-Switching-Id' => id})
end
```

テストスクリプトを動かすと，リクエストヘッダのID`X-Switching-Id`ごとにバックエンドの参照が切り替わり，異なるパラメータでAPIレスポンスが返ることを確認できます．

```
➜  takt git:(master) bundle exec ruby ./spec/system/write_score.rb
{"rarities"=>{"character_1"=>" 0.1", "character_2"=>" 0.1", "character_3"=>" 0.1"}}
{"rarities"=>{"character_1"=>" 0.3", "character_2"=>" 0.2", "character_3"=>" 0.1"}}
{"rarities"=>{"character_1"=>" 0.9", "character_2"=>" 0.7", "character_3"=>" 0.5"}}
```

サーバ側での柔軟なバックエンド切り替え
----------------

デモの通り，モバイルアプリ側のapiエンドポイントurlを変更しないままサーバ側apiのバックエンドを切り替えできることを紹介しました．今回紹介した手法は，他にくらべ，モバイルアプリ側で設定する作業負担が少ないので，開発時のモバイル側の確認者がエンジニアでない場合とくに有効であると思います．

さらに，バックエンドの開発環境を[pool]で構成しておけば，コードリポジトリから任意のcommit-idの状態でバックエンドを自動構築できるので，より一層フレキシブルに開発/ステージング環境におけるバックエンド参照の仕組みを作ることができます．

[poolとの連携を行う切り替えロジックの例][integrate-sample]:


```
# Score must returns the variable "$backend".
# $backend variable is used by parameter as http://$backend in nginx.conf
#
# This routing logic is example to integrate with [pool](https://github.com/mookjp/pool) system.
#

preview_id = r.headers_in["X-Preview-Id"]

if preview_id
  backend = "#{preview_id}.appA.pool.dev"
else
  backend = '0.0.0.0:8080'
end

return backend
```

また，[mruby-geoip]を切り替えロジックに利用すればアクセス元のipアドレスや地理情報によってバックエンドの切り替え，アクセス制御が可能になると思います．今後もこの仕組みを応用して，モバイルアプリ開発をよりスムーズにできる環境の構築に貢献する方法を探っていきたいと考えています．

[mruby-geoip]: http://hb.matsumoto-r.jp/entry/2014/12/13/004226
[integrate-sample]: https://github.com/prevs-io/logical-switching-proxy/blob/master/takt/spec/system/pool.rb
[pool]: http://qiita.com/mookjp/items/ed5961589428238d610b
[hook/proxy.rb]: https://github.com/prevs-io/logical-switching-proxy/blob/master/hook/proxy.rb#L16-L18
[conf/nginx.conf]: https://github.com/prevs-io/logical-switching-proxy/blob/master/conf/nginx.conf#L20-L23
[takt/spec/system/write_score.rb]: https://github.com/prevs-io/logical-switching-proxy/blob/master/takt/spec/system/write_score.rb
[takt/spec/system/score.rb]: https://github.com/prevs-io/logical-switching-proxy/blob/master/takt/spec/system/score.rb
[repo]: https://github.com/prevs-io/logical-switching-proxy
[day14]: http://qiita.com/ainoya/items/85744c7baf4ef6999e04