ソーシャルアプリプラットフォーム構築技法

はじめに

仕事でプラットフォームを設計・開発することになったものの、どこから手をつければいいのか見当がつかなかったところで、本書の存在を知った。書店でパラパラと見て、まさに今の自分に必要なものだと思い即購入。

第1章 ソーシャルアプリプラットフォームとは何か,何を構築するのか

プラットフォームを自分が認識したのは Open Social が最初だったな。サンシャイン牧場とか懐かしい。

プラットフォームの運営には、下記のように、たくさんのチームが関わることになる。

  • プラットフォームの開発チーム
  • 元となるサービスの開発チーム
  • 企画
  • プラットフォームのアプリ開発者を開拓する営業チーム
  • QA/テスト
  • サポート
  • 審査
  • 法務/セキュリティ

これだけでもう一大プロジェクトだというのがわかる。

第2章 プラットフォームの企画策定

プラットフォームの全体像は

  • 元となるサービス
  • アプリ
  • 「元となるサービス」と「アプリ」の間のインタフェース
  • アプリ管理サイト

という風に実はシンプル。

アプリ開発者にどんなコンテンツを提供し、どんなアプリを開発してもらうか。まずはそこから。

プラットフォーム開始時にいくつかラインナップを揃えたい。アプリ開発者候補を探して開発してもらう必要がある。プラットフォームの成否は、プラットフォームを一緒に開発するくらい熱量がある開発者を掴まえることができるかにかかっているのか。

アプリ開発者はボランティアじゃないので、当然彼らがマネタイズする手段も用意する必要がある。他にも利用規約ガイドラインと、考えることは予想以上に多い。

第3章 プラットフォームの開発

プラットフォームが提供する主たるものは API で、本書の当時から RESTful API は主流だったようだ。RESTful API は今でも現役だし、実装のしやすさと使いやすさのバランスを考えると、自分も RESTful API で作ることになりそう。

認証・認可は OAuth または OpenID Connect でアクセストークンを発行する方式が今でも有力。認可画面の作り込みも必要そうだ。部分的に認可・不認可を選択できるようにするところまでは考えてなかったけど。

負荷対策やロードバランシング、あと RateLimit についても考える必要があり、やることは多いな。これらはリバースプロキシで対応するのがいいか。

第4章 SDK(Software Development Kit)の開発

SDKを用意するにあたって一番面倒そうなのが認証と認可。OAuth または OpenId Connect での認証と認可を、どう SDK で提供すればいいのか知りたかったので、iOSAndroid での具体的な実装方法が提示してもらえて参考になった。SFSafariViewController や Chrome Custom Tabs を使えばいいのか。

プラットフォーム提供元がアプリも提供していて、そのアプリがインストールされている場合に、アプリを呼び出して認証と認可を行ってもらう方法もあることは手札の一つとして覚えておこう。

第5章 プラットフォーム運用の実際

インシデント対応では、ユーザーの被害を抑えることが最優先。事前に決めておく必要性を感じた。インシデントは問題が起きてから対応を考えるのでは遅い。

第6章 BOT プラットフォームとAI の利用

筆者は BOT プラットフォームが次に来ると予想していたようで、まさにその通りになった。機械学習によって、BOT で出来ることの幅が増えたのが大きい。ただ、BOT には食指が動かないんだよなぁ。自分がその有用さをまだ体感していないからだろうか。

まとめ

mixi と LINEでプラットフォームを構築した筆者のノウハウが満載。具体的な実装よりも抽象度の高い内容で、プラットフォームの企画から開発、運用まで全体のイメージをつかむことができた。

人を動かす 新装版

最近、マネジメントにも興味がでてきたので、名著として名高い本書を読んでみた。 といっても、自分は現在マネージャーではなくエンジニアだし、この先マネージャーにキャリアチェンジする予定もないけれど。 他のチームと交渉することはあるし、後輩の指導を行う可能性もあるので、読んで損はないだろう。

本書が掲げる原則のうち、自分に関係がありそうなのは「人を動かす三原則」「人を説得する十二原則」「人を変える九原則」。これらの原則に共通して見られたのが「ほめる」ことだった。批判するだけではだめ。ほめて伸ばす。叱って伸ばすなんて論外。まったくその通りだと思う。読んでいて頷きが止まらなかった。

注意するときも、「こうすればもっとよくなる」という風な表現に変えるだけで、はるかに受け入れやすく感じる。 相手に気付かせたり、相手に思いつかせるように仕向けるところなんかは、コーチングに近いかも。 そういった原則について本書には具体的なエピソードがたくさん掲載されていた。 昔のエピソードばかりだけど雰囲気はつかめる。 仕事だけでなく、プレイベート、とりわけ子育てにも生かすことができそうだった。 読んで満足するだけで終わらず、実践しなければ。

