しばやん雑記

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

Azure Pipelines で NuGet パッケージのデプロイを自動化

AppVeyor から Azure Pipelines への移行を見越しつつも、ちゃんと NuGet パッケージの発行周りを検証していなかったので Build と Release のパイプラインを組みました。

AppVeyor での自動化については前回書いたエントリを参照してください。

やっていることは AppVeyor と変わらないですが、Azure Pipelines では少しはまるポイントがあります。NuGet のタスクはありますが、テンプレートはないのである程度は手で組む必要はあります。

今回組んだパイプラインは以下のような動作となります。まあ、よくあるやつです。

  • 通常のコミットはビルド(とテスト)だけ行う
  • GitHub で Release が作られた時にはタグ名で nupkg を作成し、NuGet.org にプッシュ
  • タグ名は v1.0.0 のように v から始まるものだけ許可

タグの有無で動作が変わってくる部分が肝です。AppVeyor ではスクリプトで全て書いていたので if で簡単に分岐しましたが、Azure Pipelines ではそのあたり少し工夫します。

具体的にはビルド(とテスト)用と NuGet リリース用に Build のパイプラインを分けてしまう方法と、タスクの実行条件を使ってスキップさせる方法のどちらかを選ぶことになります。

今回はタスクがシンプルだったので、1 つのパイプラインで実行条件を指定する方法にしました。

Build パイプライン

.NET Standard 向けのライブラリを nupkg としてビルドする場合は、単純に .NET Core CLI を使ってビルドとパッケージングを行ってあげれば良いので、タスクはとても単純になります。

今回は以下のような 4 つのタスクを組み合わせて作成しました。build と pack が分かれているのは、通常のコミットではビルドだけ実行したいからです。

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

dotnet build 以降のタスクには実行条件としてタグの prefix を指定しているので、master などのブランチに対するコミットの場合はスキップされます。

YAML として書き出した場合の全体は以下の通りです。タスクに condition が付いてるのが少し微妙ですが、良い方法が無さそうだったのでこのようにしています。

pool:
  name: Hosted Windows 2019 with VS2019

steps:
- task: DotNetCoreCLI@2
  displayName: 'dotnet build'
  inputs:
    arguments: '--configuration $(BuildConfiguration)'

- powershell: 'echo "##vso[task.setvariable variable=PackageVersion]$($env:Build_SourceBranchName.Substring(1))"'
  displayName: 'PowerShell Script'
  condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))

- task: DotNetCoreCLI@2
  displayName: 'dotnet pack'
  inputs:
    command: pack
    nobuild: true
    versioningScheme: byEnvVar
    versionEnvVar: PackageVersion
    verbosityPack: Normal
  condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact: drop'
  condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))

ちなみに間に挟まっている PowerShell Script はタグ名から v prefix を外すための処理です。色々と調べても文字列操作が出来ないっぽかったので、仕方なく PowerShell を使って書いています。

タグ名からバージョン文字列を変数として用意出来れば、後は dotnet pack に任せます。versionEnvVar に用意した変数名を指定すれば、そのバージョンで nupkg を作ってくれます。

これでタスクはタグの有無で動作が変わってくれるので、後は Trigger としてタグを追加します。

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

YAML を使っている場合はタグ名で Trigger の条件を書けますが、UI からはブランチ名しか指定出来ないので refs/tags/{tagname} というフォーマットで指定します。

これで一通り完成したので、適当にコミットするとビルドだけが走ります。

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

そして GitHub から新しい Release を作成すると、nupkg 作成まで実行されます。

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

このパイプラインで作成された nupkg は Artifact に保存されているので、後は Release パイプラインで NuGet へのプッシュを行ってあげるようにします。

Release パイプライン

NuGet へのプッシュは .NET Core CLI の nuget push ではなく、NuGet タスクの push を使います。.NET Core CLI では暗号化された API Key を扱えないので、エラーとなるためです。

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

作成されたパッケージのプッシュ先は Azure Artifacts だと簡単ですが、NuGet.org や他のリポジトリでは新しく Connection を作成する必要があります。

NuGet.org へのプッシュの場合は API Key を作成して、以下のように Feed URL を設定します。

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

あまり関係ないですが、NuGet の API Key は期限付きなので安全ですが、更新を忘れそうです。

Release 用のタスクはこれだけで終わりですが、こっちでも Trigger としてタグを指定します。ドロップダウンでは選択できないので、直接入力すれば良いです。

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

これで GitHub Release を作成すると、自動的に NuGet パッケージのデプロイまで走るようになります。

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

バージョン名の扱いで少しシンプルさが欠けますが、全体を見ると単純なパイプラインで実現出来ました。

テンプレートである程度までパイプラインが組めるようになれば楽だと思いました。

Durable Functions の Activity を安全に呼び出すためのプロキシを作った

Durable Functions が相変わらず凄く便利なので、いろんな部分で使っています。しかしヘビーに使うにつれて、アクティビティ関数を呼び出す部分で事故ったり、非常に冗長だと感じるようになってきました。

新しい Function をテンプレートから作成すると、以下のようなオーケストレータが生成されると思います。

public static class Function1
{
    [FunctionName("Function1")]
    public static async Task<List<string>> RunOrchestrator(
        [OrchestrationTrigger] DurableOrchestrationContext context)
    {
        var outputs = new List<string>();

        // Replace "hello" with the name of your Durable Activity Function.
        outputs.Add(await context.CallActivityAsync<string>("Function1_Hello", "Tokyo"));
        outputs.Add(await context.CallActivityAsync<string>("Function1_Hello", "Seattle"));
        outputs.Add(await context.CallActivityAsync<string>("Function1_Hello", "London"));

        // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
        return outputs;
    }

    [FunctionName("Function1_Hello")]
    public static string SayHello([ActivityTrigger] string name, ILogger log)
    {
        log.LogInformation($"Saying hello to {name}.");
        return $"Hello {name}!";
    }
}

単純なオーケストレータですが CallActivityAsync とアクティビティ関数名が都度出てきて、少し冗長に感じると思います。そして戻り値の型は呼び出し側で指定しないと、正しい値は返って来ません。

このくらい小規模であれば我慢できるのですが、以下のコードのように複雑なオーケストレータと多くのアクティビティで構成される場合には大変です。既に何回かパラメータの渡しミスと、戻り値の型の指定ミスを経験しています。

特にアクティビティ関数のシグネチャを変更した場合は致命的で、呼び出されている部分を全て確認して、正しい型になってるか調べる必要があります。更に関数名を nameof で指定していない場合は CodeLens も使えないので詰みます。

こういうのは本来はコンパイラや IDE にやらせるべき仕事です。なので安全にアクティビティを呼び出すために、簡単なライブラリを作りました。

考え方はインターフェースとアクティビティを用意して、それに従って動的にプロキシクラスを作成するので、プロキシ経由でアクティビティを実行するという簡単なものです。

呼び出しはインターフェースに定義されたメソッドのシグネチャを元に行われるので、正しい型で値を渡したり、戻り値の受け取りが出来るわけです。

Activity Proxy を使って書き換える

上のサンプルを Activity Proxy を使って書き換えてみます。まずはインターフェースを用意します。

