しばやん雑記

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

Durable Functions のエラーハンドリングと向き合う

Durable Functions は async / await で複雑なフローを簡単に書けるので便利ですが、エラーハンドリング周りに少し癖があるので基本的な処理方法をまとめておきます。

ドキュメントはありますが、基本的すぎることしか書いてないです。

以下の内容は Durable Functions v1.8 以降を対象としているので、これ以前のバージョンでは意図したとおりに動かなかったり、インターフェースが実装されていなかったりします。

例外周りの挙動が広い範囲で直っているので v1.8 へのアップデートは割とお勧めです。v1.x 系リリースはこれで終わりで、次は v2 になるらしいです。

今回の内容を要約すると以下のような感じです。

  • Activity では普通の例外として扱える
  • Orchestrator では FunctionFailedException にラップされる
  • RetryOptions#Handle を上手く利用する
  • Orchestrator 自体の失敗は ILifeCycleNotificationHelper でキャッチする

サンプルコードではとりあえず Exception で処理してることが多いので、本番向けではもっと例外の種類によって処理を変えていこうよ、という話でもあります。

基本的なエラーハンドリング

Durable Functions の中でも Activity は普通のメソッドとして実行されるので、普通の方法で例外をハンドリングできます。catch 後は再 throw しないと Activity が成功扱いになるので注意。

[FunctionName("TestActivity")]
public static async Task TestActivity([ActivityTrigger] DurableActivityContext context, ILogger log)
{
    try
    {
        // 例外が起きるかもしれない処理
        await _httpClient.GetAsync("https://blog.azure.moe/");
    }
    catch (HttpRequestException ex)
    {
        // 普通に try~catch で特定の例外を処理できる
        log.LogError(ex.Message);

        throw;
    }
}

実行してみても、特に変わったことなく意図したとおりの挙動となります。

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

ただし Orchestrator では扱いが変わってきます。v1.x の場合は Activity で投げられた例外は Orchestrator では FunctionFailedException にラップされた形で投げられるので、実際の例外を調べるには InnerException を見る必要があります。

C# 6 からは例外フィルターが使えるので、以下のようにシンプルに書くことが出来ます。

