ProxyKit を使ってリバースプロキシを実装する

はじめに

ASP.NET Core で実装した Web API の前段にリバースプロキシを置く必要がありそう。 しかも、そのリバースプロキシはデータベースにある情報を使ってリクエストの振り分けを行わなければならないため、 プログラマブルなリバースプロキシが求められる。

ProxyKit

Microsoft が最近 reverse-proxy を GitHub で公開したので、最初これを使おうかと思ったら、2020/05/11 時点で NuGet パッケージは無かった。時期尚早。

github.com

同じようなリバースプロキシを実装するためのツールキットを探したところ、『ProxyKit』というものがあったので試してみた。

github.com

リバースプロキシの後ろで動く Web API Ver.1

リバースプロキシの後ろで動かす Web API .NET Core 3.1 + ASP.NET Core 3.1 で実装。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace WebApiV1
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>()
                        .UseUrls("http://localhost:5001");
                });
    }

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }

    [Route("api/[controller]")]
    [ApiController]
    public class GreetingController : ControllerBase
    {
        [HttpGet("hello/{name}")]
        public string Hello(string name)
        {
            return $"[V1] Hello {name}.";
        }
    }
}

リバースプロキシの後ろで動く Web API Ver.2

Web API の Ver.2 も実装。といっても違うのは、名前空間と、返す文字列と、起動する際のポートくらい。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace WebApiV2
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>()
                        .UseUrls("http://localhost:5002");
                });
    }

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }

    [Route("api/[controller]")]
    [ApiController]
    public class GreetingController : ControllerBase
    {
        [HttpGet("hello/{name}")]
        public string Hello(string name)
        {
            return $"[V2] Hello {name}.";
        }
    }
}

リバースプロキシ

リバースプロキシを .NET Core + .ASP.NET Core 3.1 で実装。RunProxy に渡すデリゲート内で任意の処理を書けるので、リクエストヘッダーをチェックして後ろの Web API のどちらかに振り分ける。

実際に使うときにはヘッダーとデータベースを突き合わせて振り分け先を決定する予定。今回のサンプルではそこまでやらない。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ProxyKit;

namespace ReverseProxy
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>()
                        .UseUrls("http://localhost:5000");
                });
    }

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddProxy();
        }

        const string WebApiV1Url = "http://localhost:5001";

        const string WebApiV2Url = "http://localhost:5002";

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.RunProxy(context =>
            {
                if (context.Request.Headers.TryGetValue("X-API-Version", out var versions))
                {
                    if (versions[0] == "v1")
                    {
                        return context.ForwardTo(WebApiV1Url)
                            .Send();
                    }
                    if (versions[0] == "v2")
                    {
                        return context.ForwardTo(WebApiV2Url)
                            .Send();
                    }
                }
                return context.ForwardTo(WebApiV2Url)
                    .Send();
            });
        }
    }
}

Web API を呼び出すクライアント

リバースプロキシにアクセスするクライアントを .NET Core 3.1 のコンソールアプリとして作成。HTTP リクエストのカスタムヘッダーなし、Ver.1 を指定、Ver.2 を指定の 3 パターンを実装した。

using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace Client
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var httpClient = new HttpClient();

            // ヘッダーでバージョンを指定しなかったら v2
            {
                var request = new HttpRequestMessage(
                    HttpMethod.Get,
                    "http://localhost:5000/api/greeting/hello/Kubo");

                var response = await httpClient.SendAsync(request);

                response.EnsureSuccessStatusCode();
                Console.WriteLine(await response.Content.ReadAsStringAsync());
            }

            // X-API-Version ヘッダーで v1 を指定
            {
                var request = new HttpRequestMessage(
                    HttpMethod.Get,
                    "http://localhost:5000/api/greeting/hello/Minamino");
                request.Headers.Add("X-API-Version", "v1");

                var response = await httpClient.SendAsync(request);

                response.EnsureSuccessStatusCode();
                Console.WriteLine(await response.Content.ReadAsStringAsync());
            }

            // X-API-Version ヘッダーで v2 を指定
            {
                var request = new HttpRequestMessage(
                    HttpMethod.Get,
                    "http://localhost:5000/api/greeting/hello/Osako");
                request.Headers.Add("X-API-Version", "v2");

                var response = await httpClient.SendAsync(request);

                response.EnsureSuccessStatusCode();
                Console.WriteLine(await response.Content.ReadAsStringAsync());
            }

            Console.ReadLine();
        }
    }
}

実行結果

X-API-Version ヘッダーで指定したバージョンの Web API にリクエストが転送されたことを確認できた。

f:id:griefworker:20200507154310p:plain

おわりに

HTTP リクエストのヘッダーをチェックして、リクエストを振り分けるリバースプロキシを実装できた。 ASP.NET Core のミドルウェアを書いている感覚に近い。同じくらいの自由度はありそう。

今回のサンプルでは、ヘッダーの値をバージョンを表す文字列にしたけど、これが JWT になっても対応できるはず。本当にやりたいことはそっちなんだけどね。