public interface IHelloActivity
{
    Task<string> SayHello(string name);
}

文字列を受け取って、文字列を返すだけの単純なメソッドだけ持ちます。今のところ制約としてメソッドは 1 つのパラメータが必須かつ、戻り値の型は TaskTask<T> が必須です。

パラメータを受け取らないメソッドを書きたいところですが、今は ActivityTrigger を指定するためにはパラメータが最低 1 つ必要なので、Durable Functions 側を弄らないと対応は難しいでしょう。

上のインターフェースを元にアクティビティ関数を実装します。Task が必須なので async を使わないメソッドの場合は少し冗長に見えますが、現実的には同期で終わるアクティビティを書かないので良いでしょう。

public class HelloActivity : IHelloActivity
{
    [FunctionName(nameof(SayHello))]
    public Task<string> SayHello([ActivityTrigger] string name)
    {
        return Task.FromResult($"Hello {name}!");
    }
}

ポイントとしては nameof を使って FunctionName を指定している部分と ActivityTrigger の指定です。最初はコード生成で上手く隠したかったのですが、大掛かり過ぎたのでこの形に落ち着きました。

ここまででアクティビティは実装出来たので、後はオーケストレータから呼び出すだけです。DurableOrchestrationContextCreateActivityProxy という拡張メソッドが増えているはずなので、インターフェースを型引数に指定して呼ぶとプロキシのインスタンスが返って来ます。

プロキシを作ってしまえば、後は普通にメソッドを呼び出す感覚でコードを書けば良いです。

public class Function1
{
    [FunctionName("Function1")]
    public async Task<List<string>> RunOrchestrator(
        [OrchestrationTrigger] DurableOrchestrationContext context)
    {
        var outputs = new List<string>();

        var proxy = context.CreateActivityProxy<IHelloActivity>();

        // Replace "hello" with the name of your Durable Activity Function.
        outputs.Add(await proxy.SayHello("Tokyo"));
        outputs.Add(await proxy.SayHello("Seattle"));
        outputs.Add(await proxy.SayHello("London"));

        // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
        return outputs;
    }
}

最初のサンプルコードと比べて、かなりスッキリしたと思います。型情報を保ったまま呼び出せるので、間違った値で呼び出そうとしてもコンパイル時に発見できます。

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

勿論ちゃんと実行出来ます。特に面白みがない実行結果ですが、意図したとおりの挙動です。

Retry 付きで Activity を実行する

Durable Functions は組み込みでアクティビティのリトライ機能を持っているので、よくあるリトライ処理を書く必要がないので便利です。

基本的に冪等であることを意識して書く必要があるので、全体として安全にリトライが行えます。

アクティビティ関数のリトライ付き実行は CallActivityWithRetryAsync を使って行っていましたが、Activity Proxy の場合は RetryOptions 属性をインターフェースに付けて行うようにしました。

public interface IHttpGetActivity
{
    // 5 秒のウェイトを入れつつ 10 回までリトライする
    [RetryOptions("00:00:05", 10)]
    Task<string> HttpGet(string path);
}

属性を使っているので実行時に変更が出来ないですが、大抵はアクティビティ単位でリトライ設定が行えれば、固定値で困らないことが多いのでこのようにしています。

リトライ用に属性を設定しても、アクティビティの実装は同じです。制約にだけ注意が必要ですが、通常の Azure Functions なので DI を使ってコンストラクタへのインジェクションも行えます。

public class HttpGetActivity : IHttpGetActivity
{
    public HttpGetActivity(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    private readonly HttpClient _httpClient;

    [FunctionName(nameof(HttpGet))]
    public async Task<string> HttpGet([ActivityTrigger] string path)
    {
        var response = await _httpClient.GetAsync(path);

        response.EnsureSuccessStatusCode();

        var content = await response.Content.ReadAsStringAsync();

        return content;
    }
}

この Azure Functions での DI はかなり便利なので、既に大半を DI ベースに書き換えています。

プロキシがリトライ用の設定があれば、自動的に呼び出すメソッドを変えてくれるので、オーケストレータは何も意識する必要はありません。

public class Function2
{
    [FunctionName("Function2")]
    public async Task<string> RunOrchestrator(
        [OrchestrationTrigger] DurableOrchestrationContext context)
    {
        var proxy = context.CreateActivityProxy<IHttpGetActivity>();

        // ブチザッキのタイトルを取る
        var content = await proxy.HttpGet("https://blog.azure.moe/");

        var match = Regex.Match(content, @"<title>(.+?)<\/title>");

        return match.Success ? match.Groups[1].Value : "";
    }
}

このオーケストレータを実行すると、ちゃんとブチザッキのタイトルが返ってきます。ネットワークエラーが発生した場合でも、自動でリトライを行ってくれます。

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

大したことのないサンプルではありますが、どこかのタイミングで Let's Encrypt 更新くんを全面的に Activity Proxy を使って書き換えようと思っています。

パラメータ無しのメソッドを使えるようにしたいところなので、解決したらプレリリースを外します。

Azure Functions Consumption と SQL Database Serverless の組み合わせは相性が良い

プレビュー中の SQL Database Serverless は設定した時間アイドルが続くと、自動的に停止するように構成できます。Serverless Tier の説明はドキュメントや SE の雑記を見てください。

アイドルが続くと停止してしまいますが、新しく接続したタイミングで復帰するようになっているので、クライアント側でのリトライを適切に実装していれば低コストに利用出来ます。

この挙動は Azure Functions の Consumption と組み合わせるのが相性が良さそうだったので、久し振りに SQL Database を作成して試してみました。

作成時に vCore ベースを選ぶと Serverless の設定が出てくるので、vCore とメモリを設定します。今回はテスト目的だったので、設定はほぼデフォルトのままにしました。自動停止は最低の 6 時間にしてあります。

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

作成後 6 時間アイドルが続くと、自動的に一時停止状態になります。6 時間はムッシュが言ってるように長すぎるので、Aurora Serverless に対抗できるぐらいにして欲しいですね。

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

後はアプリケーション側で SQL Database への接続をリトライする処理を組み込めば良いですが、いくつか方法があると思います。例えば Polly を使って実装したり、ORM 組み込みのリトライなどがあります。

最近の SqlConnection に組み込まれているリトライ機能は、コマンド実行時のリトライになり、初回コネクションの失敗時には使えないらしいので注意したいところです。

Polly を使ってリトライを行う

ドキュメントによると中断時にはエラーコード 40613 を返すようになっているらしいので、Polly を使って以下のようなリトライポリシーを用意して試してみました。

実際に使う場合は SqlConnection の Open だけリトライだけにして、コマンド実行時のリトライは SqlConnection に組み込みの機能を使った方が良いかもしれません。

await Policy.Handle<SqlException>(ex => ex.Number == 40613)
            .WaitAndRetryAsync(10, x => TimeSpan.FromSeconds(10))
            .ExecuteAsync(async () =>
            {
                using (var sqlConnection = new SqlConnection(Environment.GetEnvironmentVariable("DefaultSqlConnection")))
                {
                    await sqlConnection.OpenAsync();

                    // SqlConnection を使って何かする
                }
            });

SQL Database Serverless が一時停止後に上のコードを組み込んだ Function を実行しても、リトライが Polly によって行われるために時間はかかりますが失敗とはなりません。

Application Insights の E2E Transaction を見ると、リトライが行われていることが確認できます。

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

初回ログイン時にエラーを返すまでに多少時間がかかるようなので、リトライ時のウェイトは 5 秒ぐらいにしておいた方が、全体としての待機時間を少なく出来そうです。

ちなみに今回の例では Exponential Backoff は向いていないので、固定値の方が良いでしょう。

Entity Framework Core でリトライポリシーを設定

Entity Framework Core には組み込みでリトライ機能が用意されているので、オプションを設定するだけで良い感じに処理してくれます。

ドキュメントも用意されているので、安全に SQL Database Serverless を利用できます。

有効にするには EnableRetryOnFailure を呼び出せばよいので、非常に簡単です。

SQL Database Serverless は復帰するまでに 1 分ほど必要とドキュメントに書かれているので、リトライ回数が少ないと上手く動作しないこともあるので気を付けましょう。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(Environment.GetEnvironmentVariable("DefaultSqlConnection"), sqlOptions => 
                sqlOptions.EnableRetryOnFailure(10, TimeSpan.FromSeconds(10), null)));
    }
}

