Infratasterでリバースプロキシのテストをする

インフラ部の荒井(@ryot_a_rai)です。この記事ではインフラの振る舞いテストのツールであるInfratasterを使ってリバースプロキシの設定のテストをしてみたいと思います。

Infratasterとは


Infratasterはインフラの振る舞いをテストするフレームワークで、RSpecのテストヘルパとして機能します。例えば、

  • 特定のヘッダ付きのHTTPリクエストを送信した時にあるレスポンスヘッダが返ってくることをテストする
  • Capybaraを使って実際のWebブラウザ上での挙動をテストする
  • MySQLのSHOW VARIABLESの結果をテストする

といったことが可能になります。

細かい概要についてはこちらのスライドREADMEをご覧ください。

Serverspecとの違い

インフラのテストといえばServerspecが有名かと思いますが、InfratasterはServerspecとはテストする領域が異なっています。Serverspecはサーバの内部のミドルウェアやファイルをテストしますが、Infratasterはサーバの外部からテストをします。つまり、Infratasterは中で動いているミドルウェアが何かは関係なく、外から見てどういった振る舞いをするかを検証するためのものです。

Infratasterを使ってnginxの設定のテストをする

上で紹介したInfratasterと仮想マシン(Vagrant, VirtualBox)を使ってnginxの設定が意図したとおり行われているかをテストしてみたいと思います。

本記事のコードは https://github.com/ryotarai/proxy-configtest-sample にあります。

仮想マシンを用意する

Vagrantをつかってテスト用の仮想マシンを用意します。Vagrantで仮想マシンを起動、セットアップすることで再現性のあるテスト環境を用意することが可能になります。

VirtualBox、Vagrantをインストール後、プロキシ用、バックエンド用に1つずつVagrant VMを立てますが、今回はこちらのVagrantfileを使います。このVagrantfileでは、proxy VMに/etc/hostsを置いて、バックエンドへのアクセスをapp VMに向けてテストしやすくしています。

Vagrantfileを置いたあと、vagrant upを実行しておきます。

f:id:ryotarai:20141118112005p:plain

Infratasterをインストールする

Infrataster, RSpecをGemとしてインストールします。

# Gemfile
source 'https://rubygems.org'
gem 'infrataster'
gem 'rspec-json_matcher'

rspecコマンドを使って、テストに必要なファイルのひな形を生成します。

$ bundle exec rspec --init

Infrataster(とrspec-json_matcher)を使うために、生成されたspec/spec_helper.rbの先頭に以下を追記します。

require 'rspec/json_matcher'
require 'infrataster/rspec'

RSpec.configuration.include RSpec::JsonMatcher
Infrataster::Server.define(
  :proxy,           # name
  '192.168.0.0/16', # proxy VM's IP address
  vagrant: true     # for vagrant VM
)

200が返ってくることをテストする

準備ができたので、プロキシに対するテストを書いてみます。手始めに、http://foo.example.comにアクセスした時に200が返ってくることをテストします。

# spec/foo_spec.rb
require 'spec_helper'

describe server(:proxy) do
  describe http('http://foo.example.com') do
    it 'returns 200' do
      expect(response.status).to eq(200)
    end
  end
end

まだnginxの設定を書いていないので、テストは失敗します。

$ bundle exec rspec
  1) server 'proxy' http 'http://foo.example.com' with {:params=>{}, :method=>:get, :headers=>{}} returns 200
     Failure/Error: expect(response.status).to eq(200)
     Faraday::ConnectionFailed:
       Connection refused - connect(2) for "192.168.33.10" port 80

テスト対象のnginxの設定を書きます。

# nginx/foo.conf
server {
  listen 80;
  server_name foo.example.com;
  location / {
    proxy_pass http://app-001/;
  }
}

このままだとproxy VM内でapp-001が名前解決できないので、/etc/hostsでapp VMに向けます。

# Vagrantfile
-hosts = %w!!
+hosts = %w!app-001!

再度テストを走らせると、通ることが確認できると思います。

$ vagrant provision
$ bundle exec rspec
server 'proxy'
  http 'http://foo.example.com' with {:params=>{}, :method=>:get, :headers=>{}}
    returns 200

