Usual Software Engineer

よくあるソフトウェアエンジニアのブログ

リバースプロキシのテストを書く方法

6月30日です。今日で6月も終わりです。でっていう。

Nginx や OpenResty 、 Varnish などをリバースプロキシとして使用していることが多いと思いますが、 リバースプロキシ単体で設定が正しいかどうかをテストするのって難しそうなイメージがありませんか?

ここではリバースプロキシの後ろのバックエンドをモック化して、 リバースプロキシ単体のテストを書く方法を紹介します。
本当はリバースプロキシのIP制限のテストのために、ソケット通信でネットワークインターフェースを指定して 接続元のクライアントIPを自由に書き換える方法を紹介しようと思いましたが、 Dockerコンテナ内でテストを動かさないといけなかったのでやめました。でっていう。

今回のサンプルリポジトリこちらです。

github.com

目的は、 Nginx の conf が意図したとおりの動作をするかどうかのテストをすることです。
バックエンドのモック化のために MockServer を使用します。

MockServer https://www.mock-server.com/images/system_under_test_with_mockserver.png

JavaAPI を使ってランタイムで MockServer を起動することもできますが、 Docker image が提供されているので いつも通り Docker Compose で環境を準備します。

reverseproxy-mockserver/docker-compose.yml at master · innossh/reverseproxy-mockserver · GitHub

version: '3'
services:
  reverseproxy:
    image: nginx:1.12.0-alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - backend1
      - backend2
  backend1:
    image: jamesdbloom/mockserver
    ports:
      - "1081:11111"
    entrypoint: /opt/mockserver/run_mockserver.sh -serverPort 11111
  backend2:
    image: jamesdbloom/mockserver
    ports:
      - "1082:22222"
    entrypoint: /opt/mockserver/run_mockserver.sh -serverPort 22222

backend1 というサービスが backend1コンテナの11111番ポートで、 backend2 というサービスが backend2コンテナの22222番ポートで動いていると想定します。
Nginx のコンテナを用意し、 テストしたい Nginx の conf に各 backend へリクエストを流すような設定を入れて動かします。 Nginx の設定内容はこちら。

reverseproxy-mockserver/nginx.conf at master · innossh/reverseproxy-mockserver · GitHub

...
http {
...
    set_real_ip_from   172.29.0.0/16;
    real_ip_header     X-Forwarded-For;
    real_ip_recursive  on;

    geo $denied {
        default        1;

        127.0.0.1      0;
        172.29.0.0/16  0;
        1.2.3.4        0;
    }
...
    server {
        listen       80;
        server_name  localhost;
...
        location /backend1 {
            rewrite     ^/backend1(.*)$ $1 break;
            proxy_pass  http://backend1:11111;
        }

        location /backend2 {
            if ($denied) {
                return 403 "Forbidden";
            }
            rewrite     ^/backend2(.*)$ $1 break;
            proxy_pass  http://backend2:22222;
        }
    }
}

だいぶ適当な設定ですね。でっていう。
/backend1 へのリクエストは http://backend1:11111 に、 /backend2 へのリクエストは http://backend2:22222 に流れるはずですね。

それでは docker-compose up -d した後にJavaのテストを実行してみましょう。
Javaのテストの内容はこちらです。

reverseproxy-mockserver/AccessDeniedTest.java at master · innossh/reverseproxy-mockserver · GitHub

    @Test
    @Parameters({
            "1.2.3.4,204",
            "4.3.2.1,403"
    })
    public void testAccessDenied(String clientIP, int expectedStatus) throws IOException {
        clients.getBackend2()
                .when(
                        request()
                                .withMethod("GET")
                                .withPath("/bar")
                )
                .respond(
                        response()
                                .withStatusCode(204)
                );

        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .header("X-Forwarded-For", clientIP)
                .url("http://localhost/backend2/bar")
                .get()
                .build();
        Response response = client.newCall(request).execute();

        assertEquals(expectedStatus, response.code());
        clients.getBackend2().verify(
                request()
                        .withMethod("GET")
                        .withPath("/bar"),
                VerificationTimes.exactly(expectedStatus == 204 ? 1 : 0)
        );
    }

上記のコードは

  1. backend2 に対して期待する動作を設定する。 # 裏で backend2 の MockServer にリクエストを投げて expectation を登録している
  2. OKHttpClient で実際に Nginx に対してHTTPリクエストを実行する。 # URL は http://localhost/backend2/bar
  3. レスポンスコードの比較と backend2 の MockServer が正しく呼び出されたか検証する。

という感じの流れです。
2つのクライアントIPをヘッダに設定して 1.2.3.4204 に、 4.3.2.1403 になることをテストしています。
実行はもちろん

$ ./gradlew test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test

innossh.reverseproxy.mockserver.AccessDeniedTest > testAccessDenied(1.2.3.4,204) [0] STARTED

innossh.reverseproxy.mockserver.AccessDeniedTest > testAccessDenied(1.2.3.4,204) [0] PASSED

innossh.reverseproxy.mockserver.AccessDeniedTest > testAccessDenied(4.3.2.1,403) [1] STARTED

innossh.reverseproxy.mockserver.AccessDeniedTest > testAccessDenied(4.3.2.1,403) [1] PASSED

innossh.reverseproxy.mockserver.AccessDeniedTest > testNormalToBackend1 STARTED

innossh.reverseproxy.mockserver.AccessDeniedTest > testNormalToBackend1 PASSED

innossh.reverseproxy.mockserver.AccessDeniedTest > testNormalToBackend2 STARTED

innossh.reverseproxy.mockserver.AccessDeniedTest > testNormalToBackend2 PASSED

BUILD SUCCESSFUL

Total time: 8.079 secs

成功しますね。

これで基本形ができましたので、あとはひたすらいろんな path へのリクエストやいろんな header のあるなしなどをテストしていけば、 リバースプロキシ単体の動きのテストができることになります。
リバースプロキシのテストでお困りの人は試してみてはいかがでしょうか。