Azure Functions で Entity Framework Core を利用する場合は、DI 経由で DbContext のインスタンス管理を行った方が楽です。詳しくは以前書いたエントリを参照してください。

作成したアプリを Azure Functions にデプロイして実行すると、SQL Database が自動的に再開されます。

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

Monitor から実行履歴を確認すると、時間はかかっていますが処理自体は成功しています。

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

更に Application Insights から該当の E2E Transaction を確認すると、何回かは SQL Database への接続に失敗していますが、最終的には成功していることが確認できます。

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

Consumption でのデフォルト functionTimeout は 5 分なので、ほとんどのケースでは SQL Database Serverless の復帰にかかる時間は無視できると思います。

Function 自体の処理に長い時間かかる場合は、適宜 functionTimeout を調整すれば良さそうです。

Startup でウォームアップを行う

SQL Database Serverless を使ってコスト削減が可能なのは、決まった時間でバッチ的に処理を行って、それ以外はほぼアクセスされないというようなケースでしょう。

そういう場合、Azure Functions では TimerTrigger を使うことになると思いますが、Azure Functions の Scale Controller は TimerTrigger で設定した時間より少し前にホストを起動する仕組みになっています。

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

この挙動を利用すると、ホスト起動のタイミングで SQL Database Serverless を自動的に再開させて、Function 本体では再開済みの状態で処理で行うことが出来そうです。

ログインだけ行えば自動で再開されるので、以下のように Open だけ行う処理を追加しました。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(Environment.GetEnvironmentVariable("DefaultSqlConnection"), sqlOptions => 
                sqlOptions.EnableRetryOnFailure(10, TimeSpan.FromSeconds(10), null)));

        // SQL Database へのログインだけ実行する(失敗しても良い)
        _ = SqlDatabaseWarmupAsync();
    }

    private async Task SqlDatabaseWarmupAsync()
    {
        try
        {
            using (var sqlConnection = new SqlConnection(Environment.GetEnvironmentVariable("DefaultSqlConnection")))
            {
                await sqlConnection.OpenAsync().ConfigureAwait(false);
            }
        }
        catch
        {
            // ignored
        }
    }
}

停止状態であれば間違いなく失敗するので、それが原因で落ちないようにだけしておきます。処理自体を投げっぱなしにして、ホストの初期化が遅れないようにもしておきます。

これでデプロイすると TimerTrigger が発動する前にホストが起動され、SQL Database Serverless が非同期で再開されます。これで実際に処理が行われる前には SQL Database は再開されているので、E2E Transaction でもエラーが発生していないことが確認できます。

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

当然ながら全体の処理時間も短縮されています。もちろん 6 時間アイドルが続けば再度停止状態になりますが、また Function が起動するタイミングで自動的に再開されるようになります。

これで Azure Functions と SQL Database のそれぞれで、本当に必要な時のみ課金が行われる環境を用意できます。常時リソースが確保される場合と比べて、コストは格段に最適化されます。

Durable Functions v2 で追加される Durable Entities を実際に動かして学んでみた

Build 合わせでリリースされた Durable Functions v2 ですが、Typed Invocation が入ったバージョンを待ってたら 1 ヵ月が過ぎようとしてたので、諦めて現行バージョンで Durable Entities を学ぶことにしました。

ちなみに Durable Entities 以外に Storage Provider の分離とかありますが、Redis を試したところ PubSub じゃなくて Polling だったので割とがっかりです。Cosmos DB Provider が来るのを待ってます。

Durable Entities に関しては Chris が既にブログに書いてくれています。Typed Invocation はかなりスッキリと書けるようになりそうなので期待しています。

これまでの Durable Functions で解決できないパターンとして Aggregate が挙げられています。

名前は Actor ではないが、Actor と同じような動作をするという説明です。

ドキュメントでは近いものとして Service Fabric の Reliable Actors が挙げられています。Durable Entities を実装したのが MSR の人なので似たようなものになるのは理解できます。

今のところ例として出てくるのが Counter ぐらいなのと、Reliable Actors も楽しいサンプルが見当たらないので、実際問題としてどういう場面で使うのが良いのか難しいです。細かい粒度でのスケーリング目的なら Task / Activity で良いのではという気もしてきます。

サンプルとして用意されている RideSharing プロジェクトは中々分かりやすかったです。

思想は理解しているのですが、実際に使う場面がやはりパッと浮かばなかったので、実際に簡単なアプリケーションを書いてみることにしました。

思いついたのがメール配信管理だったので、その配信ステータスを管理する部分を今回書きました。いろんなところで話してる気がしますが、メールは API 実行して終わりという訳ではなく、正しく相手まで届いたかというトラッキングが重要なので、今回は SendGrid を使って実装しました。

作成したプロジェクト

とりあえず作成したプロジェクトは GitHub に公開しています。手元では意図したとおりに動きました。

色々と手探りな状態で書いてみたので、もっと良い設計があると思います。そもそも Durable Entities を使うより良い方法がある気もしますが勉強目的なので。

Entity の設計

Durable Entities で非常に重要となる Entity の設計は以下の通りです。

2 つの Entity が存在していて MailEntity は 1 通のメールを表しています。そして MailStatusEntity は現在の配信ステータス単位の一覧情報を保持しています。

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

メールの送信と配信ステータスの保持と更新は MailEntity が行っています。Event Webhook からイベントが更新されたタイミングで、該当するステータスの MailStatusEntity を実行して一覧を保持しています。

要するに MailStatusEntityMailEntity にアクセスするためのインデックスです。

実装した REST API

必要最低限の REST API を以下の通り実装しました。大体は名前から想像がつくと思います。

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

この時、メールを識別するための ID はそのまま EntityKey としています。

メール送信

実際に REST API を使ってメールを送信してみます。単純な API なので Body に JSON を渡すだけです。

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

API の実装は MailEntity に送信を行うようにメッセージを投げているだけです。

await client.SignalEntityAsync(new EntityId(nameof(MailEntity), Guid.NewGuid().ToString()), "Send", message);