人を動かす 新装版

人を動かす 新装版

https にリダイレクトすると Authorization ヘッダーが失われる

はじめに

HttpClient を使って http で Authorization ヘッダー付きのリクエストを送信し、https にリダイレクトされると、https のリクエストには Authorization ヘッダーが付かない。

以下、実験コード。

Web API

HTTP リクエストのヘッダーをログに出力している。リダイレクト前のリクエストヘッダーを確認するために、ログを出力するミドルウェアも挟んでいる。

using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace HttpsRedirectSample
{
    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>();
                });
    }

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.Use(next => context =>
            {
                LogRequestHeaders(context);
                return next(context);
            });

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    LogRequestHeaders(context);
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }

        static void LogRequestHeaders(HttpContext context)
        {
            var sb = new StringBuilder();
            foreach (var h in context.Request.Headers)
            {
                sb.AppendLine($"{h.Key}:{h.Value}");
            }

            var logger = context.RequestServices
                .GetService<ILoggerFactory>()
                .CreateLogger("RequestHeaders");
            logger.LogInformation(sb.ToString());
        }
    }
}

Client

適当な値の Authorization ヘッダーを付けて http にリクエストを送信する。

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

namespace ClientSample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var httpClient = new HttpClient()
            {
                BaseAddress = new Uri("http://localhost:5000"),
            };
            httpClient.DefaultRequestHeaders.Authorization =
                new AuthenticationHeaderValue("Bearer", "Test");

            var response = await httpClient.GetAsync("/");

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

実行結果

f:id:griefworker:20200521171641p:plain

リダイレクト前にはあった Authorization ヘッダーが、リダイレクト後には含まれていないことを確認できた。

おわりに

IdetityServer4 ホストと、IdetityServer4 が発行するアクセストークンを使って呼び出す Web API を Azure App Service にデプロイしたところ、アクセストークンを付けて Web API を呼び出しているのに 401 が返ってきていた。

IdetityServer4 と Web API 両方ローカルだと上手くいく。IdetityServer4 だけ Azure 上でも上手くいく。両方 Azure 上だとダメ。

検証とはいえアクセストークンの署名に開発用キーを使っているからか?それとも ASP.NET Core の JwtBearer か IdetityServer4 のバグか?と疑ってライブラリのソースコードを調べていたが、原因は Azure 上の Web API を呼び出すときの URL を https ではなく http にしていたからだったとさ。自分のコードが悪かった。

こんな凡ミスに1日半費やしてしまった。自戒を込めてブログに残しておく。

認証スキームの異なる複数の JWT Bearer 認証を構成してみた

はじめに

ASP.NET Core で JWT Bearer 認証を使うときに Startup.ConfigureServices で呼び出す AddJwtBearer は、 任意の authenticationScheme を指定できる。

となると、authenticationScheme さえ重複しなければ複数回呼び出しても問題ない、はず。 気になったので実験してみた。

Web API

AddJwtBearer では、authenticationScheme だけでなく署名に使うキーも変えておく。 JWT Bearer のデフォルト authenticationScheme は Bearer だけど、今回は分かりやすいように Bearer1 と Bearer2 にしてみた。

using System;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;