Finished in 0.01166 seconds
1 example, 0 failures

意図したホストにプロキシされているかをテストする

つぎに、意図したホストにプロキシされているかをテストしてみます。app VMのnginxでX-MOCK-HOSTヘッダをつけるようにしたので、これを使います。モック用のアプリはリクエストヘッダをそのままJSONにして返すようになっているので、レスポンスをテストすることでプロキシ→バックエンドのリクエストヘッダをテストすることができます。

--- a/spec/foo_spec.rb
+++ b/spec/foo_spec.rb
@@ -5,6 +5,12 @@ describe server(:proxy) do
     it 'returns 200' do
       expect(response.status).to eq(200)
     end
+
+    it 'proxies to app-001' do
+      expect(response.body).to be_json_including({
+        'X_MOCK_HOST' => 'app-001',
+      })
+    end
   end
 end
$ bundle exec rspec
server 'proxy'
  http 'http://foo.example.com' with {:params=>{}, :method=>:get, :headers=>{}}
    returns 200
    proxies to app-001

Finished in 0.01675 seconds
2 examples, 0 failures

無事、プロキシ先のホストの確認ができました。

レスポンスヘッダをテストする

プロキシでCache-Controlヘッダを返すようにしてみます。先にテストを書いて、失敗させてから設定を書きます。

--- a/spec/foo_spec.rb
+++ b/spec/foo_spec.rb
@@ -12,5 +12,11 @@ describe server(:proxy) do
       })
     end
   end
+
+  describe http('http://foo.example.com/isucon') do
+    it 'returns Cache-Control header' do
+      expect(response.headers['Cache-Control']).to eq('max-age=86400')
+    end
+  end
 end
$ bundle exec rspec
  1) server 'proxy' http 'http://foo.example.com/isucon' with {:params=>{}, :method=>:get, :headers=>{}} returns Cache-Control header
     Failure/Error: expect(response.headers['Cache-Control']).to eq('max-age=86400')

       expected: "max-age=86400"
            got: nil

       (compared using ==)
     # ./spec/foo_spec.rb:18:in `block (3 levels) in <top (required)>'

nginxの設定を追加します。