オーケストレーター以外では Fire-and-forget になるので、呼び出し自体は一瞬で返って来ます。

このメッセージを受け取る MailEntity のエントリポイントは以下の通りです。Typed Invocation が使えるとこの辺りスッキリ書けるようになるのですが、今は switch を使って分けるしかないです。

public class MailEntity
{
    [FunctionName(nameof(MailEntity))]
    public async Task EntryPoint([EntityTrigger] IDurableEntityContext context)
    {
        switch (context.OperationName)
        {
            case "Send":
                await Send(context);
                break;
            case "Resend":
                await Resend(context);
                break;
            case "UpdateStatus":
                await UpdateStatus(context);
                break;
        }
    }
}

Durable Entities は EntityId 毎に別々の状態を持つので、上手く EntityId を設計する必要があるでしょう。省略しましたが MailEntity はメールの情報以外にも送信日やステータス更新日なども持っています。

この辺りのコードを書いていて Durable Entities と名前が付けられた理由が分かってきました。

配信ステータスを更新

Event Webhook から送られてきたペイロードには EntityKey が含まれるようにしてあるので、その値を使って MailEntity に対して配信ステータスの更新を実行します。

foreach (var payload in eventPayloads)
{
    await client.SignalEntityAsync(new EntityId(nameof(MailEntity), payload.EntityKey), "UpdateStatus", payload.Event);
}

本来なら都度 await しない方が良いのですが、全て呼び出して Task.WhenAll するコードを書くのが面倒だったので妥協しました。

上のメッセージを受け取った MailEntity では状態の更新と一緒に MailStatusEntity に対して EntityKey を追加するようにメッセージを投げています。

if (state.Status != status)
{
    context.SignalEntity(new EntityId(nameof(MailStatusEntity), status), "Add", context.Key);
}

そして MailStatusEntity は配信ステータス単位に作られて、それぞれが HashSet を持っているので、そこに EntityKey を追加しています。

現在の配信ステータスを確認

ここまでの実行によって MailStatusEntity の状態を、読めばバウンスしたメールが存在するかといった情報を確認出来るはずです。

MailStatusEntity の状態を読むだけの REST API を用意しているので、実行して確認してみます。

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

ちゃんと delivered になっているメールの EntityKey 一覧が返って来ました。Durable Entities に送信したメッセージは 1 つずつ順に処理されるので、競合を考えなくて良いのは便利です。

返ってきた EntityKey を適当に 1 つ選んで、メール情報を取得する API を実行すると該当する MailEntity が保持する状態が返って来ます。

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

とりあえず簡単なアプリケーションでしたが、Durable Entities を実際に動かしてみることができました。

疑問点と課題

1 つは Reliable Actors の時にも思ったのですが、何処まで状態を持たせるべきなのかという点です。

Service Fabric も結局レプリケーションのためにシリアライズされて、VM のディスクに書き出されるという実装だったはずです。上の実装では無限に MailStatusEntity の状態が大きくなっていくので、シリアライズ周りのコストが馬鹿でかくなることが容易に想像できます。

結局、妥協点を見いだせず終わった気がします。外部のストレージに書けという結果になったはずです。

もう 1 つが Aggregate を行うパターンでは Cosmos DB の Change Feed を使った方が良いのではないかという点です。多用しているパターンですが、集計を行う場合は SQL に入れた方が楽という結論が多いです。

逆に Cosmos DB の Change Feed を受け取った後の処理を Durable Entities で行うのはアリなのではという気がしています。Change Feed は PartitionKey 単位で順序保証がされているので、PartitionKey をそのまま EntityKey にしてしまえば、順序を保ったまま後続の処理を実装できるのではと思っています。

順番にメッセージを処理するという点では、EC サイトのカートは直列処理に出来て良いのではないかと思いましたが、結局在庫の引き当てで悩むことになりそうです。商品自体を Entity にすれば安全に在庫の引き当てが出来そうですが、スケールしないなとか色々思いました。

やっぱり結構利用する場面が難しいなと感じる結果となりました。もう少し試してみたいですね。

Azure Functions 向けに App Center Push を簡単に扱える拡張を作った

de:code 2019 の準備からの逃避として App Center Push を扱う Azure Functions 向けの拡張を書きました。単に IAsyncCollector<T> を提供して、REST API を呼ぶだけの簡単な実装です。

SignalR Service の実装を調べながら作ったので構造はほぼ同じです。

Azure Functions v1 では Notification Hub 向けの拡張はありましたが、既に Notification Hub は終わってそうな雰囲気が漂ってくるのと、App Center が結構良いと聞いていたので勢いで書きました。

NuGet も適当にビルドして公開しておきました。.NET Core になってからいろいろ楽です。

README にも使い方を簡単に書いてありますが、App Center Push の設定を行っていれば、API Token を作成しておくだけなので難しいことはないです。

App Center Push は Target をいろいろ選択できますが、その辺りはクラスを用意して対応しました。何も指定しなかった場合は全てのデバイスに送信となるので、テストには向いています。

IAsyncCollector<T> を実装しているので、プッシュを送信する際はコレクションに追加する感覚で書けます。この辺りは SignalR Service と同じですね。

public static class Function1
{
    [FunctionName("Function1")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
        [AppCenterPush(OwnerName = "shibayan", AppName = "AppCenterPushTest")] IAsyncCollector<AppCenterPushMessage> collector,
        ILogger log)
    {
        await collector.AddAsync(new AppCenterPushMessage
        {
            Content = new AppCenterPushContent
            {
                Name = "First Push From App Center",
                Title = "Push From App Center",
                Body = "Hello! Isn't this an amazing notification message?",
                CustomData = new { key1 = "val1", key2 = "val2" }
            }
        });

        return new OkResult();
    }
}

少し悩んだのですが、属性のプロパティとして Owner と AppName を取るようにしています。接続文字列とかあれば省略可にしていたと思いますが、こっちの方が分かりやすいかも。

メッセージ用のクラスは REST API とほぼ合わせているので、ドキュメントを読めばわかるはずです。

API Token は local.settings.jsonAppCenterPushApiToken という名前で追加します。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "AppCenterPushApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}

これで App Center Push を Azure Functions から使えるようになります。簡単でしたね。

動作確認用のサンプルアプリは UWP で適当に用意しました。iOS や Android 向けアプリのビルド向け環境が無かったので、割と都合がよかったです。

App Center Push の特徴としてプラットフォームが変わっても API は同じという点があります。UWP の場合は Function を実行すると、同じマシンに通知が飛んでくるので確認も簡単です。

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

送信ログは App Center から確認できます。今回はターゲットを指定しなかったですが、指定した場合は条件も App Center から確認出来るので便利だと思います。

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

App Center Push の REST API は難しくないのでそのまま叩いても良いと思うのですが、Azure Functions 向けの拡張を書きたかったので良い題材でした。

時間があれば JavaScript から呼べるようにしたり、Java 向けのバインディングを作ってみたいと思います。

Azure App Service への Key Vault 証明書のインポートがお手軽になった

以前は ARM REST API を直接叩く必要のあった App Service への Key Vault 証明書のインポートですが、いつの間にか Azure Portal から行えるようになっていました。

