しばやん雑記

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

Azure Resource の Tags を使って Service Discovery を実現するライブラリを作った

こないだ Azure App Service / Functions 向けに App Configuration を使ったシンプルな Service Discovery を作ってみたのですが、 Azure Resource は全て Azure Resource Manager の API を使えば探せます。

そして各 Resource にはタグを設定できるので、こっちの方が優れている気がしたので ARM を利用する Service Discovery を実装しました。

ドキュメントは未整備なので、とりあえず軽く仕組みと使い方を紹介しておきます。

最近実装した SimpleDiscovery に対する拡張して SimpleDiscovery.AzureResourceManager というパッケージを用意しています。SimpleDiscovery については前回のエントリを参照してください。

結局のところ、環境ごとに変わってしまう URL をそのまま扱うのではなく、識別子を使って実行時にサービスの接続先を解決することで、柔軟な依存関係を構築できるようにしたいという目的です。

今回の ARM 拡張では、その識別子に Azure Resource のタグを使ったという話です。

特定のタグが付いている Azure Resource を抽出するためには、Resource Graph と Kusto を使ってクエリを書きます。前回 Resource Graph で遊んだのはこれを実装していたからです。

実装の解説はこれぐらいにして、実際に使ってみます。現在の制約としては、サービスの接続先は App Service (Web Apps / Functions) かつ HTTPS を受け付けるようになっている必要があります。*1

サンプルとしてフロントエンドとなる ASP.NET Core MVC アプリケーションと、サービス API を提供している Azure Function という構成を用意します。この例では呼び出しは一方通行です。

呼び出される Azure Function を用意

先に API として Function を用意しておきました。ほぼテンプレートのままで、API 名だけ変更しています。

public static class SayHello
{
    [FunctionName("SayHello")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        string name = req.Query["name"];

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        name = name ?? data?.name;

        return name != null
            ? (ActionResult)new OkObjectResult($"Hello, {name}")
            : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
    }
}

適当な Resource Group に Azure Function を新しくデプロイして、作成した Function App もデプロイするわけですが、忘れないようにタグを追加しておきます。

タグ名は今のバージョンでは Registry となっていますが、微妙な気がしてきたので正式版までに変える気がします。値には API のサービス名を設定しておきます。

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

この辺りは ARM Template を使ったデプロイを行えば、タグの追加も行えるので楽です。Azure Function 側の設定はこれでほぼ終わりです。

呼び出し側 Core MVC アプリケーションの用意

まずはサービスを呼び出す Core MVC 側に SimpleDiscovery をインストールします。

Install-Package SimpleDiscovery.AzureResourceManager -Pre

インストールが済んだら Startup で DI に登録するコードを追加します。Managed Identity を Azure 上では使うので、基本的にコード内での設定は不要ですが、開発環境のために必要な情報を設定可能にしています。

この辺りのオプションは、正式リリースまでには Configure<TOptions> も使えるようにします。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSimpleDiscovery()
                .AddAzureResourceManager();
                // 必要であれば TenantId / SubscriptionId / ResourceGroup の設定をする
                //.AddAzureResourceManager(options =>
                //{
                //    options.TenantId = "<tenant id>";
                //    options.SubscriptionId = "<subscription id>";
                //    options.ResourceGroup = "<resource group>";
                //});

        services.AddHttpClient<DemoService>()
                .WithSimpleDiscovery();

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }
}

後は AddHttpClient<TClient>WithSimpleDiscovery を呼び出して、サービス名で接続先を解決するように設定します。設定はこれで終わりです。

登録した DemoService の実装は HttpClient を使って、Function App に実装されている /api/SayHello にリクエストを投げているだけなので省略します。

サービスを使う側ですが、コントローラを少し弄って POST で渡された名前を、そのまま DemoService の呼び出しに使うようにしたという、非常に簡単なコードです。

public class HomeController : Controller
{
    public HomeController(DemoService demoService)
    {
        _demoService = demoService;
    }

    private readonly DemoService _demoService;

    public IActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public async Task<IActionResult> Index(string name)
    {
        var result = await _demoService.SayHelloAsync(name);

        ViewBag.ApiResult = result;

        return View();
    }
}

後はビューを用意しましたが、本質的な部分ではないので省略します。今回の重要なポイントは ARM の情報を使って接続先を動的に解決して、ちゃんと通信が確立できるかという部分です。

呼び出し側のアプリケーションはこれで完成したので、既に Azure にデプロイされているサービスを使ってローカルからテストしてみたのが以下の図です。

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

意図したとおりに Managed Identity を使って ARM から接続先のサービスを解決できています。

動作は問題なかったので、このアプリケーションも Azure にデプロイしますが、当然ながら Managed Identity の設定が必要になるのでそこだけは注意が必要です。

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

アクセス先となる Function App に対して個別に IAM を設定しても良いですし、リソースグループに対して付けても良い気がします。複数のサービスがある場合には共通の User Assigned Managed Identity を作成して、それを使いまわした方が便利でしょう。

Managed Identity を設定済みの Web App にデプロイすると、ローカルと同じようにすんなり動きます。

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

これで環境が変わったとしても適切なタグが設定されていれば、サービスの URL を気にせずに扱えるようになりました。ちなみに同名のサービスが複数見つかった場合は、ランダムでどちらかが選ばれます。

更に Application Insights を ASP.NET Core アプリと Azure Functions で共通化しておけば、分散トレーシングもちゃんと動作するので共通化はお勧めしています。

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

ネットワーク周りの課題に関しては VNET Integration と Service Endpoint、そして Azure Functions の Premium Plan でほぼ解決しそうです。

これで Azure Serverless を使って Microservices Architecture を実現し、更に ARM Template で展開まで出来そうな感じがしてきました。暇な時に大きめのサンプルを書いてみたいところです。

*1:普通に App Service を作るとデフォルトで HTTPS が使えるので、特に気にする必要はないです。