しばやん雑記

ASP.NET Core とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure App Service 向けに簡単な Service Discovery を提供するライブラリを作った

Azure App Service や Azure Functions でアプリケーションを書いている時に、別でホストされている API を叩きたい時にはデプロイされた環境単位で向き先を変える必要がありますが、大抵のケースでは App Settings に入れることになるので設定が面倒になっていました。

アプリが 1 つならまだ我慢できそうですが、複数の API を呼び出す場合などは大体破綻します。

そこでこれまで App Settings に追加していた設定をプレビュー中の Azure App Configuration に入れると、中央集権的に管理できるので楽になります。

そもそもコンテナーを使っている場合は DNS を使ってサービス名で簡単に参照できるので、App Service でも同じようにサービス名でアクセス出来るようにしたいと思っていました。

というような話を六本木でしていたところ HttpClientFactory と組み合わせると、もっとシンプルかつ使いやすく出来るのではないかと思ったのでライブラリとして作ってみました。

これは俗にいう Service Discovery だなと思ったので、雑に名前を付けました。

ただし App Service や Azure Functions をターゲットにしているので、Self-registration 周りの機能は用意しませんでした。App Service には既に LB とインスタンス管理が付いているので、デプロイ時に登録すれば変更することはまずありません。*1

Service Registry としては App Configuration をそのまま使うようにしていますが、DI で拡張可能なので Key Vault や他のストアを追加することも出来ます。

特徴としては HttpClientFactory に乗っかるように作っているので、Polly や Refit と組み合わせて利用できます。この辺りは公式ドキュメントでも紹介されています。

Retry と Circuit Breaker は必要な機能なので、HttpClientFactory に乗っかっておいたのは正解でした。

とりあえず前置きが長くなりすぎたので、簡単に使い方を紹介しておきます。

基本的な使い方

最初に App Configuration をデプロイして Services:<ServiceName> というキー名で API エンドポイントを登録します。必要であれば環境名でラベルを追加しても良いです。

f:id:shiba-yan:20190708002223p:plain

ここで指定したサービス名は HttpClientFactory で使います。

Azure Functions を例に説明します。普通に HttpClientFactory を使う時と同じコードを書いていきますが、利用する Service Registry の登録と Service Discovery を利用することを宣言します。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        // App Configuration を Service Registry として利用する
        builder.Services
               .AddSimpleDiscovery()
               .AddAzureAppConfiguration(Environment.GetEnvironmentVariable("ConnectionStrings:AppConfig"));

        // Buchizo という名前のサービスを Service Registry から参照する
        builder.Services
               .AddHttpClient("Buchizo")
               .WithSimpleDiscovery();
    }
}

この設定で App Configuration から API エンドポイントを解決するようになります。

設定さえ終わってしまえば、使う側は同じです。普通に HttpClientFactory を使う時と同じですが、サービス名を指定して CreateClient を呼び出す必要があります。*2

public class Function1
{
    public Function1(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    private readonly IHttpClientFactory _httpClientFactory;

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        var httpClient = _httpClientFactory.CreateClient("Buchizo");

        // BaseAddress が設定済みなのでホスト名は不要
        var response = await httpClient.GetStringAsync("/");

        return new OkObjectResult(response);
    }
}

実際にこの Function を実行すると、ちゃんと App Configuration で設定した通りのエンドポイントに対してリクエストが実行されます。

良い API が思い浮かばなかったので、適当にブチザッキを叩くようにしています。

f:id:shiba-yan:20190708003640p:plain

利用する側でサービス名を文字列で指定するのは残念ですが、Typed Client を使うと上手く解決できます。

Typed Client と組み合わせる

これも特に難しい事はないですが AddHttpClient<TClient> を使うように変更するだけです。それ以外の設定は先ほどとほぼ同じですが、サービス名の扱いだけは注意します。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services
               .AddSimpleDiscovery()
               .AddAzureAppConfiguration(Environment.GetEnvironmentVariable("ConnectionStrings:AppConfig"));

        // Typed Client を使うとクラス名がサービス名になるのでオーバーライドする
        builder.Services
               .AddHttpClient<BuchizoService>()
               .WithSimpleDiscovery("Buchizo");
    }
}

public class BuchizoService
{
    public BuchizoService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    private readonly HttpClient _httpClient;

    public Task<string> GetAsync(string relativePath)
    {
        return _httpClient.GetStringAsync(relativePath);
    }
}

これでコンストラクタで Typed Client を受け取れるようになっているので、後は適当に使います。

App Configuration のオプション変更

全体的に App Configuration に乗っかっているので、オプションで LabelFilter を指定したり、変更があった場合に自動で読み込み直すことも可能です。

ちなみに AddAzureAppConfiguration で App Configuration のオプションを設定できます。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services
               .AddSimpleDiscovery()
               .AddAzureAppConfiguration(options =>
               {
                   // ラベルが Test のものを使う
                   options.Connect(Environment.GetEnvironmentVariable("ConnectionStrings:AppConfig"))
                          .Use(KeyFilter.Any, "Test");
               });

        builder.Services
               .AddHttpClient("Buchizo")
               .WithSimpleDiscovery();
    }
}

f:id:shiba-yan:20190708020857p:plain

上の例のように設定すると、接続先が Test というラベルが付いている方に切り替わります。

今回は接続文字列を使いましたが、App Service 上では Managed Identities を使うようにすると、個別に管理しないといけない項目がさらに減って幸せになれます。

*1:ARM Template で App Configuration に追加できれば凄く便利になりそう

*2:自動で HttpClient を登録しても良い気がしてきた