Azure Portal から SSL settings を開くと、Key Vault 向けに新しくボタンが追加されています。

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

REST API を使うのとは異次元の便利さです。これまでも一度追加した証明書の同期は Azure Portal から行えていましたが、これで Key Vault で証明書を全て管理する機運が高まってきました。

Key Vault を使って App Service の証明書を管理するメリットは以前に書きましたが、簡単に説明しておきます。例えば複数の App Service に証明書を追加するケースでは、PFX をそれぞれに追加する必要がありましたが、Key Vault の場合は一元管理できるため管理が大幅に簡略化できるという話です。

App Service の証明書は Webspace に紐づく仕組みなので、リージョンやリソースグループが別の場合には追加し直しが必要で、証明書の更新の際にはそれぞれの App Service に対して設定が必要です。

それに対して Key Vault を使った場合では、購入した PFX を Key Vault に追加して App Service へインポートしてしまえば、後はこれまでと同じ操作で HTTPS のバインドを追加できます。

さらに Key Vault 側で証明書を 1 度更新すれば、自動的に App Service 側の証明書も更新されるため、更新時のオペレーションミスを避けることが出来ます。Key Vault で Let's Encrypt を使う Function も公開しているので、機会があれば試してみてください。

実際に Key Vault にある証明書をインポートしてみました。REST API を使う際には Key Vault のアクセスポリシーの設定が必要でしたが、Azure Portal からの場合は自動で行ってくれるようです。

適当にサブスクリプションと Key Vault、そして証明書を選んでインポートします。

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

Key Vault からインポートした証明書を選ぶと詳細情報が表示されます。必要であれば Sync を選んで最新バージョンに同期することも出来ます。

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

実際にインポートした証明書を Key Vault 側で更新してみたところ、しばらくすると自動的に新しいものに切り替わっていたので安心して使えますね。これで Traffic Manager を使って負荷分散や DR 構成を組んでいる場合でも、安全な証明書管理を実現できるはずです。

Azure Functions v2 の Dependency Injection を使って設定値を上手く扱いたい

Build 2019 に合わせて Azure Functions v2 で Dependency Injection が正式にサポートされましたね。これまでも static を外して WebJobsStartup を書けば使えていましたが、DI 向けに整理されました。

ドキュメントがあるのでそっちを読んでおいてください。基本は ASP.NET Core と同じです。

そして横浜さんが日本語で書いてくれているので、読んでおけば理解できるはずです。

DI にサービスを追加する分には特に違和感なく触れるので問題ないです。特に HttpClient は扱いに気を付けないと死ぬので、DI に Singleton として追加したり HttpClientFactory を使うことで解消します。

個人的には Azure Functions v2 の最大の弱点としては、Configuration を触る公式な手段が失われたことだと考えています。これまでは ConfigurationBuilder を使って独自に解決していましたが、DI を使うと上手く解決できるはずなので、いろんなパターンで試しました。

アプリケーション設定を扱う

ここで指すのは Azure Portal から設定したり、local.settings.json に書いておく設定となります。ASP.NET Core だと Options パターンを使って解決したりします。

共通する部分としてクラスと設定を予め載せておきます。ちなみに local.settings.jsonValues 以下にはオブジェクトを書くことが出来ないので、セパレータを使ってネストを表現します。

public class SampleOptions
{
    public string Value { get; set; }
}
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "Sample:Value": "buchizo"
  }
}

これ自体は特に意味のない設定ですが、Azure Functions v2 では環境変数から読み込むか、先ほど紹介したように ConfigurationBuilder を使って独自に管理する必要がありました。

しかし、DI に対応した今ではホストが使っている IConfiguration を読めるので、簡単なコードで実現出来るようになります。上の設定を扱う方法として 2 つ試したので、それぞれ紹介します。

IConfiguration を直接受け取る

単純な方法としては IConfiguration 自体をコンストラクタで受け取ってしまうことです。以下のように書けば、用意したクラスに設定値をバインド出来ます。

public class Function1
{
    public Function1(IConfiguration configuration)
    {
        _sampleOptions = configuration.GetSection("Sample").Get<SampleOptions>();
    }

    private readonly SampleOptions _sampleOptions;

    [FunctionName("Function1")]
    public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req, ILogger log)
    {
        return new OkObjectResult($"Hello, {_sampleOptions.Value}");
    }
}

簡単な代わりに、コンストラクタでの処理がちょっとイマイチです。出来れば設定値をバインドした状態で値を受け取りたいところです。

Startup で Options として追加する

もう一つは Options パターンを使う方法です。Core MVC と同じ書き方ですが、Configure を呼び出す際には事前に IConfiguration が必要となるので、一旦 IServiceProvider を作成して取得してしまいます。

IServiceProvider を作成してしまえば、後は GetRequiredService を使ってインスタンスが得られます。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var serviceProvider = builder.Services.BuildServiceProvider();

        var configuration = serviceProvider.GetRequiredService<IConfiguration>();

        builder.Services.Configure<SampleOptions>(configuration.GetSection("Sample"));
    }
}

メソッドの引数として IConfiguration を受け取れれば楽なんですが、この辺りは今後に期待という感じがします。おそらく正式なサンプルか実装が公開されると思います。

Configure を使って設定した値は、以下のようなコードで受け取れます。Core MVC と同じです。

public class Function1
{
    public Function1(IOptions<SampleOptions> sampleOptions)
    {
        _sampleOptions = sampleOptions.Value;
    }

    private readonly SampleOptions _sampleOptions;

    [FunctionName("Function1")]
    public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req, ILogger log)
    {
        return new OkObjectResult($"Hello, {_sampleOptions.Value}");
    }
}

利用するセクション名は Startup 側に移譲されたので、コンストラクタは単純になりました。名前付き Options とかも使えるはずなので、用途に合わせて使い分けたいところです。

接続文字列を扱う

v1 から v2 への移行で大体問題になるのが接続文字列周りだと思います。環境変数から取ればよいと言われればそれまでなんですが、DI を使って綺麗に解決したいところです。

サンプルとして以下のような local.settings.json を用意しました。接続文字列がメインです。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  },
  "ConnectionStrings": {
    "DefaultSqlConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=TestDB;Integrated Security=True;Connect Timeout=30",
    "DefaultStorageConnection": "DefaultEndpointsProtocol=https;AccountName=***;AccountKey=***;EndpointSuffix=core.windows.net",
    "DefaultCosmosConnection": "AccountEndpoint=https://****.documents.azure.com:443/;AccountKey=***;"
  }
}

とりあえず代表的な 3 つのパターンでの使い方を試したので紹介しておきます。

Entity Framework Core の場合

よく使われるであろう Entity Framework Core の場合ですが、ASP.NET Core で使ってるのと同じように AddDbContext を使って利用する DbContext を追加してあげます。

Core MVC の場合は IConfiguration を受け取り済みので簡単に取得できましたが、Azure Functions の場合は IServiceProvider を受け取れるオーバーロードメソッドを使えば良いです。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddDbContext<MyDbContext>((provider, options) =>
        {
            var configuration = provider.GetRequiredService<IConfiguration>();

            options.UseSqlServer(configuration.GetConnectionString("DefaultSqlConnection"));
        });
    }
}