namespace JwtBearerSample
{
    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>();
                });
    }

    public class Startup
    {
        internal static readonly SymmetricSecurityKey Bearer1SecurityKey =
            new SymmetricSecurityKey(Guid.Parse("FA99A883-1B90-4515-A841-64BE3322A663").ToByteArray());

        internal static readonly SymmetricSecurityKey Bearer2SecurityKey =
            new SymmetricSecurityKey(Guid.Parse("AECD611D-B2DC-4909-B921-8465A9C238A8").ToByteArray());

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

        public IConfiguration Configuration { get; }

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

            // JWT Bearer 認証を組み込む
            services.AddAuthentication("Bearer1")
                .AddJwtBearer("Bearer1", options =>
                {
                    // サンプルを簡略化するため検証機能を OFF にする
                    // 本番でこんなことをしてはダメ
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateAudience = false,
                        ValidateIssuer = false,
                        ValidateActor = false,
                        ValidateLifetime = true,
                        IssuerSigningKey = Bearer1SecurityKey
                    };
                })
                .AddJwtBearer("Bearer2", options =>
                {
                    // サンプルを簡略化するため検証機能を OFF にする
                    // 本番でこんなことをしてはダメ
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateAudience = false,
                        ValidateIssuer = false,
                        ValidateActor = false,
                        ValidateLifetime = true,
                        IssuerSigningKey = Bearer2SecurityKey
                    };
                });
        }

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

            // パイプラインに認証と認可を組み込む
            app.UseAuthentication();
            app.UseAuthorization();

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

    public class TokenRequest
    {
        [Required]
        public string UserName { get; set; }
    }

    public class TokenResponse
    {
        public string Token { get; set; }
    }

    // アクセストークンを発行する API
    [Route("api/[controller]")]
    [ApiController]
    public class AccountController : ControllerBase
    {
        // Bearer1 用のアクセストークンを発行する
        [HttpPost("bearer1token")]
        public ActionResult<TokenResponse> Bearer1Token([FromBody] TokenRequest request)
        {
            var token = GenerateJwtToken(request.UserName, Startup.Bearer1SecurityKey);
            return new TokenResponse
            {
                Token = token,
            };
        }

        // Bearer2 用のアクセストークンを発行する
        [HttpPost("bearer2token")]
        public ActionResult<TokenResponse> Bearer2Token([FromBody] TokenRequest request)
        {
            var token = GenerateJwtToken(request.UserName, Startup.Bearer2SecurityKey);
            return new TokenResponse
            {
                Token = token,
            };
        }

        string GenerateJwtToken(string userName, SecurityKey securityKey)
        {
            var claims = new[]
            {
                new Claim(ClaimTypes.Name, userName),
            };
            var credentials = new SigningCredentials(
                securityKey,
                SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                "ExampleServer",
                "ExampleClients",
                claims,
                expires: DateTime.Now.AddSeconds(60),
                signingCredentials: credentials);
            var tokenHandler = new JwtSecurityTokenHandler();
            return tokenHandler.WriteToken(token);
        }
    }

    [Route("api/[controller]")]
    [ApiController]
    public class GreetingController : ControllerBase
    {
        // Bearer1 用のアクセストークンで呼び出せる
        [Authorize(AuthenticationSchemes = "Bearer1")]
        [HttpGet("hello")]
        public string Hello()
        {
            return $"Hello {User.Identity.Name}";
        }

        // Bearer2 用のアクセストークンで呼び出せる
        [Authorize(AuthenticationSchemes = "Bearer2")]
        [HttpGet("good-morning")]
        public string GoodMorning()
        {
            return $"Good morning {User.Identity.Name}";
        }

        // Bearer1, Bearer2 両方のアクセストークンで呼び出せる
        [Authorize(AuthenticationSchemes = "Bearer1,Bearer2")]
        [HttpGet("good-afternoon")]
        public string GoodAfternoon()
        {
            return $"Good afternoon {User.Identity.Name}";
        }
    }
}

Client

Bearer1 と Bearer2 それぞれのアクセストークンを使って、保護しているすべての API を呼び出してみる。

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace Client
{
    class Program
    {
        static readonly Uri BaseAddress = new Uri("http://localhost:5000");

        static async Task Main(string[] args)
        {
            await Bearer1Test();

            Console.WriteLine();

            await Bearer2Test();

            Console.ReadLine();
        }

        static async Task Bearer1Test()
        {
            Console.WriteLine("== Bearer1 ==");

            var httpClient = new HttpClient()
            {
                BaseAddress = BaseAddress,
            };

            var tokenResponse = await httpClient.PostAsync(
                "/api/account/bearer1token",
                new StringContent(
                    @"{
                        ""userName"": ""Kubo""
                      }",
                    Encoding.UTF8,
                    "application/json"));
            var json = await tokenResponse.Content.ReadAsStringAsync();
            var jDocument = JsonDocument.Parse(json);
            var accessToken = jDocument.RootElement.GetProperty("token").GetString();
            Console.WriteLine(accessToken);

            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
                "Bearer", accessToken);

            var helloResponse = await httpClient.GetAsync("/api/greeting/hello");
            Console.WriteLine(await helloResponse.Content.ReadAsStringAsync());

            var goodMorningResponse = await httpClient.GetAsync("/api/greeting/good-morning");
            Console.WriteLine(goodMorningResponse.StatusCode);

            var goodAfternoonResponse = await httpClient.GetAsync("/api/greeting/good-afternoon");
            Console.WriteLine(await goodAfternoonResponse.Content.ReadAsStringAsync());
        }

        static async Task Bearer2Test()
        {
            Console.WriteLine("== Bearer2 ==");

            var httpClient = new HttpClient()
            {
                BaseAddress = BaseAddress,
            };

            var tokenResponse = await httpClient.PostAsync(
                "/api/account/bearer2token",
                new StringContent(
                    @"{
                        ""userName"": ""Minamino""
                      }",
                    Encoding.UTF8,
                    "application/json"));
            var json = await tokenResponse.Content.ReadAsStringAsync();
            var jDocument = JsonDocument.Parse(json);
            var accessToken = jDocument.RootElement.GetProperty("token").GetString();
            Console.WriteLine(accessToken);

            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
                "Bearer", accessToken);

            var helloResponse = await httpClient.GetAsync("/api/greeting/hello");
            Console.WriteLine(helloResponse.StatusCode);

            var goodMorningResponse = await httpClient.GetAsync("/api/greeting/good-morning");
            Console.WriteLine(await goodMorningResponse.Content.ReadAsStringAsync());

            var goodAfternoonResponse = await httpClient.GetAsync("/api/greeting/good-afternoon");
            Console.WriteLine(await goodAfternoonResponse.Content.ReadAsStringAsync());
        }
    }
}

