読者です 読者をやめる 読者になる 読者になる

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を使ったテストの参考になれば幸いです。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/