これで DbContext をコンストラクタで受け取れるようになるので、SQL Database などへのアクセスが行えます。DbContext は Singleton で持つことが出来ないので、DI を使った方が安全です。

Azure Storage の場合

Blob や Queue へのアクセスを行うためのクライアントは Singleton で扱っても良いらしいので、接続文字列を取得した後に DI へ追加してあげれば解決します。

この時 AddSingletonIServiceProvider を受け取れるオーバーロードを使います。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddSingleton(provider =>
        {
            var configuration = provider.GetRequiredService<IConfiguration>();

            var storageAccount = CloudStorageAccount.Parse(configuration.GetConnectionString("DefaultStorageConnection"));

            return storageAccount.CreateCloudBlobClient();
        });
    }
}

これで CloudBlobClient を Singleton で扱えるようになります。非常に簡単ですね。

Cosmos DB の場合

最後は Cosmos DB ですが、このクライアントは Singleton で持たないとパフォーマンスが悪くなってしまうので、DI を使うのがシンプルで良いです。

考え方は Storage と同じなので説明は省略します。Cosmos DB の v3 クライアントを使っているので、Builder パターンでインスタンスを作るようになっています。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddSingleton(provider =>
        {
            var configuration = provider.GetRequiredService<IConfiguration>();

            return new CosmosClientBuilder(configuration.GetConnectionString("DefaultCosmosConnection"))
                .UseConnectionModeDirect()
                .Build();
        });
    }
}

ちなみに Cosmos DB の v3 クライアントは今月中に GA するらしいです。パフォーマンスも改善されていて、インターフェースが分かりやすくなっているので今から慣れておいて損はないです。

ここで紹介した以外のクライアントも IServiceProvider 経由で IConfiguration を取得する部分までは同じなので、特に問題なく対応できるでしょう。

Azure App Configuration の Feature Management が便利そうだったので試した

恐らく Build 2019 に合わせたと思ってますが、Azure App Configuration がアップデートされて Feature Management という新しい機能が追加されています。

公式にも A/B テストとか、新しい機能の段階的なリリースにも使えると紹介されています。App Service の Test in production ではなく、コードレベルでの対応の方が良いことも多いでしょう。

要するに bool 値を専用に扱うための機能です。Azure Portal からオンオフの管理が簡単なので、普通に Key を追加するよりも便利に扱えます。

同時にクライアントライブラリもアップデートされています。前に動かなかった Time-Based Access は簡単に試している範囲では直っているようでした。

基本的な使い方

ドキュメントでは ASP.NET Core ベースで書かれていますが、DI を使って基本となる IFeatureManager を取得できれば良いので、Console App などでも使えます。

同様に Azure Functions v2 でも問題ないでしょうが、基本は Web アプリで使うかと思います。

単純に新しい Feature を追加してオンオフするだけで試しておきます。基本はドキュメントにあるサンプルコードと同じなので、気になった部分だけコードを出すことにします。

Azure Portal から App Configuration を作成すると、Feature Management という項目が増えているはずです。

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

適当にキー名を設定して追加すると、Portal にトグルスイッチ付きの項目が出てきます。

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

このオンオフが bool 値にそのまま対応しているので難しいことはないです。

そしてキーを参照する側の話ですが、ASP.NET Core の場合は FeatureAttributefeature Tag Helper が用意されているので、組み込みも違和感なく行えます。

[Feature("Beta")]
public class NewFeatureController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}
<feature name="Beta">
  Beta flag on
</feature>

キーをオンにした時だけコントローラや表示が有効になるので、テストや新機能のリリース時の切り替えに使えるという話です。App Configuration よりも ASP.NET Core との統合の方が重要という感じがします。

ちなみにデフォルト設定では 30 秒間隔でポーリングするようになっているので、常時起動している Web アプリでも問題なく切り替えが可能です。

少し高度な使い方

Feature Management には ASP.NET Core 向けにいくつか組み込みで機能が用意されています。ドキュメントにちゃんと書いてあるので、特に説明は不要かと思います。

実装としては IFeatureManager を DI で受け取って、フラグを見て処理を分岐しているだけなので、DI で解決される部分でならカスタム実装を入れたりと自由に使えます。

例えば独自の Service を用意した場合にも、コンストラクタで IFeatureManager を受け取れば良いです。

public class DemoService
{
    public DemoService(IFeatureManager featureManager)
    {
        _featureManager = featureManager;
    }

    private readonly IFeatureManager _featureManager;

    public string Hello(string name)
    {
        if (_featureManager.IsEnabled("Beta"))
        {
            return $"こんにちは, {name}";
        }

        return $"Hello, {name}";
    }
}

ドキュメントでは一部 enum を使っていましたが、基本は string しか受け取ってくれないので Feature 用の文字列をいい感じに管理する必要があります。

この辺りは設定からコード自動生成したり、同期を上手くとる方法が必要になりそうです。

FeatureFilter を使う

ここまでの機能だと目新しさはないというか、これまでも出来てただろという感じですが、Feature Management ではフィルターを使って動作をカスタマイズできます。

分かりやすい部分だと、トラフィックの数 % だけ有効化するといった動作を簡単に追加できます。

組み込みで用意されているフィルターは以下の 2 種類です。名前からわかるように % 指定するものと、期間指定するものの 2 つです。

  • PercentageFilter
  • TimeWindowFilter

使う前には DI にフィルターを追加しておく必要があるので、忘れないようにします。組み込みのフィルターは自動で追加されていても良い気がしますが、今は手動で追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddFeatureManagement()
            .AddFeatureFilter<PercentageFilter>();
}

DI に追加してしまえば、後は Azure Portal から使いたいフィルターを追加するだけです。

名前がクラス名と異なっているので少し注意が必要です。フィルター毎にパラメータを設定可能なので、今回の PercentageFilter の場合は 0-100 の数字を設定して、有効化する確率を指定します。

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

保存するとトグルスイッチ部分が Conditional 表示に変わります。

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

これで指定した % だけフラグが有効になるので、段階的なリリース向けに利用できます。

再デプロイは不要で、Azure Portal から設定を変えれば再起動無しで反映されるのは使い勝手が良いです。

FeatureFilter を自作する

組み込みの FeatureFilter はやはり機能が少ないので、必要な場合は自作してしまいましょう。基本は IFeatureFilter を実装したクラスを用意すれば良いだけなので簡単です。

DI には Singleton として追加されますが、IHttpContextAccessor が使えるので HTTP リクエストによって動作を変えるフィルターを作ることも出来るようです。実際に以下のコードで動作を確認しておきました。

[FilterAlias("MyCustomFilter")]
public class MyCustomFilter : IFeatureFilter
{
    public MyCustomFilter(IHttpContextAccessor httpContextAccessor, ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<MyCustomFilter>();
        _httpContextAccessor = httpContextAccessor;
    }

    private readonly ILogger _logger;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public bool Evaluate(FeatureFilterEvaluationContext context)
    {
        _logger.LogInformation(_httpContextAccessor.HttpContext.TraceIdentifier);

        return true;
    }
}

フィルターとしての動作は持たず、単純にログにトレース ID を書き出すだけですが、ちゃんとリクエスト単位で ID が出力されています。

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