実行結果

f:id:griefworker:20200512145643p:plain

Bearer1 用のアクセストークンを使うと、AuthenticationSchemes に Bearer1 を指定している Hello と GoodAfternoon は呼び出せるが、指定していない GoodMorning は呼び出せない。

Bearer2 用のアクセストークンを使うと、AuthenticationSchemes に Bearer2 を指定している GoodMorning と GoodAfternoon は呼び出せるが、指定していない Hello は呼び出せない。

おわりに

予想通り、authenticationScheme を変えて複数回 AddJwtBearer を呼び出してもちゃんと動くことを確認できた。 十中八九上手くいくと思っていても、実際に動かしてこの目で確認するまでは安心できないからね。

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 になっても対応できるはず。本当にやりたいことはそっちなんだけどね。

アルスラーン戦記(13)

忠臣のザンデが相手とはいえ、あとヒルメスが部下の言葉に対して聞く耳をもつとはね。人間的な成長を感じられた。アルスラーンと比べられたらすぐ激昂してしまうのは相変わらずだけど。

アンドラゴラスは王都エクバターナを脱出したが、アルスラーンの軍に合流するんだろうか。合流した場合、パルス軍の指揮権は、まぁ王太子よりは王が優先されるよな、やっぱ。アンドラゴラスに嫌われているナルサスアルスラーン側に付くだろう。ダリューンも。エラムアルフリードは言うに及ばず。ファランギースもおそらくは。ただ、万騎長のキシュワードクバードはそうもいかないだろう。彼らが忠誠を誓っているのはアルスラーンではなくパルス王家だから。

そのアルスラーン率いるパルス軍はトゥラーン軍といよいよ衝突。大規模な戦争はアトロパテネ以来か。戦力は10万対10万で五分。ペシャワール城があるとはいえ、まともにぶつかったら勝つにしても犠牲は大きそうだ。ナルサスが策を弄するだろうか。続きが気になる。

WEB+DB PRESS Vol.116

毎号購読している WEB+DB PRESS の Vol.116 を読み終わったので、 感想などをメモしておく。

特集1 はじめてのトラブルシューティング

フロントエンドとバックエンド、それにデータベースとモバイルに渡って、それぞれの領域でのトラブルシューティング入門記事。

トラブルシューティングのやり方、とりわけパフォーマンスの改善を新人研修で教えることは無さそうだけど、ぜひとも知ってほしい知識なので新人研修の教材にしたいくらいだ。

特集2 [実践] AWS CodeDeploy

デプロイに特化したマネージドサービスには懐疑的だったけど、自分で組むには骨が折れる Blue/Green デプロイやイミュータブルデプロイをかなり手軽に組めるのは魅力的だ。

EC2 を使っているなら十分あり。というか、追加費用が発生しないなら使わない理由がない。自分がメインで使っているのは Microsoft Azure だけど、Azure にも欲しい。AWS は先を行ってるな。

特集3 アプリケーションアクセシビリティ

業務ではバックエンドと社内向けツールのフロントエンドを少々やってきたが、恥ずかしながらアクセシビリティの意識が皆無だった。

アクセシビリティを向上させることで、今まで使いたくても使えなかったようなユーザーにリーチできるし、将来自分が必要とする可能性もゼロでは無いので、将来の自分の役に立つと考えて取り組もう。

マネジメントの現場[第1回] エンジニアにマネジメントは必要?

マネジメント不要論者だったが、自走できるエンジニアばかりとは限らないし、 自走できても皆が同じ方向に走れるとも限らないので、エンジニアリングマネージャーによるマネジメントは必要と思い直した。

自分が所属する組織は定期的な 1on1 をやっていないどころか、年 1 回あったりなかったりするんだけど、そこで自分から 1on1 を要求する発想はなかったな。反省。

まずゴールを明確化して、そのゴールに対して起こっている状況を把握し現状を整理したうえで、本人にどういったことができるか可能性を考えてもらい、最後にどういったことをしたいかをネクストアクションとして決めてもらいます。

上記の、Google が採用している GROW というコーチングのフレームワークは学んでおこう。自分がマネージャーになることは転職でもしない限り無さそうだけど、後輩をコーチングする場面は少しずつ出てきたので。