--- a/nginx/foo.conf
+++ b/nginx/foo.conf
@@ -1,6 +1,12 @@
 server {
   listen 80;
   server_name foo.example.com;
+
+  location /isucon {
+    expires 24h;
+    proxy_pass http://app-001/;
+  }
+
   location / {
     proxy_pass http://app-001/;
   }

再度テストを実行すると、適切に設定されていることが確認できます。

$ vagrant provision
$ bundle exec rspec
server 'proxy'
  http 'http://foo.example.com' with {:params=>{}, :method=>:get, :headers=>{}}
    returns 200
    proxies to app-001
  http 'http://foo.example.com/isucon' with {:params=>{}, :method=>:get, :headers=>{}}
    returns Cache-Control header

まとめ

Infratasterを使ってリバースプロキシの設定をテストする方法を紹介しました。本記事ではnginxを例に出しましたが、Apacheやその他のソフトウェアでも同様にテストすることができます。

プロキシの設定は徐々に複雑化していき、挙動が見えなくなっていきがちです。テストを書いておけば、意図しない挙動の変更を防げたり、テスト自体をドキュメントとして使うこともできます。この記事がInfratasterを使ったテストの参考になれば幸いです。

iOSアプリ間連携の実装に x-callback-url を使う

はじめに

モバイルファースト室の @slightair です。 クックパッドが提供しているiOSアプリには、連携して機能するものがあります。

買い物リストアプリを例に挙げると、クックパッドアプリのレシピ画面からレシピに使われている材料を買い物リストアプリに登録することができます。

f:id:Slightair:20141113101333p:plain

この機能は、x-callback-url という仕様に沿って実装しています。 x-callback-url は別のアプリの呼び出しや情報の受け渡しに使うカスタムURLスキームの形式を定義するものです。 この仕様に沿って実装することで、他のアプリから呼び出せる処理や必要なパラメータをきれいにまとめることができます。

この記事では x-callback-url を用いたアプリ間連携の実装について説明します。

カスタムURLスキーム

iOSアプリで他のアプリに遷移しつつなにかしらの情報を渡すにはカスタムURLスキームを使う事になると思います。 遷移先のアプリでは叩かれたURLを受け取れるので、URLをパースして渡ってきた値を解釈したり、次にどのような処理をするか決めることができます。

どのような形式のURLを受け付けるかはアプリそれぞれで定義することになります。 URLで表現できるものであればなんでもよいのですが、ここで好き勝手に形式を決めてしまうと、よほどうまくやらない限りいつか破綻してしまうでしょう。

x-callback-url

クックパッドでは、アプリ間連携を x-callback-url という仕様に沿って実装しています。 この仕様では、パラメータをURLに含める形式を定めています。 x-callback-url では、他のアプリに渡す値だけではなく、他のアプリに委譲した処理の結果を受け取るためのパラメータも決められています。

具体的には以下の様な形式です。

[scheme]://[host]/[action]?[x-callback parameters]&[action parameters]

action に遷移先のアプリに委譲したい処理名を、action parameters には action に必要なパラメータを指定します。 x-callback parameters には、遷移先のアプリで表示するために使う遷移元のアプリ名(x-source)、成功時に遷移元のアプリに戻るためのURL(x-success) などを指定します。 詳しくは x-callback-url の仕様を読んでみてください。

x-callback-url は Google Chrome でも使われているようです。 https://developer.chrome.com/multidevice/ios/links#using-the-x-callback-url-registration-scheme

このページに書かれている仕様に沿ってアプリを実装すると、Webページを開く際にChromeを使い、ユーザがページの閲覧を終えたら元のアプリに戻ってくるような動きをさせることができます。

実装

x-callback-url の仕様に沿ったアプリ間連携を簡単に実装するためのライブラリ InterAppCommunication があります。 クックパッドではこのライブラリをラップし、他のクックパッドのアプリの機能を簡単に呼び出せるようにして社内共通ライブラリに組み込んでいます。

呼び出し側の実装例

InterAppCommunication を使うと、他のアプリの機能を呼び出すときには以下のように書けます。

    IACClient *client = [IACClient clientWithURLScheme:@"url-scheme"];

    [client performAction:@"action"
               parameters:@{
                             ...
                            }
                onSuccess:^(NSDictionary *params) {
                    ...
                }
                onFailure:^(NSError *error){
                    ...
                }
     ];

受け側の実装例

application:didFinishLaunchingWithOptions: でcallbackURLSchemeとアクションに対応する処理を書くデリゲートをセットします。

    [IACManager sharedManager].callbackURLScheme = @"url-scheme";
    [IACManager sharedManager].delegate = <IACDelegateに準拠したクラスのインスタンス>;

application:openURL:sourceApplication:annotation: で IACManager のインスタンスメソッド handleOpenURL: を使い、呼び出された url をわたします。

    if ([[url scheme] isEqualToString:@"url-scheme"]) {
        return [[IACManager sharedManager] handleOpenURL:url];
    }

上記2つのコードは、呼び出し側で処理が終わった後に元のアプリに戻ってくる場合にも必要です。

IACManager の delegate に設定するインスタンスのクラスで supportsIACAction:performIACAction:parameters:onSuccess:onFailure: を定義します。

- (BOOL)supportsIACAction:(NSString *)action
{
    NSArray *supportedActions = @[@"action1", @"action2", ...];
    return [supportedActions containsObject:action];
}

- (void)performIACAction:(NSString *)action
              parameters:(NSDictionary *)parameters
               onSuccess:(IACSuccessBlock)success
               onFailure:(IACFailureBlock)failure
{
    if ([action isEqualToString:@"action1"]){
        // action1 の処理
        // 結果に応じて success/failure block を呼ぶ
    }
    ...
}

ライブラリが x-callback-url の仕様に沿ったURLを解析して、パラメータやsuccess/failureブロックに展開してくれます。

おわりに

クックパッドで公開しているアプリのアプリ間連携の実装の話をしました。 別のアプリに遷移して、再びアプリに戻るような動きがなかったとしても、x-callback-url のような仕様に合わせたほうがきれいに実装できます。 自前でURLクエリをパースする処理を書いたり、独自の形式を考えるより楽だと思います。

スムーズなアプリ間連携を実装して、ユーザーに気持ちのよい体験を届けられるとうれしいですね。

Swiftで遊んでますか?

モバイルファースト室の三浦です。

みなさんはplayground使っていますか?

Swiftにはplaygroundが用意されていて手軽にかつライブレンダリングでコーディングをすることができます。 CoreGraphicsの描画などを確認しながらコードを書くこともできてとても便利です。

早速Swiftで簡単なスケッチをしてみましょう!

Xcodeでplaygoundファイルを新規作成します。次にUIKitをimportします。

import UIKit

次に表示のためのUIViewを生成します。

// ビューのサイズ
let size = CGSize(width: 200, height: 200)
// UIViewを生成
let view:UIView = UIView(frame: CGRect(origin: CGPointZero, size: size))
view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
// PlaygroundのTimelineに表示するためのview
let preview = view

20141112192138 previewの行の右端をマウスオーバーして表示される+ボタンをクリックすると、タイムラインにその行の実行結果が表示されます。 previewは常にコードの最終行にします。

本来はplayground用に用意されているXCPlaygroundフレームワークのXCPShowViewを使ってTimelineに表示することが可能ですが、現行のXcode6.1でiOS用にUIKitを使って表示した場合コンソールにエラーが出てしまうため使用していません。

早速線を描画してみます。

// CoreGraphicsで描画する
UIGraphicsBeginImageContextWithOptions(size, false, 0)

// 描画する
let path = UIBezierPath()
path.moveToPoint(CGPointMake(50, 100));
path.addLineToPoint(CGPointMake(150, 100))
UIColor.orangeColor().setStroke()
path.stroke()

// viewのlayerに描画したものをセットする
view.layer.contents = UIGraphicsGetImageFromCurrentImageContext().CGImage

UIGraphicsEndImageContext()

20141112192139

右の目のアイコンを押せば行単位の実行結果も確認できます。 strokeのカラーなどを変えれば即座に色が変わります。 20141112192140

UIパーツをつくってみる

過去の記事 iOSアプリデザインリニューアルの舞台裏 で記載していましたが、クックパッドアプリの中でもUIパーツの一部はコードで実装されています。 コード化することでわざわざ画像を用意しなくて済み、さまざまサイズにも柔軟に対応することができます。

左上と右下が角丸のおすすめバッヂも下記のように生成できます。

20141112192141

描画部分のコードは以下のようになっています。

// CoreGraphicsで描画する
UIGraphicsBeginImageContextWithOptions(size, false, 0)

// アイコン画像を描画する
let image = UIImage(named: "image")
image?.drawInRect(CGRectMake(0,0, 192, 192))

// バッヂの背景を描画する
let rect = CGRectMake(0, 0, 96, 36)
let roundCorner = UIRectCorner.TopLeft | UIRectCorner.BottomRight
let roundSize = CGSizeMake(6.0, 6.0)
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: roundCorner, cornerRadii: roundSize)
UIColor(red: 0.545, green: 0.678, blue: 0.0, alpha: 1.0).setFill()
path.fill()

// 文字を描画する
let attrString = NSAttributedString(
    string: "おすすめ",
    attributes:[NSForegroundColorAttributeName: UIColor.whiteColor(),
        NSFontAttributeName: UIFont.boldSystemFontOfSize(20.0)])
attrString.drawAtPoint(CGPointMake(6, 4))

新規のパーツはデザインイメージと違いがないように文字位置やサイズなどコード上で微調整をする必要がありますが、 playgroundであれば描画結果を見ながら調整することができるのでとても便利です。 プロジェクトに組み込む前にも手軽に確認しておくことができます。

まとめ

今回は静的なパーツをつくるところまでなので開発速度における大きなメリットはでにくいですが、 少し複雑なアイコンをパスで描く際や、パスをさらにアニメーションさせるときなどは、変化させたいプロパティを調整していけばコンパイルすることなく動きを確認できるので、playgroundであらかじめ試作しておけばプロジェクトの組み込み時には確度の高いアニメーションを実現することができます。 そしてなによりコードを書いていて楽しい!

アプリ開発にもぜひplaygorundを活用してみてください!