この結果を踏まえて、サンプルとして User-Agent の値で振り分けるフィルタを書いてみました。

実用性はあまりないですが、Azure Portal からパラメータの設定が可能なサンプルとしてみてください。

[FilterAlias("UserAgentFilter")]
public class UserAgentFilter : IFeatureFilter
{
    public UserAgentFilter(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    private readonly IHttpContextAccessor _httpContextAccessor;

    public bool Evaluate(FeatureFilterEvaluationContext context)
    {
        var userAgentSettings = context.Parameters.Get<UserAgentSettings>();

        var userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"].FirstOrDefault();

        return userAgent != null && userAgent.Contains(userAgentSettings.Value);
    }
}

public class UserAgentSettings
{
    public string Value { get; set; }
}

Azure Portal で設定する名前は FilterAlias で指定した名前になります。

これも DI に追加した後に Azure Portal からフィルターを設定しておきます。ちなみに複数フィルターを追加した場合は上から順に評価されるかつ OR 扱いになります。

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

パラメータとして Edge を設定して、Edge と Chrome でページを確認しておきました。ちゃんと Edge ではナビゲーションに項目が出ていますが、Chrome では出ていないので正しく動いています。

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

HTTP リクエストを見て判断できるので、例えばログイン中のユーザーが所属するロールを見て動作を変えるというフィルターも書けるはずです。

他にもアクセス元が社内の IP の場合だけ機能を有効にするといった処理も簡単に実現出来ます。セッションアフィニティ用にクッキーを吐いたりも出来そうな気がするので、割と応用範囲が広いです。

Feature Management を単独で使う

Azure Portal では Feature Management は App Configuration に強く統合されていますが、ライブラリ的には分離された設計になっているので、当然ながら単独で使うことが出来ます。

Feature Management がやっていることは Configuration を読み取って、いい感じの切り替えを提供しているだけなので、構造を分かってしまえばシンプルに扱えます。

単独で使う場合は Microsoft.FeatureManagement.AspNetCore を追加して、AddFeatureManagement で利用するセクションを指定するだけで済みます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    // Feature section の下を見る
    services.AddFeatureManagement(Configuration.GetSection("Feature"));
}

Feature Management で使う値は appsettings.json に追加しておきます。環境変数なども使えます。

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Feature": {
    "Beta": true
  } 
}

これで App Configuration 無しでも動作します。Azure Portal のサポートは結構大きいので、組み合わせて使う方が便利だと思いますが、Feature Management だけ使いたいというニーズもあると思うので。

Application Gateway v2 が GA したので気になった機能を試す

既に Application Gateway v2 は GA してると思ってましたが、実はプレビューだったようです。

プロビジョニングや設定変更は圧倒的に早くなってますし、機能的にも v2 のが優れているので、もう v1 を使うメリットはほぼ無いでしょう。

価格は v1 と v2 で大きく変わってます。v2 は最低でも 2 インスタンス必要なので v1 よりは高くなると思いますが、パフォーマンスは v2 の方が良いのでトラフィックは多い場合は安くなりそうです。

個人的には Front Door 推しなんですが、Application Gateway も同じ L7 なのと前に使ったリソースが残っていたので、気になった機能を試しておきました。

HTTP Redirect

気が付いたら HTTP リダイレクトに対応していました。ユースケースとしては HTTP から HTTPS へのリダイレクトが多いかなと思いますが、パスベースでもリダイレクト出来るので URL Rewrite でやっていたことは実現できます。

External site へのリダイレクトは単純なので良いですが、Application Gateway の場合は別のリスナーへのリダイレクトが行えるので、HTTPS へ飛ばす場合はこっちを使います。

当然ながら HTTP と HTTPS でそれぞれリスナーを作っておく必要があります。

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

Include Query と Include Path は忘れずにチェックを入れておきます。

これで保存すれば、HTTP から HTTPS へのリダイレクトが行われるようになります。

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

Host 名を使ったマルチテナント時にも動作するようなので安心ですね。

パスベースで書く場合は、大体ルールが多くなりすぎてメンテナンスできず破綻することになりそうなので、何でも Application Gateway でやらない工夫は必要になると思います。

Rewrite HTTP Header

これは URL Rewrite では普通に行えていましたが Front Door では実現できなかった機能ですね。かなり柔軟に書けるので、大体のことは Application Gateway だけでカバーできると思います。

条件を正規表現で指定して、アクションでキャプチャした文字列を利用するというのは、割と URL Rewrite で使ってきたパターンですね。

URL Rewrite と同じような感じなので、今回は条件を弄らずに簡単な部分だけ試しました。

まず新しく Rewrite set を作成しますが、1 つのルールに対して 1 つの Rewrite set のみ割り当て可能なので、名前には少し気を付けたいところです。

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

ドキュメントにもあったように HSTS のヘッダーをレスポンスに付ける Rewrite ルールを作成してみます。

と言っても固定値で済むので以下のように単純な設定で済みます。

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

UI が分かりやすいので特に難しいことはないと思います。Header type だけは注意しましょう。

この Rewrite ルールを保存してブラウザからサイトにアクセスすると HSTS のヘッダーが返って来ます。

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

とても簡単に設定できました。UI が分かりやすいというのは非常に良いポイントです。

Server Variables を使う

先ほどは固定値を返すだけのルールを作りましたが、条件で指定した正規表現がキャプチャした結果以外に、 Application Gateway が持ついくつかのサーバー変数を使ってヘッダーを弄ることも出来ます。

サーバー変数の一覧はドキュメントに書いてありますが、今回はテストとして SSL 周りの面白そうな値をヘッダーとして返すルールを作成しました。

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

サーバー変数を使う場合は {var_SERVER_VAR_NAME} のように頭に var_ を付ける必要があります。

保存後にブラウザでアクセスすると、使用された暗号スイートと TLS バージョンが返って来ます。

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

本来ならバックエンドへのリクエストに付けて、そのままアクセスログに保存することのが多いと思いますが、確認しやすいのでレスポンスに設定しています。

サーバー変数には Cookie の値やクライアントとの TCP RTT の値など、面白いものが多いです。

X-Forwarded-* の扱いに注意

L7 のロードバランサーを使っている場合には X-Forwarded-For などを適切にハンドリングしないと困ることになりますが、Application Gateway も例外ではなく設定が必要になります。

ASP.NET Core での対応方針は Front Door と同じなので、そっちを参照してください。

1 点だけ面倒な部分として、Application Gateway はアクセスされたホスト名を一般的な X-Forwarded-Host ではなく X-Original-Host というヘッダーで通知してきます。

These headers are x-forwarded-for, x-forwarded-proto, x-forwarded-port, and x-original-host.

How an application gateway works | Microsoft Docs

デフォルトでは X-Forwarded-Host を見に行くので、思ったような挙動になりません。

対応としては ForwardedHeadersOptions が見に行くヘッダー名を X-Original-Host に変更するか、Header Rewrite を使って X-Forwarded-Host を付ける方法があります。

今回は X-Forwarded-Host を付ける方法で対応しました。ルールは以下のようになります。

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

これで ASP.NET Core アプリケーションが、正しくアクセスされたホスト名を認識出来るようになります。

Service Endpoint との組み合わせ時は更に注意