[FunctionName("TestOrchestrator")]
public static async Task TestOrchestrator([OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
{
    try
    {
        await context.CallActivityAsync("TestActivity", null);
    }
    catch (FunctionFailedException ex) when (ex.InnerException is HttpRequestException httpRequestException)
    {
        // FunctionFailedException で wrap されているので when で InnerException まで確認する
        log.LogError(httpRequestException.Message);

        throw;
    }
}

ちゃんと例外フィルターが有効になっているのでハンドリング出来ています。

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

これも例によって catch 後に再 throw しないと成功扱いになるので注意が必要です。Durable Functions の Orchestrator を失敗扱いにするためには、例外を投げるしか今のところ方法がないです。

そして Sub Orchestrator を使った場合にはまた挙動が変わってきます。Orchestrator が入れ子になっているので、例外も FunctionFailedException が入れ子になってやってきます。

[FunctionName("TestSubOrchestrator")]
public static async Task TestSubOrchestrator([OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
{
    try
    {
        await context.CallSubOrchestratorAsync("TestOrchestrator", null);
    }
    catch (FunctionFailedException ex)
    {
        // Sub Orchestrator の場合は FunctionFailedException が入れ子になる
        var innerException = (FunctionFailedException)ex.InnerException;

        // 2 つ辿れば Activity で発生した例外になる
        var httpRequestException = (HttpRequestException)innerException.InnerException;

        log.LogError(httpRequestException.Message);

        throw;
    }
}

実行すると FunctionFailedException を 2 つ辿ることで、実際に投げられた例外を確認できています。

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

少し複雑ですが、v1.x 系ではこのように処理するしかないです。v2 からは FunctionFailedException でのラップが削除される予定なので、もっとシンプルに書けるようになるはずです。

リトライとエラーハンドリング

Durable Functions は組み込みで Activity と Sub Orchestrator のリトライ機能を持っているので、専用のメソッドを使って実行すれば以下のようにシンプルな形で書くことが出来ます。

[FunctionName("TestOrchestrator")]
public static async Task TestOrchestrator([OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
{
    try
    {
        // 5 秒間隔で 3 回までリトライする
        await context.CallActivityWithRetryAsync("TestActivity", new RetryOptions(TimeSpan.FromSeconds(5), 3), null);
    }
    catch (FunctionFailedException ex) when (ex.InnerException is HttpRequestException httpRequestException)
    {
        log.LogError(httpRequestException.Message);

        throw;
    }
}

[FunctionName("TestActivity")]
public static async Task TestActivity([ActivityTrigger] DurableActivityContext context, ILogger log)
{
    // 例外が起きるかもしれない処理
    var response = await _httpClient.GetAsync("https://httpstat.us/500");

    response.EnsureSuccessStatusCode();
}

上の例では Activity が失敗すると 5 秒開けて 3 回までリトライを行います。リトライに失敗すると、最後に発生した例外が自動的に投げられるので try~catch で簡単に処理できるのは同じです。

リトライを行うか制御する

何回か実行して成功する可能性のあるネットワーク系などの一時的なエラーとかであれば一律のリトライで問題ないですが、中にはリトライしても絶対に成功しない系のエラーもあります。

Durable Functions は RetryOptions#Handle を使うことで、特定の例外の場合のみリトライを行うように指定できますが、v1.8 より前のバージョンでは InnerException が常に Exception 型になってしまう問題があったので、簡単に書けませんでした。

今回は 50x 系の HTTP ステータスコードが返ってきたときはリトライを行いつつ、40x 系の場合はそもそもリクエストがおかしいとして即失敗にする例を用意しました。

[FunctionName("TestOrchestrator")]
public static async Task TestOrchestrator([OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
{
    try
    {
        await context.CallActivityWithRetryAsync("TestActivity", new RetryOptions(TimeSpan.FromSeconds(5), 3)
        {
            // 特定の例外の場合だけリトライして、それ以外は即失敗にする
            Handle = ex => ex.InnerException is RetriableException
        }, null);
    }
    catch (FunctionFailedException ex) when (ex.InnerException is RetriableException)
    {
        log.LogError("リトライしたけどダメだった");

        throw;
    }
    catch (FunctionFailedException)
    {
        log.LogError("リトライ不可能なエラーだった");

        throw;
    }
}

[FunctionName("TestActivity")]
public static async Task TestActivity([ActivityTrigger] DurableActivityContext context, ILogger log)
{
    // 例外が起きるかもしれない処理(今回は常に 500 を返すように)
    var response = await _httpClient.GetAsync("https://httpstat.us/500");

    if (!response.IsSuccessStatusCode)
    {
        // 5xx の場合は一時的な問題としてリトライしたい
        if ((int)response.StatusCode >= 500)
        {
            throw new RetriableException();
        }

        // それ以外の場合はリトライせず失敗にしたい
        response.EnsureSuccessStatusCode();
    }
}

リトライをするべきか伝える例外を用意して判別に使います。こういう時のテストには httpstat.us を使うと任意のステータスコードを返すことが出来るので便利です。

500 エラーを返すようにして実行すると、指定した回数リトライされた後に例外が投げられて Orchestrator が失敗します。テストなので成功することはないので、リトライが行われていれば確認としては OK です。

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

次に 400 エラーを返すようにして再実行すると、リトライが行われることなく即座に Orchestrator が失敗します。無駄なリトライを行うことなくすぐに失敗するので待たされることはありません。

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

今回は専用の例外を用意しましたが、例外の型や例外が持つプロパティで判別できる場合はそれで行えば良いです。Cosmos DB や ARM API などではスロットリング発生時のみリトライするなどが書けるはずです。

オーケストレーターの失敗を検知する

前にオーケストレーターの失敗を検知するための方法をいくつか考えて紹介しましたが、v1.8 からは LifeCycle Event 周りを拡張して独自の通知処理を組み込めるようになりました。

なので ETW Listener や Event Grid を用意しなくても使えます。

利用方法はシンプルに ILifeCycleNotificationHelper を実装したクラスを用意して、host.json に追加するだけです。適当に以下のような実装を用意しておきました。

public class InProcLifeCycleNotificationHelper : ILifeCycleNotificationHelper
{
    public Task OrchestratorStartingAsync(string hubName, string functionName, string instanceId, bool isReplay)
    {
        return Task.CompletedTask;
    }

    public Task OrchestratorCompletedAsync(string hubName, string functionName, string instanceId, bool continuedAsNew, bool isReplay)
    {
        return Task.CompletedTask;
    }

    public Task OrchestratorFailedAsync(string hubName, string functionName, string instanceId, string reason, bool isReplay)
    {
        return Task.CompletedTask;
    }

    public Task OrchestratorTerminatedAsync(string hubName, string functionName, string instanceId, string reason)
    {
        return Task.CompletedTask;
    }
}

ちなみに OrchestratorTerminatedAsync は不具合で今は発火しないようです。*1

host.json には CustomLifeCycleNotificationHelperType に用意した型とアセンブリ名を指定します。この辺りは v2 で大きく書き方が変わってくると思いますが、v1 ではこんな感じです。

{
  "version": "2.0",
  "extensions": {
    "durableTask": {
      "CustomLifeCycleNotificationHelperType": "FunctionApp47.InProcLifeCycleNotificationHelper, FunctionApp47"
    }
  }
}

実行して適当に Orchestrator を起動すると該当するメソッドが呼び出されます。以下は失敗した場合です。

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

これで Orchestrator が失敗した場合に Webhook などで通知できるようになります。失敗した理由も取れるので、情報としてはまあまあ足りると思います。

これでようやく Let's Encrypt 証明書の発行失敗時に通知する機能が実装出来ます。

*1:多分未実装な気がする

Azure App Configuration を一通り試したのでメモ

今朝出てきた Azure の新しいアプリケーション向けサービスの Azure App Configuration が、今持ってる課題をいい感じに解決してくれそうなので、プレビューのうちに一通り触っておきました。

普通は ASP.NET Core の Configuration Provider や ASP.NET の Configuration Builder を使って触るはずなので、基本的な部分はドキュメントだけで十分です。なので特に触れません。

REST API のドキュメントは GitHub に用意されていました。Provider が叩いてる API なので、よくわからない設定や挙動があった場合はこっちを読めば大体解決します。Consistency Model の話は興味深いですね。

App Configuration で解決できそうな課題として、複数の App Service や Azure Functions を管理している時に App Settings がバラバラになって事故りやすいという問題です。特に複数リージョンにデプロイしている場合や、本番とステージングなど複数持っている場合などでは簡単に事故ります。

これまでも Key Vault を使うことで同じように解決できますが、Key Vault はもっと堅いイメージがあります。Managed Identities との組み合わせで、Key Vault の secret を使う機会は減りそうな予感がします。

昔に Azure Storage を使う Configuration Provider を書いたことがありましたが、完全に App Configuration で置き換え可能かつ便利です。余裕で移行する予定です。

App Configuration は単なる Key-Value Store なので、大雑把には Azure Table と変わらない感じもありますが、クエリが充実していて更に履歴も保持してくれるので設定向けに最適化されています。

そのあたりについてはドキュメントに書いてあるので読んでおいてください。

実際にドキュメント通りに動かしてみましたが、当然ながらあっさり動くので特に書くことはないです。

ASP.NET Core の Configuration と同じ構造を持っているので、キーの参照時は以下のように直接でも Section を経由しても扱えます。この辺りもお馴染みですね。

@Configuration["TestApp:Title"]

<hr>

@Configuration.GetSection("TestApp")["Title"]

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

ドキュメントには他にも動的に設定を更新する方法もありましたが、App Configuration が変更があったタイミングで Push してくれるわけではなく、単にクライアント側で Polling してるだけっぽいので使う機会はあまりなさそうです。

ASP.NET Core では IOptionsSnapshot<T> で意識せずに扱えますが、実行中に設定が変わることを考慮して作るより、再起動で変わってくれた方が個人的には好きです。

ここから先は気になった機能について調べたことをメモとして残しておきます。

Azure Portal で使える機能

History

App Configuration に追加したキーは履歴を持ちます。どのくらい持てるのかは調べてないですが、時間ベースでのアクセスも出来るので、それなりの期間保持できるのではないかと思っています。

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

Configuration 系のライブラリを使っている場合は意識しないですが、ETag を使った条件付きアクセスが REST API には実装されています。例によって If-Match / If-None-Match を使う形になります。

Import / Export

地味に欲しかったのが Import / Export ですね。ASP.NET Core プロジェクトに含まれている appsettings.json を読み込ませると、簡単に App Configuration への移行が出来るはずです。

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

地味にいろんなフォーマットに対応しています。properties ファイルは最近見なくなりましたが、Win Forms や WPF で使っていた XML ベースのやつです。

インポートしたいファイルをアップロードすると、Separator や Prefix などを指定できます。

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

Label に関してはいろんな使い道がありそうです。ドキュメントではバージョン番号を設定していたりしますが、リージョン名や環境名*1も良さそうな気がしています。

エクスポートもファイル形式や Separator / Label を指定するだけです。簡単ですね。

Compare Settings

割とあるあるネタだと思うのが Production / Staging / Development で何故か設定されているキーの数や、環境非依存のはずの設定値が異なっていることです。比較するのも割と手間がかかるので、問題が起こるまで放置されがちですし、追加だけされて削除されないのも特徴だと思います。

そして App Configuration では比較する機能が用意されてました。こういうのが欲しかったです。

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

特徴としては別の App Configuration とも比較できる点ですね。Production / Staging などをリソースグループ単位で分けている場合には、App Configuration 自体も分けてデプロイするはずですが、そういった場合でもサクッと差分を確認できます。

ちなみに日付を指定すると、その時点での設定値との比較も出来ます。

クライアントで使える機能

Label Filter

Label の使い道としてバージョン番号を付けるのがドキュメントにありましたが、付けた Label を使うためには App Configuration Provider 側で設定が必要です。

正直メソッド名は分かりにくいですが、Use を使うとフィルタを設定できます。以下の例では Label として 1.0.0 が付いているものだけを拾うことが出来ます。

config.AddAzureAppConfiguration(options =>
{
    options.ConnectionString = settings["ConnectionStrings:AppConfig"];

    // Label = 1.0.0 が付いているものを追加
    options.Use("*", "1.0.0");
});

バージョン番号の場合は、全てのキーに対して同じ Label を用意すると思うので上のコードで問題ないですが、リージョンや環境名の場合には一部の値は共通にしつつ、特定のキーでは別にしたいケースが出てくると思います。そういった場合では少し工夫が必要になります。

例として以下のように Label 無しの共通キーと Label を付けたキーを用意しました。

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

共通キーを使いつつ、特定の Label が付けられたキーも使う場合には Use を複数回呼び出すことで実現できます。呼び出す順番が重要なので、そこだけは注意が必要です。

config.AddAzureAppConfiguration(options =>
{
    options.ConnectionString = settings["ConnectionStrings:AppConfig"];

    // Configuration は後から追加したものが優先
    options.Use("*");
    options.Use("*", "NewVer");
});

これで実行してみると、Label が付けられたキーの値が優先して使われていることが確認できます。

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

キーにリージョンや環境名を入れるのは長くなるし、共通のキーを用意するのも手間がかかってイマイチだと思っていたので、上手く Label を使って解決出来そうです。

Time-Based Access

上で使ったUse メソッドで preferredDateTime を指定すると、その時点での値を取ってくることが出来ます。ただし今のバージョンでは英語 OS 以外だとエラーで動かないと思われます。

config.AddAzureAppConfiguration(options =>
{
    options.ConnectionString = settings["ConnectionStrings:AppConfig"];

    // 現在のバージョン (1.0.0-preview-007830001) ではエラーになって動かない
    options.Use("*", preferredDateTime: new DateTimeOffset(2019, 2, 28, 19, 0, 0, TimeSpan.FromHours(9)));
});

例外の内容からしてカルチャ指定を忘れているっぽいです。報告はしたので GA までには多分直るでしょう。

追記

Slack のチャンネルで報告したら Issue が上がってました。

再現できたらしいので直るはずです。Workaround も公開されたので、一応はこれでエラーを回避できます。

Offline Cache

設定系を外部のサービスに依存した時に問題となるのが、そのサービスが落ちた時です。デフォルトでは起動時に 1 回だけ読み込むのでインスタンスが起動している限りは問題にはならないですが、App Service はちょいちょいインスタンスが変わるので、タイミング次第では起動しなくなります。

GA した時には SLA が提供されると思いますが、サービスが落ちるときは落ちるので本番系では Offline Cache を有効にしておいた方が良さそうです。名前の通り最後に使った設定を、オフラインでも使えるように保存しておいてくれる機能です。

基本は SetOfflineCache を呼ぶだけですが、用意されているのはファイルに保存する OfflineFileCache となります。App Service の場合は OfflineFileCache の設定不要で動いてくれるみたいです。

config.AddAzureAppConfiguration(options =>
{
    options.ConnectionString = settings["ConnectionStrings:AppConfig"];

    // 開発環境以外では Offline Cache を有効化する
    if (!hostingContext.HostingEnvironment.IsDevelopment())
    {
        options.SetOfflineCache(new OfflineFileCache());
    }
});

ローカル開発環境では動いて欲しくないので if で囲んでおきます。

App Service 以外の場合は永続化されたストレージに書き込んでおけば良いです。適当に TEMP 以下に書くようなコードで試してみましたが、保存場所には注意しましょう。

config.AddAzureAppConfiguration(options =>
{
    options.ConnectionString = settings["ConnectionStrings:AppConfig"];

    // 開発環境以外では Offline Cache を有効化する
    if (!hostingContext.HostingEnvironment.IsDevelopment())
    {
        options.SetOfflineCache(new OfflineFileCache(new OfflineFileCacheOptions
        {
            Path = Environment.ExpandEnvironmentVariables(@"%TEMP%\app-config-cache.json")
        }));
    }
});

ちなみに Offline Cache が有効になるのはネットワーク系のエラーの時だけのようです。

手元でネットワーク接続を切った後に実行して、アプリケーションが起動することは確認しました。

Managed Identities

App Configuration で設定周りは楽になったといっても、結局は App Configuration へのアクセス情報を持たないといけないので、App Service の場合は Managed Identities を有効にして使いましょう。

AAD 統合されているわけではなく、アクセスキーを Management API を使って取得してるだけみたいです。

自分の環境では Visual Studio の Azure サービス認証を使ったアクセスはエラーで動かなかったのですが、仕組み上は可能なはずなので自分のアカウントに問題があったのかも知れません。

*1:Production / Staging / Development など

Azure Service Bus のスループットとクォーターを確認した

久し振りに Service Bus について調べる機会があったので、適当にコードを書いてどのくらいのスループットが出るのかと、クォーターで引っかかった後にどうなるかなど、気になっていた挙動も調べました。

Service Bus は Premium で試しています。Standard でもパーティションを設定すれば近いような性能は出るのではないかと思いますが、Service Bus 自体がいいお値段するので確認はしてません。

課金体系がプランごとに異なっているので分かりにくいですが、Premium の場合はユニット数分の時間課金だけとなるみたいです。Standard でパーティション増やして処理するより、Premium 1 つに変えた方が良いパターンもありそうです。

容量課金はないですが、1 Queue / Topic 単位での上限が存在しています。Standard でパーティションを使った場合と Premium でも上限は 80GB となっています。

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

この辺り Storage Queue は上限がかなり多いので気にすることはなかったですが、Service Bus の場合はデータサイズに気を付けた方が良いかもしれません。

Storage Queue は Base64 されて格納されてましたが、Service Bus はバイナリで直接扱われるので、LZ4 などの高速な圧縮アルゴリズムを使うのも良さそうです。

とりあえず Service Bus の限界にチャレンジするために、適当に Topic と Subscription を作成して、目一杯データを投入してみることにします。

まずはメッセージサイズの上限を確認するために 1MB のペイロードで試しました。

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

メッセージサイズがオーバーしているという例外となりました。Service Bus Premium では 1MB まで対応しているはずですが、ペイロード以外の付随するデータ分も含まれるみたいなので、UserProperties とか使っている場合ははまるかも知れません。

Service Bus と同じリージョンに VM を作成して、そこから 4KB のデータを投入してみました。データサイズが小さいので送信トラフィックは 130Mbps 前後でした。App Service を使っている場合はプランによって帯域も変わるので、ここも注意するポイントですね。

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

Azure Portal から Service Bus Topic を開くと Active message count などが確認出来ますが、この値は色々試していると秒間のメッセージ数っぽさがありました。

実際に 1000 並列ぐらいでメッセージを送信していたので、数値としてはまあまあ合っています。

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

しばらくすると用意した 80GB を全て使い果たしました。4KB のデータを使ったので約 2000 万メッセージというのも、予想から大きなずれはないですね。

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

用意した容量を全て使い切ると、送信時にクォーターに達したという例外となりました。

ちなみにエラーメッセージからわかるように 80GB というのは 80 * 1024 * 1024 * 1024 B です。

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

この状態からでも問題なく Topic からメッセージの受信は行えますし、受信して余裕が出来れば新しくメッセージを送信することも出来ます。ちゃんと動いてくれてますね。

一応確認しておきたかったのが、新しく Subscription を追加した場合でした。追加したとしても、既存の Subscription に入っているメッセージがコピーされることはないです。

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

メッセージを新しく送信すると 2 つ Subscription があれば両方ともに追加されるので、1 メッセージで消費する容量は 2 倍になります。当たり前と言えば当たり前の挙動です。

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

思っていたよりも Service Bus Premium は良い性能が出ていました。不安定さもあると少し聞いていましたが、今回のテストではきっちりと約 2000 万メッセージの投入もエラーなく行えました。

TTL も確認しておきたかったのですが高いので止めておきました。他にも autoDeleteOnIdle*1 が気になりましたが、それはまた今後試そうかと思います。

そろそろ Azure Cloud Services の移行先についてひとこと言っておくか

最近はまあまあ Cloud Services (Web Role / Worker Role) からの移行という話を聞くようになってきましたが、大体のケースで Service Fabric への移行を勧められているようです。

正直言ってこの提案は全くの見当外れであるケースが大半でしょう。何故なら現状 Cloud Services を使っているアプリケーションは、そもそもマイクロサービスに適した形になっていないことが多いからです。従って移行先としては最初に App Service が検討されるべきです。

1 VM に 1 アプリケーションという構造でマイクロサービスとか、難易度が高すぎるので当たり前です。提案した人は Cloud Services と Service Fabric の違いを全く理解してないのではないかと思ってしまいます。

既存サービスの Cloud Services からの移行を検討している理由としては、開発やデプロイの負荷を下げることや高速で柔軟なスケーリング、後は VNET 統合やモニタリング改善などいろいろあると思いますが、Service Fabric は最初に検討すべきサービスではありません。

Cloud Services を使う上での課題

Cloud Services はある種のプリミティブな PaaS として、非常にシンプルかつスケーリングを考慮したアーキテクチャになっています。Azure の各種基盤的な部分で使われていて、オーケストレーターやコンテナとかいう話なんて全く無かった頃のサービスです。

PaaS としての思想は非常に良かったのですが、一方で Cloud Services 向けにアプリケーションを開発する場合は専用のライブラリを参照する必要がありました。アプリケーション設定の取得すら ASP.NET の標準仕様に乗っかっていなかったり、やたらと複雑なマニフェストや専用のパッケージを作ったりと、アプリケーションのポータビリティが失われていました。*1

無制限にスケーリングは可能でしたが VM の起動コストという問題も抱えています。しかし、それらの問題は App Service を採用することで、標準機能だけでほぼ解消可能です。

Service Fabric が向いていない理由

それに比べて Service Fabric を採用するということは、既存のアプリケーションにかなりの複雑さを持ち込むという結果になるでしょう。Docker を使ったマイクロサービスを既に開発している場合は、現在では AKS などの Managed Kubernetes がデファクトスタンダードとなっていますし、Service Fabric で Docker を使うと Reliable Services / Reliable Actors が使えなくなります。

単なるオーケストレーターとして Service Fabric を使うのは、あまりにもメリットが少ないです。そもそも IIS が必要な ASP.NET Web Forms / MVC は Docker しか選択肢がないです。

Docker しか選択肢が無いということは、Windows Containers を使う必要があるということなので、Web App for Containers (Windows) と同じような課題を持つことになります。*2

普通にクラスタを本番向けに作ると、最低でも 5 インスタンスが必要なのは変わりませんし、スケーリングの際は結局 VM を新しく作成するので Cloud Services とほぼ変わらないでしょう。

Service Fabric 自体が悪いというわけではなく、単純に Cloud Services からの移行先として不適切というだけです。Azure の SQL Database や Cosmos DB などのサービス基盤として Service Fabric は使われていますし、最近は Azure Functions の Linux 向け Consumption Plan が Service Fabric Mesh 上に実装されました。

今後は App Service の基盤が Cloud Services から Service Fabric Mesh などに変わって、特に意識することなく Service Fabric を使うようになる未来になるかもしれません。そういう形で良いです。

Web Role / Worker Role の移行に必要なこと

Web Role は単純な ASP.NET アプリケーションに改善すれば良いですが、Worker Role は仕組み上無限ループで処理する形だったので、まずは処理を整理してイベントドリブンに書き換えた方が良いでしょう。なので検討する移行先は Azure Functions となります。

Worker Role でよくある処理は Storage Queue からメッセージを無限ループで取得することだと思いますが、Azure Functions では QueueTrigger を使えば終わりますし、Consumption Plan を使えば Queue の長さによって自動でスケーリングしてくれます。

複雑な処理の場合は Durable Functions を使えば、Worker Role と Storage Queue を組み合わせて作っていたような処理も、処理が分かりやすい形かつスケーラブルに実装できるはずです。

現在の App Service で実現できない処理としては GDI / GDI+ を使った画像周りです。App Service の Sandbox によって API がブロックされるので、API を使っていると失敗します。

それ以外にも細かい制約はありますが、Web アプリケーションの実行環境としては影響は小さいです。

現在 Public Preview 中の Web App for Containers (Windows) を使うと GDI / GDI+ などの制約なしに扱えるようになるので、必要な部分だけ適用するなどアーキテクチャを少し変えるだけで、Cloud Services から App Service への移行が行えるかと思います。

実際に Cloud Services からの移行で Service Fabric を勧められていて困っている場合などは、お気軽にご相談ください。App Service への移行のお手伝いとかできます。

*1:ありていに言うとベンダーロックインだった

*2:Server Core / Nano Server ベースイメージのキャッシュや更新など

Web App for Containers と ACR Tasks が Windows Server 2019 にアップデートされていた

久し振りに Web App for Containers (Windows) を作成してみたら、ホスト OS が Windows Server 2019 にアップデートされてることに気が付きました。

正式リリースではないですがアップデートはほぼ確実でしょう。これで大幅にサイズが削減された Docker Image を使えるようになりそうです。

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

確認出来たのは West US の Premium Container だったので、、Windows Containers を現実的な時間で扱うためと、パブリックプレビュー中につき互換性問題が発生しないので早々にアップデートしたのでしょう。

前に書いた通り、Windows Server 2019 の Docker Image は 2016 と比較して 1/3 ぐらいになっています。

ただし今のところはベースとなる Server Core や Nano Server のイメージはキャッシュされていないようでした。GA までには改善されるのではないかと思っていますが、キャッシュされていたとしても Deployment Slot を使ったウォームアップは必須でしょう。

前に試した時には Hyper-V Containers なのか裏を取れてなかったですが、今回 2016 版の Nano Server + IIS なイメージをデプロイして動作を確認したので Hyper-V Containers で動いてると確認出来ました。

Process Isolation と異なりメモリ使用量が多いので、PC2 だと割とギリギリなケースが多そうです。実際に 4 サイト分を立ち上げたら、メモリ不足で最後のコンテナが上がってこなくなりました。

とりあえず実際に Windows Server 2019 ベースのイメージをデプロイして試しました。分かりやすくて手っ取り早いので、大体確認には IIS の Docker Image を使うようにしています。

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

IIS のいつもの画面を見せてもしょうがないので、Docker のログで LTSC 2019 でもコンテナが立ち上がってることを確認しました。Server Core のイメージがキャッシュされていないので時間はかかりましたが、それでも LTSC 2016 と比べると圧倒的に速いです。

今は Azure Portal 側のバリデーションが 2019 のイメージに対応していないので、Portal から 2019 のイメージを設定しようとするとエラーになります。ARM Explorer や Azure CLI からなら回避できるはずです。

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

Web App for Containers の Windows Server 2019 化によって実行環境は整いそうなので Docker Image のビルド周りを調べてみると、ACR Tasks がひっそりと Windows Server 2019 にアップデートされたようです。

これも実際に LTSC 2019 のイメージを使うように変更してビルドさせると、問題なく終わりました。

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

以前に調べた時は Version 1803 だったので、ACR Tasks は割と対応が早い方のようです。

ACR Tasks は Hyper-V Containers で動いていることを以前に確認していたので、もちろん LTSC 2019 より古いイメージも扱えるので現時点では最強感があります。

残念ながら Azure DevOps や AppVeyor などは 2019 にアップデートされていません。気長に待ちましょう。

今回は Web App for Containers (Windows) だけのアップデートでしたが、順当にいけば 2016 のアップデートの時のように、特定のリージョンから通常の App Service も 2019 になっていくのだと思います。

Azure CLI と Key Vault を使って自己署名証明書を作成する

たまに検証とかで必要になるので自己署名証明書を作ることがあるのですが、コマンドを毎回覚えていられないので最近は Azure Key Vault で作ってしまうことが多いです。

Azure Portal から作るのは少し面倒なのと、内部で使われている API バージョンが古くて新しい機能に対応していないみたいなので、Azure CLI で作る方法を試しました。

Cloud Shell で行えば Azure CLI のセットアップも不要なので、簡単に試すことが出来ます。ファイルのアップロードとダウンロードも行えるので作成した証明書や鍵のやり取りも楽です。

自己署名証明書を作成する

Azure Portal からは簡単に作れますが、Azure CLI の場合は先にポリシーを記述した JSON を用意する必要があります。ポリシーのテンプレートは get-default-policy を叩けば生成できます。

# デフォルトのポリシーをファイルに書き出す
az keyvault certificate get-default-policy > policy.json

以下のように見覚えのある形式で JSON が出力されるので、TLS 向けなら最低限 subjectsubjectAlternativeNames を追加しておきます。

ちなみに Azure Portal からは EC を使った証明書を作成できませんが、Azure CLI なら作れます。*1

{
  "issuerParameters": {
    "certificateTransparency": null,
    "name": "Self"
  },
  "keyProperties": {
    "curve": "P-256",
    "exportable": true,
    "keySize": null,
    "keyType": "EC",
    "reuseKey": true
  },
  "lifetimeActions": [
    {
      "action": {
        "actionType": "AutoRenew"
      },
      "trigger": {
        "daysBeforeExpiry": 90
      }
    }
  ],
  "secretProperties": {
    "contentType": "application/x-pkcs12"
  },
  "x509CertificateProperties": {
    "keyUsage": [
      "digitalSignature"
    ],
    "subject": "CN=daruyanagi.com",
    "subjectAlternativeNames": {
      "dnsNames": [
      	"daruyanagi.com"
      ]
    },
    "validityInMonths": 12
  }
}

上の例では EC P-256 を使った証明書を作成していますが、以下のように keyProperties を変更すると RSA で作成できます。キーのサイズは RSA で使える適切なものを選べばよいです。

  "keyProperties": {
    "curve": null,
    "exportable": true,
    "keySize": 2048,
    "keyType": "RSA",
    "reuseKey": true
  },

他にも keyType には EC-HSM と RSA-HSM が選べそうですが、何が異なるのかは試してはいないです。大体のケースでは EC と RSA で問題なさそうです。

修正したポリシー JSON を使って、以下のコマンドで実際に自己署名証明書を作成します。

# mykeyvault に example という名前で証明書を作成する
az keyvault certificate create --name example --vault-name mykeyvault --policy @policy.json

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

少し待つと作成が完了するので、後は秘密鍵と証明書をダウンロードしたり、Key Vault に対応したサービスから利用したりできます。ポリシーは少し分かりにくいですが、TLS 向けならほぼテンプレで行けます。

証明書のダウンロード

先ほど作成した証明書は download コマンドを使うと PEM か DER のどちらか指定した形式でダウンロードできます。簡単すぎるので特に説明は不要ないと思います。

# PEM 形式でエクスポート
az keyvault certificate download --name example --vault-name mykeyvault --encoding PEM --file example.crt

# DER 形式でエクスポート
az keyvault certificate download --name example --vault-name mykeyvault --encoding DER --file example.crt

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

OpenSSL を使って中身の確認をしておきました。ちゃんと EC P-256 で作成されています。

PFX のダウンロード

最後は秘密鍵を含んだ形でダウンロードします。PFX でダウンロードする certificate のコマンドは用意されていないですが、Key Vault で作成した秘密鍵は secret 扱いで保存されているのでダウンロード出来ます。

以下の Issue に Workaround としてダウンロード方法が紹介されています。

Key Vault の secret には PKCS#12 として保存されているので、そのままダウンロードして拡張子を付ければ Windows からも扱える形式になります。

そのままだと Base64 でエンコードされた状態なので、encoding を指定してバイナリとして保存します。

# PKCS#12 形式でエクスポート
az keyvault secret download --name example --vault-name mykeyvault --encoding base64 --file example.pfx

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

エクスポートされた PFX はパスワード無しなので扱いには気を付けましょう。

Windows を使って PFX の中身を一応確認しておきました。ちゃんと秘密鍵が含まれています。

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

手動で作成するのと異なり、Key Vault で作ると自動で更新もしてくれるので結構便利です。ポリシーをちゃんと書けば他の Issuer に対しても証明書を発行できるはずです。

もう少し Key Vault に対応したサービスが増えるともっと便利になるはずです。*2

*1:ただし Azure Portal 上での表示には未対応、今は App Service へのインポートも出来ない

*2:例えば Application Gateway とか CDN のこと

Durable Functions のオーケストレーターが失敗したことをキャッチしたい

Durable Functions ではアクティビティが失敗した場合にはオーケストレーターで例外が投げられるので、try ~ catch を使って簡単にハンドリングしたり、アクティビティの実行に CallActivityWithRetryAsync を使えばいい感じにリトライもしてくれます。

以下のような簡単なコードで実現可能なのが Durable Functions の魅力の一つですね。

[FunctionName("Function1")]
public static async Task RunOrchestrator([OrchestrationTrigger] DurableOrchestrationContext context)
{
    try
    {
        // 失敗するかもしれない処理
        await context.CallActivityAsync("Function1_PostHttp", "kazuakix");
    }
    catch (Exception ex)
    {
        // 何かする
    }

    // 5 秒間隔で 10 回までリトライする
    await context.CallActivityWithRetryAsync("Function1_PostHttp", new RetryOptions(TimeSpan.FromSeconds(5), 10), "kazuakix");
}

このように Durable Functions を使った処理の流れで回復不能なエラーが発生した場合には、失敗としてオーケストレーターを終わらせたいです。ちなみに明示的にオーケストレーターを失敗させるには、例外を投げるしかありません。*1

割と切実にサクッとオーケストレーター自体が失敗したことをキャッチする方法で悩んだので、いろんな方法を調べた結果をまとめておきます。

全体を try ~ catch で囲む

これはアンチパターンという感じですが、単純なオーケストレーターなら許せるかなという気がします。

[FunctionName("Function1")]
public static async Task RunOrchestrator([OrchestrationTrigger] DurableOrchestrationContext context)
{
    try
    {
        // 失敗するかもしれない処理
        await context.CallActivityAsync("Function1_PostHttp", "kazuakix");

        await context.CallActivityAsync("Function1_PostHttp", "daruyanagi");
    }
    catch (Exception ex)
    {
        // 失敗したので何かする

        // オーケストレーターが失敗した扱いにするために再スロー
        throw;
    }
}

このくらい単純なら良いですけど、複雑なオーケストレーターになると現実的ではないです。catch した後に再スローしないとオーケストレーターが成功扱いになるので、Application Insights などで見た時に詰みます。

残念なコードですが、一応は手っ取り早く目的を達成できます。

Application Insights を使う

ちゃんとオーケストレーターを失敗扱いにしておくと Application Insights から簡単に追跡できます。Search から Severity level を Error でフィルタすると、失敗したログが流れてきているのを確認出来ます。

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

ログは送信されているので、後はアラートを用意するだけです。新しい Azure Monitor の Alert は Classic Alert より格段に使いやすくなっているので、設定も分かりやすいです。

Metrics を選択すると、Azure Functions の場合は Function 単位で各メトリクスが存在するので、適当に Failure で絞り込んでオーケストレーターのものを選択すれば良いです。

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

後はアクショングループで Webhook やメール通知の設定などをしておけば、オーケストレーターが失敗するとイベントが送信されます。Application Insights の都合上タイムラグが発生するのと、複数回エラー発生するとグルーピングされるのでそのあたりが許容できる場合は使えます。

Event Grid を使う

他にも Event Grid を使って Durable Functions のイベントを受信する方法がありますが、これは準備に手間がかかりますね。受け取る Function とかも必要なので手軽さはない感じです。

今回のサクッと扱いたいという条件から外れるので特に検証はしてないですが、ドキュメント通りならオーケストレーターが失敗した場合のイベントも飛んでくるはずです。

ETW / EventSource を使う

Durable Functions は ETW / EventSource を使ってイベントを投げてくれているので、それを受け取るようにすればエラーが発生したかどうかを簡単に確認出来ます。

実装としては EventListener を継承したクラスを作成して、有効化するぐらいです。とても簡単。

public class MyEventListener : EventListener
{
    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        var result = new StringBuilder();

        for (int i = 0; i < eventData.Payload.Count; i++)
        {
            result.Append($"{eventData.PayloadNames[i]} = {eventData.Payload[i]}, ");
        }

        Debug.WriteLine($"{eventData.EventName} : {result.ToString(0, result.Length - 2)}");
    }
}

作成した EventListener を有効化するタイミングは少しシビアで、まず WebJobsStartup を使って試したところ読み込みタイミングの問題で EventSource を取得できませんでした。

Azure Functions ではこれ以外に初期化で使えそうなタイミングがないので、仕方なく Function を実装したクラスの static コンストラクタに書きました。

public static class Function1
{
    static Function1()
    {
        var eventSource = EventSource.GetSources()
                                     .FirstOrDefault(x => x.Name == "WebJobs-Extensions-DurableTask");

        if (eventSource != null)
        {
            new MyEventListener().EnableEvents(eventSource, EventLevel.LogAlways);
        }
    }
}

とりあえずは EventLevel をほぼ無視して取得できるように LogAlways を指定しています。エラーのみ欲しい場合は適宜変えると、そのイベントだけ取得出来るので便利です。

動かして見ると、ちゃんとデバッグ出力に ETW 経由で Durable Functions のイベントが取れています。

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

Payload には以下のようなデータが入っているので、オーケストレーター自体が失敗したのか、それともアクティビティが失敗したのかという判別も出来ます。

Azure 上で実行すると AppNameSlotName も入ってくると思われます。

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

例えばオーケストレーターのエラーだけを受け取りたい場合には EventLevelError に変更しつつ、以下のように EventListener を用意すれば良いです。

public class MyEventListener : EventListener
{
    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        var payload = new Dictionary<string, object>();

        for (int i = 0; i < eventData.Payload.Count; i++)
        {
            payload[eventData.PayloadNames[i]] = eventData.Payload[i];
        }

        if ((string)payload["FunctionType"] != "Orchestrator")
        {
            return;
        }

        Debug.WriteLine($"Orchestrator Failed : {payload["FunctionName"]}");
    }
}

組み込みで ETW を使ったイベントを提供してくれているのは便利ですね。今回はエラーのキャッチが目的でしたが、ETW を使えばオーケストレーターが成功した時のイベントもちゃんと取れます。

*1:エラー時にはちゃんと失敗させないと Application Insights などに影響が出る

Azure SignalR Service の Service Mode 設定による違い

Azure SignalR Service を仕事で少し使ったときに、設定が増えていることに気が付いたので調べたのですが、中の人に聞くまで情報がほぼ無かったのでメモとして残します。そして Serverless mode がかなり良かったので、それの紹介も簡単にですがします。

チャックさん曰く、ちょっと前から Service mode を選べるようになっていたらしいです。

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

Azure Portal には最小限の説明しかなかったので、具体的にどのように変わるのか分かりませんでした。ドキュメントも無いみたいでしたし、とりあえず Default で良いかなという気分がしてくる設定です。

バックエンドが必要ない場合には Serverless を選択すればよいと言われても、何かが変わる気がしません。SignalR Service の仕組みは公開されているので、読んでおくとこの先の理解が容易になります。

SignalR Service で指されるサーバーというのは、ASP.NET Core SignalR で実装された Hub を持つアプリケーションのことを指しています。Azure SignalR Internals にもあったように、クライアントとサーバーの両方が SignalR Service に対しての接続を保持します。

サーバー接続が存在する場合には、クライアントからのメッセージは必ずサーバーに実装された Hub を実行し、その結果でメッセージの送信などが行われます。このようにサーバー側で Hub の実装が必要だったのが、これまでの ASP.NET Core SignalR でした。

それに対して Serverless というのはクライアント向けの API だけで動作するモードとなります。この辺りの挙動がいまいち想像つかなかったので、オリジナルの SignalR 開発者の David Fowler 氏に Azure SignalR Service チームの Ken Chen 氏を紹介していただき詳しく教えてもらいました。

要するに Azure SignalR Service はサーバーが落ちていてもクライアントの通信は維持されたままなので、この挙動では障害が発生しても切断やエラーにならないので困るよね、ということです。当たり前ですが、これまでの SignalR ではサーバーが落ちた場合は接続が切れます。

さらに Azure SignalR Service ではクライアントだけでも通信が行えるので、チャットのようにクライアントから双方向で通信する場合には、切断を検知する方法が今までは存在しなかったということになります。

実際に Default に設定した SignalR Service を用意して、サーバーを起動した後に別クライアントの接続を行っている状態で、サーバーを落とすと以下のようにエラーが通知されます。

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

クライアント側では Closed イベントが実行されるので、エラーの通知やリトライを行えば良いです。

そして Serverless に設定した状態でサーバーを起動させると、正常に立ち上がらずにエラーとなります。

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

返ってきたエラーメッセージには Serverless mode ではサーバー接続が行えないと書いてあります。

ここまでに調べた情報をまとめると以下のようになります。分かってしまえば単純です。

  • Default の場合
    • サーバーの接続が切れた場合にクライアントの接続も切断される
    • 実際には SignalR Service からリトライリクエストが投げられる
  • Serverless の場合
    • そもそもサーバーが接続しようとするとエラーを返す
  • Classic の場合
    • これまで通りの挙動。特にエラーになったり、切断されたりしない

現時点では ASP.NET Core SignalR で実装しているアプリケーションの場合は Default を使い、Azure Functions で SignalR Binding を使う場合には Serverless を設定しておけば良い感じです。

SignalR での Hub 実装は割と 1,2 行で終わるケースが多かったので、そういったケースでは SignalR Binding を使うと驚くほど簡単にリアルタイム通信のアプリケーションを実装できるはずです。GitHub にある Serverless Chat のサンプルが非常に良い出来だったので、非常に参考になります。

SignalR Service への接続周りは Azure Functions の SignalR Binding で行えば、クライアント側実装は JavaScript が書ければよいので Azure Storage の Static Website Hosting で十分ということになります。これで Serverless mode と呼ばれている理由が理解できますね。

特に Azure Functions はイベントドリブンなので、SignalR Service とかなり相性が良いです。そしてリアルタイムで状態が変わっていくので Vue.js などと組み合わせると、更に良い感じでアプリを作れるはずです。

Azure Functions v2 でインスタンスメソッドも Function として利用可能に

先週リリースされた Azure Functions Runtime 2.0.12265 からは、インスタンスメソッドでも Function として扱われるようになりました。これまでは静的メソッドしか使えなかったので、モックしにくいとかいろいろ言われていたと記憶しています。

リリースノートにひっそりと載っていますが、おそらく DI 周りサポートに向けて作業の途中なのでしょう。

まだ全てのリージョンにデプロイはされていないみたいですが、来週までには完了しそうです。

昨日ぐらいにようやく Visual Studio 向けにも 2.0.12265 が入った Functon Tools がリリースされたので、実際にローカルで動作を確認しておきました。結論としては DI 周りがサポートされると、これまでの不満点の改善が期待できる感じがしてきました。

限定的に DI が使えるように

これまでの Function は静的メソッド限定だったので、ASP.NET Core では一般的に書いているような DI を使ったコンストラクタへのインスタンスの注入は行えなかったですが、限定的ですが利用出来るようになりました。ただし公式にサポートされた使い方ではないです。

具体的には Azure Functions Runtime 自体が DI に追加したクラスの場合、コンストラクタインジェクションで受け取れるようになっています。

public class Function1
{
    public Function1(HttpClient httpClient)
    {
        // Function Runtime 側が DI に追加しているのでシングルトンな HttpClient インスタンスを受け取れる
        _httpClient = httpClient;
    }

    private readonly HttpClient _httpClient;
}

詳しい方なら WebJobsStartup を使えば DI への追加が行えると思われるでしょうが、Azure Functions Runtime は DI を 2 系統持っているのが問題となります。

ここでは便宜的に WebHost 側と WebJobs 側と呼びますが、WebJobsStartup で渡される IServiceCollection は WebJobs 側の DI になり、実際に Function のクラスが解決される際は WebHost 側の DI が使われるので、現時点では手の出しようがない部分となります。公式にサポートされるのを待つしかないです。

ユーザー側で DI への追加は出来ないですが、試していると IConfiguration は Functions Runtime 側で追加されているので受け取れることが分かりました。テスト用に以下のようなコードを書きました。

public class Function1
{
    public Function1(IConfiguration configuration)
    {
        var options = configuration.GetSection("Kazuakix")
                                   .Get<SampleOptions>();
    }
}

特定のセクションの値を用意しておいたクラスにバインドするという、簡単かつ良くあるコードです。ちなみに ASP.NET Core のお作法的には IOption<T> で受け取りたいところですが、今は無理です。

そして local.settings.json には 2 つほどキーと値を追加しておきました。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "Kazuakix:Age": 50,
    "Kazuakix:Job": "Syachiku"
  }
}

この Functions をデバッグ実行してみると、コンストラクタインジェクションが行われて IConfiguration を取得でき、さらに local.settings.json に追加した値がバインドされたインスタンスを取得できました。

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

Azure Functions v2 では local.settings.json や本番から設定を読み込む手段が公式に提供されていないという謎があったので、以下のエントリのように手動で ConfigurationBuilder を使って読み込んだりしていましたが、DI がサポートされると必要なくなりそうです。

DI が 2 系統に分かれているのは割と混乱とバグを生んでいる感じがするので、将来的には改善されると良いですね。複雑すぎて、正直なところ PR を送る気にはならないのが辛いです。

テスタビリティの改善

静的メソッドで書いていると外部から簡単にモックしたサービスなどのインスタンスを注入できないので、テストガチ勢からは不評だったのですがインスタンスメソッドになったので、この辺りが改善されました。

ただし今はカスタムクラスを DI に追加できないのと、ASP.NET Core の DI は前にも書いたように制約があるので、利用するためには少し工夫する必要があります。

Poor Man's DI あるいは Pure DI のようにコンストラクタのオーバーロードを用意できないので、上のエントリで書いたようにデフォルト値を使ってインスタンスを作成するようにすれば、外部からサービスを注入出来るのでモック周りが楽になります。

public class Function1
{
    public Function1(ISampleService sampleService = null)
    {
        _sampleService = sampleService ?? new DefaultSampleService();
    }

    private readonly ISampleService _sampleService;

    [FunctionName("Function1")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]
        HttpRequest req,
        ILogger log)
    {
        var name = req.Query["name"];

        if (string.IsNullOrEmpty(name))
        {
            return new BadRequestResult();
        }

        return new OkObjectResult(_sampleService.Hello(name));
    }
}

Azure Functions Runtime 上で実行される場合には null になるので、デフォルトの実装が使われます。モックを使う場合にはコンストラクタでインスタンスを渡してあげれば良いので、リフレクションとかこねくり回すより圧倒的にシンプルになります。

Build 2018 か Ignite 2018 のセッションで Azure Functions v2 での DI サポートのデモをやっていたので、将来的には DI が公式に提供されるはずですが、Configuration 周りは今の状態でも使いたくなる感じです。

新しい Azure App Service の VNET Integration を試した

App Service の VNET Integration は Gateway が必要となる Point to Site VPN を使っていましたが、去年にプレビュー公開された新しい VNET Integration は Gateway 無しで VNET に参加できるようになってます。

新しい VNET Integration については公式ブログとブチザッキを参照してください。

これまで不可能だった Service Endpoint を経由した通信も行えるようになります。ExpressRoute は詳しい人がいつかは試してくれるのではないかと思うのと、そもそも環境の用意が無理なので放置します。

今頃になって新しい VNET Integration を触ってみようという気になったのは、公式ブログにこっそりと以下の文言が追加されていたからです。

This feature is in Preview in all public regions.

最初はリージョン限定になっていたはずなので、こっそりと更新するのは止めてほしいですね。

気を取り直して実際に App Service を作成して既存の VNET に参加させるわけですが、利用可能な Scale unit が限定されています。具体的には Premium V2 が使える新しい Scale unit じゃないと有効に出来ません。なので App Service 作成時に Premium V2 を指定して作りましょう。

作成した App Service の VNET Integration 設定画面を開くと、ボタンが増えているので分かりやすいです。

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

Preview の付いている方を選んで VNET と Subnet を指定していくわけですが、例によって App Service 用に新しい Subnet が必要なので適当に作成します。

既に適当に作成しておいたので、既存の Subnet を指定して VNET に追加します。

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

追加後は 1 度 App Service が自動的に再起動されるみたいです。このあたりはまた謎の技術によって実現されているっぽいので実際の仕組みが気になりますが、外から窺い知ることは難しそうです。

たったこれだけで VNET への参加が完了します。一瞬で終わりました。

参加した VNET 内での通信

確認のために別の Subnet 上に IIS が動いている VM を適当に用意したので、作成した App Service をリバースプロキシとして設定して通信が行えているのか確認してみます。この確認方法に特に意図はなくて、単に確認用のアプリを作るのが面倒だっただけです。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="VM" stopProcessing="true">
          <match url="vm/(.*)" />
          <action type="Rewrite" url="http://10.0.0.4/{R:1}" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

DNS 周りに関しては詳しそうなぶちぞう RD に任せるとして、とりあえず Private IP を直指定で行きます。普段は VNET を使う機会が本気でないので、このあたりは良くわかってません。

リバースプロキシを作成したので、ブラウザからアクセスして確認します。ちゃんとページが表示されるので、App Service から VM への通信はちゃんと行えています。

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

ちなみにこの状態でスケールアウトで台数を増やしても問題ないです。ASE みたいに時間がかかるといった罠はないので、GA した後には安心して使うことが出来そうです。

IIS 側ではログを吐くようにしているので、アクセスしてきた IP が確認出来ます。この時は 5 台にスケールアウトしていたので、ちゃんと 5 つの IP からアクセスされたログが残っていました。

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

Scale unit 共通の Outbound IP ではなく、Private IP からのアクセスというのも確認出来ました。この挙動は当然といえますが、Outbound IP は問題になってくることが多いので確認しておいて損はないです。

Service Endpoint 経由での通信

次は Service Endpoint を使った接続を Storage で試しました。これまでの App Service では不可能だったので、一応確認しておこうと思いました。App Service を VNET に入れた時に一番使いたい機能でしょうし。

適当に Storage を作成して、Service Endpoint を有効化しました。App Service が入っている Subnet を追加するだけなので、こっちも設定は一瞬で終わりました。

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

先ほどの VM を使った方法と同じように、Blob へのリバースプロキシを追加して通信が行えるかを確認しました。これもまた一番簡単な確認方法だったからという理由です。

以下のような URL Rewrite ルールを作成して、Blob を読み込むようにしています。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="Storage" stopProcessing="true">
          <match url="blob/(.*)" />
          <action type="Rewrite" url="https://serviceendpointtest1.blob.core.windows.net/images/{R:1}" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

予め Blob には適当な画像をアップロードしておいたので、これもまたブラウザから確認しました。

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

この状態で Service Endpoint から Subnet を削除すると表示出来なくなるので、ちゃんと Service Endpoint 経由で通信出来ていることが確認できます。今回は Storage で試しましたが、Service Endpoint に対応しているサービスなら問題なく使えるはずです。

かなりいい感じに使える新しい VNET Integration ですが、GA は今のところ mid-year of 2019 を目標としているみたいなので、残念ながら少し時間がかかりそうな雰囲気です。