こんにちは。吉川 ( @rrreeeyyy ) です。今期オススメのアニメはリゼロです。
Nginx は設定ファイルの記述力も高い、大変便利な Web サーバです。 便利な反面、設定ファイルの複雑化や、設定に依っては意図しない挙動を引き起こしてしまうこともあります。 そこで本稿では docker 並びに infrataster を使用し、 Nginx の挙動をテストすることによって、安全に Nginx の設定を記述する方法について紹介します。
テスト対象の Nginx の仕様
今回は例として、次のような仕様の Nginx のテストについて考えます。
- ネットワーク帯は
10.0.0.0/16
を使用している - Nginx の前段として L7 ロードバランサが存在している
- L7 ロードバランサが https を終端している
- Nginx 自体は 80 番ポートと 8080 番ポートにて待ち受けている
- 80 番ポートは通常のアプリケーション用になっている
- 8080 番ポートは Nginx の
stub_status
を閲覧する用途になっている
- http でアクセスがあった場合は https に 301 リダイレクトを行う
- Nginx とアプリケーションは Unix domain socket を使用して通信している
- 接続元 IP アドレスのブラックリストが存在している
- ブラックリストの IP アドレスからアクセスがされた場合 403 を返す
- メンテナンスモードが存在している
- メンテナンス時には
/maintenance.html
に 302 リダイレクトを行う - 特定の IP アドレスからアクセスされた場合はメンテナンス時でも通常通り表示出来るようにしている
/healthcheck
というパスに関してはメンテナンス時でも通常通りプロキシするようにしている
- メンテナンス時には
一般的に存在しそうな、少し複雑な Nginx の設定、というところでしょうか。
難しい所として、L7 ロードバランサがあるため、X-Forwarded-For
ヘッダや、
X-Forwarded-Proto
のようなヘッダに注意して設定を行う必要がある点が挙げられます。
また、実際に本番環境に設定を適用した段階の動作確認に関しても、 特定 IP アドレスのそれぞれから行う必要があるのも難しい点かと思います。
例えば、ブラックリストの IP アドレス一覧は Nginx の geo
ディレクティブを使い次のように書けます。
ここでは、同一ネットワークの上流に L7 バランサがいると仮定しているため、proxy
ディレクティブにて、
同一ネットワークから来たアクセスの場合、X-Forwarded-For
を参照させるようにしています。
(参考: Module ngx_http_geo_module
)
http { : geo $blacklist { default 0; proxy 10.0.0.0/16; 192.0.2.1/32 1; } : }
今回は、Docker のネットワーク機能など使用し、 擬似的に上記のネットワーク・アプリケーション構造を再現することにより、 このように特定ネットワーク等に依存する設定ファイルのテストを行えるようにしていきます。
ソースコード一覧
最初に、これから紹介するソースコードの全体は、 Github の rrreeeyyy/nginx-behavior-test-example にて公開しています。こちらをあわせて参照しながら読み進めて行くと分かりやすいかと思います。
docker-compose
まず、振る舞いをテストするにあたって、実際に必要なコンポーネントは以下の 3 つだと考えます。
- L7 ロードバランサ
- Nginx (テスト対象)
- アプリケーション
これらを、Docker のネットワーク機能で組み合わせる事を考えます。
各コンポーネントの詳細は後述しますが、docker-compose.yml
は次のようになります。
version: '2' networks: front: ipam: config: - subnet: 10.0.0.0/16 volumes: socket: driver: local services: haproxy: build: haproxy/ container_name: docker-haproxy-t networks: - front ports: - "8888:80" nginx: build: nginx/ container_name: docker-nginx-t networks: - front expose: - "80" ports: - "8080:8080" volumes: - socket:/var/run/unicorn/ app: build: app/ container_name: docker-app-t networks: - front volumes: - socket:/var/run/unicorn/
まず、Compose のファイルフォーマットは v2 です。そのため Compose 1.7 以上をお使いください。
networks
ディレクティブで仮想ネットワークを作成します。
ここでは front
という名前のネットワークを作成しています。
この subnet を 10.0.0.0/16
とし、実際の本番環境のネットワーク構造と合わせることで、
Nginx の proxy
や set_real_ip_from
などの設定を変更すること無く、
実際の設定ファイルをそのまま使用してテストを行う事を可能にしています。
また、nginx とアプリケーションは、Unix domain socket で通信するという前提になっているので、
ソケットを配置する共有ボリュームを volumes
ディレクティブで作成し、
Nginx とアプリケーションの両方のコンテナにて、 /var/run/unicorn
としてマウントしておきます。
もしソケットではなく TCP で通信する場合でも、expose
を上手く使用すれば同等の事を実現可能です。
フロントのロードバランサは、ホストの 8888
をコンテナの 80
に対応付けるようにしています。
また、Nginx 自体は 80
をコンテナネットワーク内で通信できるようにし、
8080
はホストの 8080
に対応付けるようにしています。
続いて、各コンポーネントの説明をしていきます。
ロードバランサのモック
今回は L7 ロードバランサとして、haproxy を使用します。
haproxy の設定ファイルとして、次のようなものを用意しました。
global log 127.0.0.1 local0 log 127.0.0.1 local1 notice chroot / user root group root defaults log global mode http option httplog option dontlognull timeout connect 5000ms timeout client 50000ms timeout server 50000ms frontend http-in bind *:80 default_backend nginx backend nginx server nginx nginx:80
80 番ポートで待ち受け、nginx コンテナの 80 番にプロキシするだけの設定ファイルです。
実際の本番環境で HAProxy のような L7 ロードバランサを使用する際には、
X-Forwarded-For
や X-Forwarded-Proto
等を正しく設定し、後段のサーバに渡す必要があります。
今回は、あえて設定しないことにより、上記のようなヘッダをクライアントが任意に設定出来るようになり、 様々なシチュエーションを考慮したテストを記述することが可能となります。
アプリケーションのモック
アプリケーションに関しては、実際にプロキシを行うアプリケーションが Docker で用意できればベストですが、 なかなかそうなっていない事もあると思うので、Sinatra で次のようなアプリを用意しました。
require 'json' require 'sinatra' class App < Sinatra::Base set :protection, :except => [:ip_spoofing] get '/healthcheck' do 'success' end get '/*' do env.to_json end end
これは、/healthcheck
にアクセスが来た時には success
という文字列を返し、
その他のパスにアクセスが来た際には env
を JSON にして返すという単純なアプリケーションです。
前述のとおり、実際にプロキシを行うアプリケーションが用意できるのがベストですが、 今回はプロキシの振る舞いにのみ興味があり、コンテンツの内容までをテスト対象としていないため、 このような擬似的なアプリケーションで十分であると判断しました。
env
を返している理由は、プロキシサーバから渡ってきた env
を、
アプリケーション内で使用するパターンが存在する可能性があるため、
アプリケーションにどのような env
が渡っているかもテスト出来るようにしたいという狙いがあるためです。
また、今回は前述のとおり意図的に ip_spoofing
が可能なようにしているので、
Rack::Protection::IPSpoofing
を無効にしています。
振る舞いのテスト
さて、実際のテスト自体は spec
配下に入っています。
spec_helper.rb
に関しては rspec --init
で生成されたものに、
Infrataster 用の設定を追加しただけとなります。追加部は以下のとおりです。
require 'uri' require 'infrataster/rspec' Infrataster::Server.define( :nginx, URI.parse(ENV['DOCKER_HOST']).host, )
ここで、サーバの IP を DOCKER_HOST
の host
部にしています。
例えば tcp://192.168.99.100:2376
であれば 192.168.99.100
です。
もし DOCKER_HOST
が Unix domain socket の場合は、ここは localhost
等にしてやれば良いはずです。
テスト本体である spec/www.example.com_spec.rb
は以下のとおりです。
describe server(:nginx) do describe http( 'http://haproxy:8888/', headers: { 'Host' => 'www.example.com', 'X-Forwarded-Proto' => 'https', } ) do it '/ responds with 200' do expect(response.status).to eq(200) end end describe http( 'http://haproxy:8888/', headers: { 'Host' => 'www.example.com', 'X-Forwarded-Proto' => 'https', 'X-Forwarded-For' => '192.0.2.1', } ) do it '/ responds with 403 when access from blacklisted IP address' do expect(response.status).to eq(403) expect(response.body).to include('Forbidden') end end end
前述のとおり、X-Forwarded-Proto
や X-Forwarded-For
は、
テスト内で任意に決定することが可能なため、様々なシチュエーションを想定したテストを書くことが出来ます。
上記の例では、ブラックリストに登録された IP からアクセスが来たら本当に 403 を返すかどうかもテスト出来ています。
最初に紹介した仕様を満たすかどうかをテストしたものは、
前述のとおり、公開している Github リポジトリの spec
配下に全て入っているため、
よろしければ参考にしてみてください。
ちなみに、全てのテストの実行結果は以下のようになります。
まとめ
Docker によるネットワークの再現、ロードバランサ、アプリケーションのモックにより、 Nginx の設定ファイルの振る舞いをテストする方法について紹介しました。
このテストは、Nginx の設定をパースしたりしてテストしているわけではなく、振る舞い自体をテストしているため、
nginScript や lua-nginx-module, ngx_mruby
等にて記述された設定ファイルにも適用可能な手法だと考えています。
また、今回は Infrataster を使用しましたが、実際にはただ HTTP アクセスをしているのみなので、
例えば curl $(docker-machine ip):8888 -H 'Host: www.example.com'
などすれば、
curl
でのデバッグも出来ますし、Perlの標準モジュール Test::More と HTTP::Tiny だけでテストコードを書く 事も恐らく可能です。
このように、ミドルウェアの設定ファイルの振る舞いに対するテストを書くことで、 安全かつ積極的な設定変更が行えるようになると考えています。
ハートビーツでは、より安心してサービスを任せられる MSP となるべく、 テスト手法自体や設定ファイルのテストに興味があるエンジニアを 募集 しています。