バックエンドとして App Service を使う場合では、今後は Service Endpoint を使って Application Gateway のサブネットからのみアクセス可能なように制限をすることが多くなると思います。

前に書いたようにプレビューですが、IP アドレスでの制限より分かりやすいです。

便利な Service Endpoint ですが、サブネットで App Service 向けに有効化するとアクセス元の IP アドレスが Application Gateway ではなく、IPv6 のユニークローカルアドレスに変化します。

特に X-Forwarded-For などへの対応を行わなかった場合は、クライアントの IP アドレスが v6 になることが確認できます。v6 を有効化していない VNET でも、Service Endpoint を有効化すると v6 になります。

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

この時に問題となるのが、ASP.NET Core の ForwardedHeadersMiddleware でホスト名をオーバーライドするためには、アクセス元の IP Range を登録しておく必要があることです。

Front Door では IPv4 と IPv6 の Range が公開されていたのでそのまま設定できましたが、Service Endpoint が使う Range は公開されていないので、ユニークローカルアドレス自体の Range を追加しました。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardLimit = 2;
        options.ForwardedHeaders = ForwardedHeaders.All;
        options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7));
    });

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

とてつもなく広いですが、これでホスト名も正しくアクセスされたものが反映されるようになります。

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

既に Application Gateway とバックエンドに App Service を使っていて、後から Service Endpoint を有効化する場合にはまりそうなので注意しましょう。

実装が ARR から nginx に変更

v2 ではプロビジョニングが早くなって、パフォーマンスが上がったという点が強調されていますが、中身が v1 では IIS 10 + ARR 3 という構成だったのが v2 では nginx に変更されたのが理由のようです。

バックエンドが全て死んでいる時のエラー画面が以下の通り nginx となっています。

Windows で nginx をホストする理由はほぼ無いと思うので、VM 自体も Linux ベースに変更されたのではないかと見ています。同一の API で中身が ARR と nginx で分かれているのは、中々面白い構成です。

Azure App Service の VNET Service Endpoint がプレビューに

Azure Storage や SQL Database で使えるようになっている Service Endpoint が App Service でも使えるようになりました。現在はパブリックプレビューなので、自由に試すことが出来ます。

中の人が書いているように、アプリの前に Application Gateway などを置きたい場合に使えます。

これまでも App Service は IP アドレスでの制限が行えていましたが、特定の VNET に存在するサブネットからのアクセスだけ許可するといったような使い方が出来ます。

Access Restrictions からルールを追加する際に Virtual Network が増えているので、それを選択すると対象となる VNET とサブネットが選べるようになります。ちなみに新しい VNET Integration とは異なり、Free 含め全てのプランで使えるようです。

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

サブネットで Service Endpoint が有効化されていない場合はメッセージが表示されますが、そのまま追加すると自動的にそのサブネットの Service Endpoint が有効化されます。

先に手動で追加しておいても問題ありません。作成時のメッセージが消えるぐらいです。サブネットの設定を確認すると Microsoft.Web という Service Endpoint が有効化されています。

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

話を戻してルールの追加ですが、一度に複数のサブネットは選べないので、必要な分だけ都度追加していきます。Azure CLI でも追加できるようになるはずですし、ARM Explorer でも出来ます。

App Service は他の Service Endpoint とは UI が異なっていますが、挙動は Service Endpoint なので基本的には同じです。将来的に他に UI が合わせられる可能性もあるかもしれないですね。

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

例によって許可ルールを追加すると、暗黙的にそれ以外は全て拒否するルールが追加されます。

上のようにルールを追加すると、当然ながらインターネット経由ではアクセス出来なくなります。エラー画面は IP 制限の時と同じものが表示されます。

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

指定した VNET の特定サブネットからのみ許可されているので、そのサブネットに適当に VM を追加してアクセス出来るか試しました。これも当然ですが問題なくアクセスが行えました。

使い勝手は Azure Storage などと同じなので、特に使い方で悩むことはないでしょう。

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

これまでは ASE しか無理だった VNET 周りが必要な構成ですが、大幅に条件が緩和されましたね。

Private IP でのアクセスとかは無理ですが、大体は特定の VNET からだけアクセスを許可したいというケースだと思うので、今回の Service Endpoint で大体要件は満たせるのではないかと思います。

Application Gateway からアクセス

VM から叩いて終わりだと面白くないのと、久し振りに VNET を作成したので Application Gateway をデプロイして、バックエンドに Service Endpoint を有効化した App Service を置いてみました。

適当に新しいサブネットを追加して、そこに Application Gateway をデプロイしました。バックエンドプールには例の App Service を追加していきます。

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

今回の話とあまり関係ないですが、最近の Application Gateway はバックエンドに App Service を追加した場合には、HTTP settings から Use for App Service にチェックを入れると良い感じに設定してくれます。

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

App Service 側にも Application Gateway をデプロイしたサブネットを許可するようにルールを追加すると、問題なく Application Gateway 経由でしかアクセスできない構成を組めます。

Application Gateway にアクセスすると、ちゃんとバックエンドの App Service が持つページが返って来ます。

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

個人的には Application Gateway をフロントに置くよりも Front Door を使った方が良いと思いますが、どうしても VNET 内にデプロイする必要がある場合には使えると思います。

ちなみに上の構成も、これまでは ILB ASE を使わないとアクセス制限が面倒になってしまうものでした。

App Service からアクセス

現在プレビュー中の新しい VNET Integration を使うと Service Endpoint へのアクセスが行えるので、同様に App Service 間のアクセスを VNET 経由に出来ます。

新しい VNET Integration については以下のエントリを参考にしてください。

これでサクッと Service Endpoint を経由して App Service にアクセス出来るかと思っていましたが、現在 VNET Integration が一部で正しく動作していない気配があり、予想以上にはまってしまいました。

Storage への Service Endpoint でのアクセスも行えませんが、VNET 内にいる VM には問題なくアクセス出来るので、Azure 側の問題っぽいです。新しい Scale unit のバージョンだと直ってそうでした。

Central US に新しいバージョンがデプロイされていたので、新しく App Service を作成して VNET Integration の設定を行うと、Service Endpoint 経由で App Service にアクセス出来ました。

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

curl だけだとアレなので、いつも通り URL Rewrite を使ってリバースプロキシとして実行しておきます。以下のように Web.config を書いて、アクセス制限済みの App Service を見るようにします。

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

これ以外にも XDT を使って ARR の有効化が必要ですが、今回は省略しておきます。

これでブラウザからアクセスすると、バックエンドへのリバースプロキシとして動作します。ページもちゃんと表示出来ていることが確認できます。

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

ちゃんと VNET に参加さえ出来ていれば、Service Endpoint 自体は問題なく動作しました。

検証のために App Service と Storage しか居ない VNET を作ってみましたが、VNET Integration と Service Endpoint が GA すれば VNET をアクセス制限のためだけに使うパターンも出てきそうですね。

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

将来的には指定したリソースからのみアクセス可能とかに出来れば良いと思いました。

Storage の場合は同じリージョンの VNET しかルールに追加できないみたいですが、App Service はどのリージョンの VNET でも対象に出来ました。意図した動作なのか分からないので、変わるかも知れません。