しばやん雑記

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

Azure DevOps で特定のファイルが変更された場合のみ Pipeline を実行させる

Azure Repos を使って Monorepo 構成を採用している場合には、Azure Pipelines はそれぞれのプロジェクト単位で実行させたいので、トリガーの設定でパスを指定して実現します。

例として backend というディレクトリが変更された時だけに Pipeline を実行させたい場合には、以下のように paths にディレクトリ以下のワイルドカードを追加して対応します。

地味に忘れやすいのが Pipeline の定義ファイルも paths に含めることです。backend 以下にある場合は問題ないですが、別ディレクトリに保存していると Pipeline 定義を変更した時に動かなくなります。

trigger:
  branches:
    include:
    - master
  paths:
    include:
    - backend/*
    - .azure-pipelines/build.yml

Azure Pipelines の定義ファイルは Red Hat が公開している VS Code の YAML 拡張をインストールしていると書きやすいです。JSON Schema ベースで入力補完と検証を行ってくれます。

殆どのケースでは以下のように自動的に JSON Schema を検出してくれますが、稀に迷うことがあるので明示的に指定すれば記憶されるので次からは適用されます。

これで master ブランチにプッシュされた時は意図した通りに動くようになったので、Pull Request に対しても Pipeline を実行するように Branch Policy を設定します。

GitHub Actions の場合は YAML 側でトリガー設定を追加するだけで済むのですが、Azure DevOps では Branch Policy の Build Validation を追加しないと動作しません。

Branch Policy の設定が若干分かり難く、Build Validation の設定も挙動不審なことがありますが、以下のように実行する Pipeline を選択すると良いです。

これで保存すると Pull Request の作成や変更時に指定した Pipeline が動くようになりますが、YAML で指定した paths の指定は完全に無視されてしまいます。

以下は README.md を変更した PR の例ですが Pipeline が実行されてしまっています。

Build Validation の場合にも同じように制限したい場合には UI から Path filter を別途指定する必要があるので、以下のようにパスを / 始まりかつセミコロンで区切って追加します。

検証はしていないですが、ドキュメントでは paths は先頭 / は不要なのに対して、Build Validation では / 始まりになっています。ぶっちゃけイマイチです。

追加して保存すると Pull Request からメッセージが消えて、即時反映されていることが確認出来ます。

今度は backend ディレクトリ以下のファイルを変更した PR を作成すると、Build Validation によって Pipeline が実行されることが確認出来ます。

YAML でのトリガー設定は PR を作成すれば誰でも追加できますが、Build Validation は権限を持った人しか設定できないので使い勝手が悪いです。この辺りは GitHub Actions のが後発だけあって使い勝手が良いです。

おまけ : git submodule を使っている時のフィルタ設定

今回 backend はディレクトリでしたが、もし git submodule を使っている場合には少し書き方が変わります。以下のように Azure DevOps 上では submodule はコミットハッシュが入ったファイルとして扱われているので、このファイルの変更をトリガーにしてあげます。

Build Validation のパス指定は /backend; /.azure-pipelines/build.yml のようにファイルとして扱うようにします。同様に YAML でのトリガーも以下のようにファイル扱いにします。

trigger:
  branches:
    include:
    - master
  paths:
    include:
    - backend
    - .azure-pipelines/build.yml

設定を変更した後に submodule を変更した Pull Request を作成すると、以下のように Build Validation によって意図した通りに Pipeline が実行されることが確認出来ます。

もちろん Pull Request をマージした後には通常のトリガー設定に従い Pipeline が実行されます。

最近は GitHub Actions ばかり使っていたので、Build Validation のパス指定と複数指定できることを失念していて少しはまりました。セミコロン指定で複数指定可能とか、一貫性のない記法は止めてほしいです。

Azure Container Instances の軽量なオーケストレーションを Durable Functions を使って実現する

Azure には Docker Image を指定するだけで簡単に動かせるサービスがいくつかあり、その中でも Container Instances は非常に起動が早くてシンプルなので利用範囲が広いのですが、オーケストレーターが存在しないので並列処理のように複数立ち上げたい場合には管理が面倒です。

公式ドキュメントでもオーケストレーターについて触れられていますが、AKS と Virtual Nodes 経由で使う方法が書かれていて、ACI はシンプルなのに途端に重量級になってしまいます。

サービスが既にかなり大規模な AKS を導入している場合以外では、Virtual Nodes 経由で ACI を使うメリットはほぼ存在しません。ACI を使うために AKS というのは本末転倒です。

一方で Azure Architecture Center には Durable Functions と ACI を組み合わせて、AKS を使わずに軽量なオーケストレーションを実現するサンプルが公開されています。こちらの方が圧倒的に筋が良いです。

Durable Functions の Activity として ACI の起動と削除を実装しておけば、後は Orchestrator から自由に呼び出してインスタンスをいくつでも作れるという仕組みです。更に Durable Functions のイベントやタイマーを使うと、さらに効率的に動かすことが出来ます。

サンプルは 1 つの ACI を動かすだけになっていたので、今回は更に拡張して複数の ACI を立ち上げることで大規模かつ効率的に並列処理出来るようにしてみました。

方針としては 1 つの ACI を操作する部分は Sub Orchestrator として実装することで、並列実行を容易にしつつ ACI の最大数制限に引っかかりにくくしています。

Resource Manager SDK を使って ACI を操作

まずは Durable Functions が関係ない部分から用意しました。ACI は古い設計の SDK は公開されているので、これを使って起動と削除を C# から簡単に行えます。

ACI の軌道に必要なパラメータは多いので、SDK を直接触るのではなく 1 枚被せて使いやすくしています。

今回のサンプルではリソースグループや Docker Image の情報などは固定にしていますが、必要に応じて Options などで取るようにすれば良いです。

public class ContainerInstanceService
{
    public ContainerInstanceService(ContainerInstanceManagementClient containerInstanceManagementClient)
    {
        _containerInstanceManagementClient = containerInstanceManagementClient;
    }

    private readonly ContainerInstanceManagementClient _containerInstanceManagementClient;

    private const string ResourceGroupName = "rg-aci-worker";
    private const string Location = "westus2";
    private const string ContainerImage = "debian:bullseye";

    public Task CreateAsync(string id)
    {
        return _containerInstanceManagementClient.ContainerGroups.BeginCreateOrUpdateAsync(
            ResourceGroupName,
            $"ci-{id}",
            new ContainerGroup
            {
                Location = Location,
                Containers = new[]
                {
                    new Container
                    {
                        Name = id,
                        Image = ContainerImage,
                        Command = new[] { "echo", "Hello, world" },
                        Resources = new ResourceRequirements
                        {
                            Requests = new ResourceRequests
                            {
                                Cpu = 1,
                                MemoryInGB = 1.5
                            }
                        }
                    }
                },
                OsType = "Linux",
                RestartPolicy = "Never"
            });
    }

    public Task DeleteAsync(string id)
    {
        return _containerInstanceManagementClient.ContainerGroups.BeginDeleteAsync(ResourceGroupName, $"ci-{id}");
    }

    public async Task<string> GetStateAsync(string id)
    {
        var containerGroup = await _containerInstanceManagementClient.ContainerGroups.GetAsync(ResourceGroupName, $"ci-{id}");

        return containerGroup.Containers[0].InstanceView?.CurrentState?.State ?? "Unknown";
    }
}

実際に利用する場合は Docker Image 側でエントリポイントを定義して、コンテナの起動と同時に処理が走るようにするはずですが、用意するのが面倒だったので Command を指定しています。

Dockerfile の指定と同様に若干癖があるので、以下のドキュメントを読んでおくのをお勧めします。

後は Managed Identity でターゲットとなるリソースグループに権限を追加すると、Azure Functions から ACI の起動と削除が簡単に行えるようになります。

ACI の管理を行う Activity を実装

Durable Functions では副作用のある処理は Activity として実装する必要があるので、先ほど作成したメソッドは以下のような Activity を用意して呼び出すようにします。

[FunctionName(nameof(CreateContainerInstance))]
public Task CreateContainerInstance([ActivityTrigger] IDurableActivityContext context, ILogger log)
{
    var id = context.GetInput<string>();

    return _containerInstanceService.CreateAsync(id);
}

[FunctionName(nameof(GetContainerInstanceStatus))]
public Task<string> GetContainerInstanceStatus([ActivityTrigger] IDurableActivityContext context, ILogger log)
{
    var id = context.GetInput<string>();

    return _containerInstanceService.GetStateAsync(id);
}

[FunctionName(nameof(DeleteContainerInstance))]
public Task DeleteContainerInstance([ActivityTrigger] IDurableActivityContext context, ILogger log)
{
    var id = context.GetInput<string>();

    return _containerInstanceService.DeleteAsync(id);
}

単純にメソッドを呼び出しているだけの中身のない Activity です。パラメータとして Orchestrator で生成されたユニークな ID を渡すことで、ACI の名前やイベント名として利用しています。

処理の起点となる Orchestrator を実装

実際に ACI の起動と削除を行う Sub Orchestrator を用意する前に、起点となる Orchestrator から用意しておきます。サンプルでもよく見る Fan-out / Fan-in の実装になっています。

[FunctionName(nameof(RunOrchestrator))]
public async Task RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var tasks = new List<Task>();

    // 5 並列で ACI を起動する
    for (var i = 0; i < 5; i++)
    {
        //tasks.Add(context.CallSubOrchestratorAsync(nameof(RunSubOrchestrator_Polling), i));
        //tasks.Add(context.CallSubOrchestratorAsync(nameof(RunSubOrchestrator_Event), i));
    }

    // 全ての ACI が終了するまで待機
    await Task.WhenAll(tasks);
}

サンプルなので ACI 完了後の処理は書いていませんが、並列処理をした結果を統合して 1 つにするといった処理が簡単に書けるのがメリットです。

並列実行数が少なければ特に問題はないですが、以下のドキュメントにある通り ACI には 5 分 / 1 時間当たりの最大作成数が設定されているので、何も考えずに 1000 個作成しようとするとエラーになります。

理想としては Semaphore 的なものを使って、同時に実行される ACI の数を効率的に制限したいところですが、Durable Functions では実現するのが難しいので現実的には並列数を調整するか、クオータの上限変更をリクエストするのが手っ取り早いです。

ACI の起動と削除を行う Sub Orchestrator を実装

残りは ACI の起動と削除を行うだけですが、処理が完了したことをどのように検知するかでポーリングとイベントの 2 種類が考えられるので、今回はそれぞれ用意してみました。

完了したことをポーリングで検知する

ポーリングで完了したことを検知するには、ACI のステータスを取得して Terminated になっていることを判定すれば良いので、Durable Functions の Timer を使って 10 秒ごとにチェックする処理を書きました。

[FunctionName(nameof(RunSubOrchestrator_Polling))]
public async Task<string> RunSubOrchestrator_Polling(
    [OrchestrationTrigger] IDurableOrchestrationContext context,
    ILogger logger)
{
    var id = context.NewGuid().ToString();

    await context.CallActivityAsync(nameof(CreateContainerInstance), id);

    while (true)
    {
        var instanceState = await context.CallActivityAsync<string>(nameof(GetContainerInstanceState), id);

        if (instanceState == "Terminated")
        {
            break;
        }

        await context.CreateTimer(context.CurrentUtcDateTime.AddSeconds(10), CancellationToken.None);
    }

    await context.CallActivityAsync(nameof(DeleteContainerInstance), id);

    return id;
}

この Orchestrator 内で ACI の起動と削除が完結しているので分かりやすいです。パッと見は無限ループにしか見えませんが CreateTimer を使っているので効率的にポーリングが実行されます。

実行すると Sub Orchestrator 単位で ACI が 1 つずつ起動されるので合計 5 つ起動されます。Sub Orchestrator に分離することで ACI の起動と処理完了まで Orchestrator 単位で待機されるので、特定の 1 つが極端に起動に時間がかかったとしても他の処理には影響は与えません。

ポーリング中のログとして、分かりやすくするために ACI のステータスを出力しました。ポーリング毎にステータスが変化して、最終的に Terminated になり処理が完了していることが分かります。

時間のかかる処理の場合はポーリングでも気にならないと思いますが、短時間で終わる処理の場合はポーリング間の待ち時間が大きく影響するので、そういった処理の場合はイベントを使うのが良いです。

完了したことをイベントで検知する

Durable Functions にはイベントが発火されるまで待機する機能が用意されているので、これを使ってコンテナ側から処理が完了したタイミングでイベントを発火させることで効率化を図ります。

イベントの待機は WaitForExternalEvent メソッドにイベント名を渡すだけで終わるので、Sub Orchestrator の実装は以下のように非常にシンプルです。

ただしコンテナ側にイベントを発火させるための URI を渡す必要があるので、コンテナを起動する際に CreateHttpManagementPayload を呼び出してインスタンス単位の URI を生成しています。

[FunctionName(nameof(RunSubOrchestrator_Event))]
public async Task<string> RunSubOrchestrator_Event(
    [OrchestrationTrigger] IDurableOrchestrationContext context,
    ILogger logger)
{
    var id = context.NewGuid().ToString();

    await context.CallActivityAsync(nameof(CreateContainerInstance), id);

    await context.WaitForExternalEvent(id);

    await context.CallActivityAsync(nameof(DeleteContainerInstance), id);

    return id;
}

[FunctionName(nameof(CreateContainerInstance))]
public Task CreateContainerInstance(
    [ActivityTrigger] IDurableActivityContext context,
    [DurableClient] IDurableOrchestrationClient client,
    ILogger log)
{
    var id = context.GetInput<string>();

    var payload = client.CreateHttpManagementPayload(context.InstanceId);

    var sendEventPostUri = payload.SendEventPostUri.Replace("{eventName}", id);

    return _containerInstanceService.CreateAsync(id, sendEventPostUri);
}

生成されたエンドポイント URI は呼び出し用のキーが付いているので安全に利用できます。

後は ACI の起動時に環境変数経由でエンドポイント URI を渡してあげて、Docker Image 側で処理が完了した時に HTTP POST を投げるようにするだけです。今回はサンプルなので curl を使って呼び出しています。

public class ContainerInstanceService
{
    private const string ContainerImage = "curlimages/curl:7.83.1";

    public Task CreateAsync(string id, string sendEventPostUri)
    {
        return _containerInstanceManagementClient.ContainerGroups.BeginCreateOrUpdateAsync(
            ResourceGroupName,
            $"ci-{id}",
            new ContainerGroup
            {
                Location = Location,
                Containers = new[]
                {
                    new Container
                    {
                        Name = id,
                        Image = ContainerImage,
                        Command = new[] { "/bin/sh", "-c", "curl -X POST -H 'Content-Type: application/json' -d '{}' $SENDEVENTPOSTURI" },
                        EnvironmentVariables = new[]
                        {
                            new EnvironmentVariable("SENDEVENTPOSTURI", sendEventPostUri)
                        },
                        Resources = new ResourceRequirements
                        {
                            Requests = new ResourceRequests
                            {
                                Cpu = 1,
                                MemoryInGB = 1.5
                            }
                        }
                    }
                },
                OsType = "Linux",
                RestartPolicy = "Never"
            });
    }
}

このコードを実行するとポーリングを使った時よりも全体的な処理時間は短縮されました。今回は処理内容が 1 秒以下で終わるものだったので、特に効果がありました。

Application Insights から Sub Orchestrator の実行ログを確認すると、以下のようにイベントの待機と発火されたことが分かるようになっています。

ARM REST API の呼び出し制限に引っかかることが無いので、可能な限りイベントを使った完了通知を行うべきですが、ポーリングは Durable Functions 向けの特別な処理が必要ないというメリットも大きいです。

ACI の GPU インスタンスを使う場合に一番力を発揮してくれそうなのですが、現状 ACI の GPU インスタンスを選ぶと起動に 10 分近くかかってしまうので若干利用しにくいです。

Azure Event Hubs SDK に追加された Buffered Producer Client を使って効率的なイベント送信を行う

2022 年 5 月の Azure SDK アップデートを確認していると、Event Hubs SDK に Buffered Producer Client が追加されたという情報を得ました。明らかに面白そうなので、実際に動かして挙動を確認してみました。

必要な Event Hubs SDK はバージョン 5.7.0 になります。このエントリでは全て C# 向けで確認していますが、他の言語にも用意されているのかは未確認です。

実際には Preview として少し前から使えるようになっていたみたいですが、今回のバージョン 5.7.0 のリリースで正式版になったようです。

この Buffered Producer Client が追加された理由などは、GitHub で公開されている設計ドキュメントに記載されています。API 名が若干異なる部分はありますが、基本はこのドキュメントの通りです。

要約すると Event Hubs SDK には 1 つのイベントを送信する機能と、バッチとして複数イベントをまとめて送信する機能が別々で用意されていますが、バッチをアプリケーション開発者が上手く管理して使うのは難しいので、バッファリングを追加することで SDK レベルでバッチで送信可能にするという実装です。

当然ながら 100 イベントを 1 つずつ送信するよりも、1 度に送信した方が効率が良いので、バッファリングによる遅延が許容できるシナリオではかなり有用だと思います。設計ドキュメント曰く Event Hubs 以外のメッセージング SDK には、全てでバッファリングが用意されているようです。

リファレンスはまだ正式リリース版に更新されていませんが、Preview バージョンが用意されています。

まずは基本的なコードを書いて、どのような挙動になっているのかを確認しておきます。通常の Event Hubs Producer Client とはイベント送信に利用するメソッドが異なります。

イベント送信に使うメソッド名から分かるように、このメソッドを呼び出しても即時送信されずバッファに追加されるだけです。実際のイベント送信はバックグラウンドかつ非同期で実行されるので、エラーハンドリングのために SendEventBatchFailedAsync イベントの登録が必須になっています。

この EnqueueEventAsync メソッドは送信を行わず基本的に失敗しないので、Fire and Forget と同じ使い方が出来ます。Event Hubs にテレメトリを送信する場合に完了まで待ってしまうと、アプリケーションの処理自体が遅くなるので本末転倒ですが、Buffered Producer Client を使うと簡単に回避できます。

using Azure.Messaging.EventHubs;
using Azure.Messaging.EventHubs.Producer;

var connectionString = "EVENT_HUB_CONNECTION_STRING";

var producer = new EventHubBufferedProducerClient(connectionString);

producer.SendEventBatchSucceededAsync += arg =>
{
    Console.WriteLine($"{DateTime.Now}: PartitionId = {arg.PartitionId}, Batch Size = {arg.EventBatch.Count}");

    return Task.CompletedTask;
};

producer.SendEventBatchFailedAsync += arg =>
{
    Console.WriteLine($"{DateTime.Now}: PartitionId = {arg.PartitionId}, Batch Size = {arg.EventBatch.Count}");

    return Task.CompletedTask;
};

Console.WriteLine("Started");

try
{
    for (var i = 0; i < 100; i++)
    {
        var eventData = new EventData($"test-{i}");

        await producer.EnqueueEventAsync(eventData);

        await Task.Delay(TimeSpan.FromMilliseconds(50));
    }
}
finally
{
    await producer.CloseAsync();
}

Console.WriteLine("Completed");

バッファリングされている関係上、アプリケーションが終了する前には CloseAsyncDisposeAsync メソッドを呼び出す必要があります。DI を使っていると問題ないですが、インスタンスを自前管理する場合には呼び出しておかないとイベントが失われる可能性があります。

デフォルト設定では 1 秒間隔でバッファリングされたイベントを送信するようになっているので、このコードを実行すると以下のように 1 秒間隔で送信完了のイベントが呼び出されます。

そして受け取る側を Azure Functions の EventHubTrigger を使って適当に実装してみたところ、こちらも 1 秒間隔でイベントが受信されていることが確認出来ました。

送信時と受信時のバッチサイズは一致しないので数は異なっています。Event Hubs にはパーティションという概念もあるので、バッチサイズに依存した処理を書くのは NG です。

内部実装として System.Threading.Channels が使われているので、信頼性とパフォーマンス面での不安はありません。パーティション単位で Channel<T> をキャッシュする実装になっているので、上手く使っている印象を持っています。個人的にも勉強になりました。

先ほどのサンプルではデフォルト設定のまま使いましたが、Buffered Producer Client にはパフォーマンスとスループットに関係するオプションがいくつか用意されています。

変更する可能性が高いのは MaximumWaitTimeMaximumEventBufferLengthPerPartition だと思います。

中でも MaximumWaitTime は名前の通りバッファリングされたイベントを送信する間隔です。これがデフォルトでは 1 秒になっているので、アプリケーションの特性に合わせて変更可能です。

例えば以下のように Buffered Producer Client 作成時に MaximumWaitTime を 5 秒に変更してみます。

var producer = new EventHubBufferedProducerClient(connectionString, new EventHubBufferedProducerClientOptions
{
    MaximumWaitTime = TimeSpan.FromSeconds(5)
});

実行してみるとイベントの送信が 5 秒間隔に変更されたことが確認出来ます。

もう一つの MaximumEventBufferLengthPerPartition はパーティション単位でのバッファリングする最大イベント数となります。デフォルトでは 1500 になっているので十分だと思いますが、メモリ使用量に関わってくる値なのでイベントのサイズによっては調整した方が良いケースもありそうです。

ドキュメントによるとイベント数の上限に達した場合は、Enqueue 系のメソッドがバッファに空きが出来るまでブロッキングされるとありますが、自分が検証した範囲ではそのような挙動になりませんでした。内部では Channel<T> を使っているのでブロッキングするはずなのですが、これが不具合なのかはよくわかっていません。ただし追加していってもイベントが欠落することはありませんでした。

最後にバッファリングとバッチの違いについて説明されているドキュメントを共有しておきます。

要約するとメソッド呼び出し時に失敗したかどうかが分かるのがバッチ、分からないのがバッファリングという感じです。用途に合わせて上手く使い分けていきましょう。

Azure Container Apps がカスタムドメインと証明書の追加に対応したので試した

今朝のアップデートで Azure Container Apps でも App Service のように、カスタムドメインを直接割り当てることが出来るようになりました。これまでは Front Door などを使う必要がありましたが、直接 Container App にカスタムドメインを割り当て出来るので、コスト面でも扱いやすくなりました。

Container Apps のカスタムドメインは App Service と異なり、常に HTTPS が要求されるのでカスタムドメインの設定と同時に証明書の追加が必要になっています。Managed Certificate は数か月後に対応予定らしいので、暫くは手動で証明書を管理する必要があります。

Container App にカスタムドメインと証明書を追加する

基本的な設定方法は App Service とほぼ同じですが、折角なので試しておきます。適当なドメイン名を追加しようとすると、必要な DNS レコードの情報が表示されます。

ドメイン所有チェック用の TXT レコード名は App Service と同じく asuid でした。この asuid の TXT レコードは常に必要というわけではなく、既に検証済みのスコープであればスキップできるようでした。

DNS レコードの追加後に Validate を押すと、正しく設定されたかをチェックしてくれます。設定に問題が無い場合は、以下のように Pass という表示になるので分かりやすいです。

Next ボタンを押して次に進むと、証明書を選択する画面に切り替わります。最初は存在しないはずなので Create new を押して、PFX をアップロードする必要があります。

フォームにはパスワードの入力が存在していませんが、証明書名を入力すると出てきます。若干謎な挙動ですが、証明書名は必須なので気にしないことにします。

例によって Key Vault からダウンロードしたパスワード無しの証明書は通らないので、OpenSSL などを使ってパスワードを付ける必要があります。証明書のアップロード後は元の画面に戻るので、カスタムドメインに割り当てたい証明書を選べば完了です。

ちなみにアップロードした証明書は Container Apps Environment に紐づく形になっています。

App Service と同様に Key Vault Certificate からのインポートが出来るようになると管理が非常に楽になるので、今後のアップデートに期待したいところです。

一通り試したところ Zone Apex と Wildcard にもしっかり対応しているようでした。

Let's Encrypt 証明書を利用する

手動で証明書をアップロードする方法は、Managed Certificate や Key Vault Certificate のインポートが当たり前になった今では懐かしい手順です。最近は自作の Key Vault Acmebot を使って、必要な証明書を数秒で発行するのが当たり前だったので、手動で証明書を発行して PFX でアップロードするのが苦痛です。

Key Vault への対応を待っていたつもりですが、Azure Apps の偉い人 Jeff Hollan 氏からメールで相談を受けたので、Container Apps 向けの Acmebot 開発を引き継ぐことになりました。

App Service 版の Acmebot と同様の機能を持っています。HTTP-01 は無理なので DNS-01 のみ対応している関係上、Azure DNS が必須となっています。将来的には Key Vault Certificate からのインポートが出来ると信じているので、Azure DNS のみの対応にしています。

デプロイして RBAC 設定を行うと、DNS ゾーンを選んで必要な証明書を発行して、Container Apps Environment にアップロード出来るようになっています。

Container Apps ではカスタムドメインの追加時に証明書を指定する必要があるので、そのあたりの設定も自動で行うようなオプションを持っています。Container App を指定すると、必要な asuid TXT レコードを作成してカスタムドメインの追加まで行います。

追加したドメインでブラウザからアクセスすると、以下のように Let's Encrypt の証明書が割り当てられていることが確認出来ます。複数ドメインにも対応していることが分かるはずです。

Managed Certificate では対応されないはずの SANs と Wildcard にも対応しているので、将来的にも一定の需要は残るのかなと思っています。

最優先は Key Vault 版ですが、App Service と Container Apps 版も継続して開発していく予定です。

Durable Functions を Azure Container Apps 上でスケーリング付きで実行する方法を検証した

先日 Twitter で以下のようなツイートが流れてきた時に、Azure Container Apps 上で Durable Functions を正しく実行できるのかを調べることを思い出したので一通り検証しました。

ツイートで紹介されている Discussion にあるように、AKS であれば特に問題なく実行できるようです。

Azure Functions は Linux での Docker サポートや、それこそ Kubernetes サポートが存在しているから分かるように Docker が動く環境と Azure Storage があれば App Service 以外でも実行可能です。

既にトニーさんが Container Apps 上で Azure Functions の Docker サポートを使って試されています。

実際に HttpTrigger やバックグラウンド処理としての QueueTrigger であれば難しくはないのですが、0 へのスケール対応と Durable Functions については工夫する必要がありました。

今回の検証で Durable Functions のユースケースは一通り対応できたと思うので参考にしてください。ここから先は Durable Functions のプロジェクト作成から順を追って書いてあります。

Dockerfile 付きで Durable Functions を作成

今回は Visual Studio 2022 と .NET 6 を使って Durable Functions のプロジェクトを作成しました。

プロジェクトの作成時に Docker を有効にするオプションがあるので、チェックを入れて作成すれば Azure Functions に必要な Dockerfile が同時に生成されます。

一先ず Durable Functions の実装はテンプレートで生成されたものをそのまま使う形で問題ありません。

ここで最初に注意するポイントとして、生成された Dockerfile には X-Forwarded-Host などを処理するための環境変数が抜け落ちているので、ASPNETCORE_FORWARDEDHEADERS_ENABLED を環境変数に追加します。

以下のドキュメントにあるように .NET Core 3.0 からはデフォルトで追加されているはずですが、Azure Functions の Docker Image は独自の Image から生成されているので含まれていないという罠です。

この設定をしておかないと Durable Functions が管理用に生成する URL が http になってしまいます。地味にはまるポイントになってしまうので注意しましょう。ちなみに Container Apps 関係なく発生します。

Docker Image を作成して Container Registry に発行

最低限必要な Durable Functions のプロジェクトは作成できたので、自動生成された Dockerfile を使って Image を作成して Container Registry に発行します。

Visual Studio は Docker 対応が進んでいるので、Azure Functions に発行するのと同じ手順で Docker Image の作成から Container Registry への発行まで行えます。

通常の ASP.NET Core アプリケーションであれば、発行先に Container Apps が選べるのでデプロイまで自動で行えるのですが、Azure Functions の場合は出てきません。

GitHub にリポジトリを生成しておけば、Container App の CI/CD 設定から GitHub Actions の Workflow を生成できるので、実際に使う場合はこっちの方法を選ぶはずです。

少し脱線しましたが、これで Durable Functions を実行するための Docker Image が作成されました。

必要な Storage Account と App Insights を作成

Azure Functions は全体的に Azure Storage に依存しているので、新しく Storage Account を作成する必要があります。特に Durable Functions の場合は Azure Storage の全ての機能を使っています。

同時にモニタリング用の Application Insights を作成しておくと便利です。それぞれのリソースは必ず Container Apps と同じリージョンにデプロイしてください。

Container App を必要な環境変数付きで作成

Durable Functons の実行に必要なリソースは作成したので、先ほど作成した Docker Image を使って Container App をデプロイしていきます。環境変数の設定が必要になりますが、Azure Portal からのデプロイ時には環境変数を同時に設定できないので後から追加します。

最低限の実行に必要な環境変数は APPLICATIONINSIGHTS_CONNECTION_STRING / AzureWebJobsStorage / WEBSITE_SITE_NAME の 3 つになります。接続文字列は Secrets に登録して使いまわしましょう。

Application Insights の接続文字列も Secrets に入れても良いですが、実質的に公開情報になっているので環境変数に直接登録することにしました。

3 つのうち異色なのが WEBSITE_SITE_NAME です。Durable Functions が内部で参照しているので、明示的に設定しないと謎の Webhook エラーが出てハマることになります。

設定を追加してプロビジョニングが完了すれば、以下のように見慣れた Azure Functions のページが表示されます。設定に問題がある場合は Function host is not running というエラーメッセージが表示されるので、Log Analytics を使って原因を探る必要があります。

Function Host が問題なく動作していることが確認出来れば、Durable Functions のオーケストレーターを HttpTrigger 経由で起動してステータスを定期的に確認します。

すると以下のようにオーケストレーターの実行結果が返ってくるはずです。

これで基本的な Durable Functions の機能は、問題なく動作していることが確認出来ました。

Queue ベースのスケーリングルールを追加

サンプルのように HttpTrigger によって起動されて、オーケストレーターの実行が短時間で済む場合は問題ないですが、Durable Functions は Fan-in/Fan-out や Timer といった非同期な並列処理が行えるのが魅力です。

単純に Container Apps に Docker Image をデプロイしただけでは 1 つのレプリカから増えることはないですし、0 へのスケールも実現することが出来ません。スケーリングを実現するために Queue ベースのルールを追加する必要があります。Durable Functions が利用している Queue の詳細は以下にあります。

内部では workitems と control で 2 種類の Queue が生成されているので、それぞれに対してスケーリングルールを追加するとメッセージ数によってレプリカが増えることになります。

実際に必要な Queue に対してスケーリングルールを追加すると以下のようになります。control 用の Queue はデフォルトで 4 つ生成されるので少し手間です。

レプリカの最小数が 0 になっていることに注目してください。アイドルが続くと 0 にスケーリングされるので、以下のように CreateTimer を使った処理を書くと、ルールが正しく設定されていないと動作しません。

await context.CreateTimer(context.CurrentUtcDateTime.AddHours(1), System.Threading.CancellationToken.None);

実際にオーケストレーターの先頭に CreateTimer の呼び出しを追加して、意図的にアイドル状態にすることでレプリカの数を 0 にスケーリングさせてみます。

余談ですが CreateTimer は Queue メッセージの非表示タイムアウトを利用して実現されているので、Storage Explorer から非表示状態のメッセージ数を調べることで動作を確認出来ます。

このメッセージが見えるようになれば Container Apps によってレプリカが 1 に増やされるはずです。実際に 1 時間待機して Metrics を使ってレプリカの起動を確認しました。

グラフからはちょうど 1 時間後にレプリカが 1 つ起動されていることが確認出来ます。リクエスト数は 0 のままなので HTTP によって起動されたものではないことが確認出来ます。

実行結果は代わり映えがしないので出しませんが、ステータス API を叩くと正常にオーケストレーターは終了して結果が返ってきていました。更にメッセージ数が設定数を超えるとレプリカは増やされます。

スケーリングルールが少し面倒ですが、十分 Durable Functions を Container Apps 上で動かすことが出来ました。常に 1 つのレプリカが必要なバックグラウンド処理と違って 0 へのスケールが可能なイベントドリブンとなるので、コスト的なメリットもありそうです。

Azure Container Apps の組み込み Authentication を試した

数日前に Azure Container Apps でも App Service / Azure Functions と同様の組み込み Authenticationがサポートされました。App Service Authentication はかなりの高頻度で使っている機能なので、Container Apps でも間違いなく便利に使えるはずです。

App Service と同様のアプリケーション側に近い機能が実装されたということは、Container Apps の開発方針も透けて見えるようで今後にも期待が高まります。

ちなみに App Service と同様と書きましたが、内部で利用されているモジュールは全く同じものらしいです。Sidecar で実現しているのは Linux の App Service と同様ですね。*1

モジュールが同じということは、当然ながら App Service で利用出来た機能は使えるはずですし、問題もそのまま受け継いでいることになります。

例外的に Token Store は非対応の機能となっているようです。Token Store 自体が App Service の共有ファイルを利用している事情もあるので、Container Apps でのサポートは SWA と同様に望み薄かもしれません。

Azure AD 認証を有効化して試す

とりあえずいつも通り Azure AD 認証を Azure Portal から有効化してみます。見て分かるように Azure Portal での画面は App Service / Azure Functions と同じです。

なので詳しい説明はしませんが、Azure AD アプリケーション周りも全く同じ設定で作成されます。つまり Hybrid Flow が必要になるので手動作成時には注意が必要です。

設定を追加してしまえば、後は App Service と全く同じ挙動です。Azure AD 認証が自動的に行われて、テナントに存在するユーザーであればページが表示されます。

Authentication の機能とは関係なく App Service でも同様なのですが、Azure Portal から Azure AD 認証を追加すると Issuer URL が sts.windows.net になるのでログイン時にリダイレクトが 1 回増えます。Issuer URL を login.microsoftonline.com に変更することで無駄なリダイレクトを排除できます。

ログインしているユーザー情報は X-MS-CLIENT-PRINCIPAL-NAME などの HTTP リクエストヘッダーで渡されるので、アプリケーションでの処理は App Service で行っていたものと同じで良いです。あと Token Store が存在しないので /.auth/me は 404 になっています。

ARM Template や Bicep で利用可能な定義は以下のリポジトリで公開されています。

項目自体は App Service とほぼ同じなので、ARM Template を書いたことがある人ならすんなり理解できるはずです。認証エンドポイントが /.auth から始まるのが気に食わない場合は変更できます。

Front Door などを前段に置いた際の注意点

Container Apps は App Service と同様に自動生成されたホスト名が提供されていますが、通常はカスタムドメインを割り当てることになるので Front Door を置いて確認しておきます。

Front Door の作成時に Container Apps は出てこないので Custom を選択して、ホスト名を手動で入力すると問題なく利用できるようになります。将来的には対応されるでしょう。

作成後に Front Door のホスト名で Authentication 設定済みの Container Apps にアクセスすると、以下のようなエラーが出ることがあります。

あるいはログインが成功してもホスト名が Container Apps のものに戻ってしまっているはずです。

既に知っている方も多いと思いますが、この問題は X-Forwarded-Host がデフォルトでは無視されることに起因していて、もちろん App Service Authentication でも発生します。

もちろん回避方法は用意されているので、ARM Template や ARM Explorer から設定を変更することで対応可能です。詳しくは以下のエントリを参照してください。

ARM Explorer で forwardProxy の設定を変更すると、Front Door のホスト名でアクセスした場合でもそのホスト名が維持されたままリダイレクトされます。

割とはまる部分なので頭の片隅に置いておきたい設定になります。Azure Portal から設定できると良いのですが、今のところは隠し機能みたいになっていてあまり良くないです。

Azure Functions (Windows / Linux) へのデプロイを行う方法と挙動の違いをまとめた

最近は Visual Studio 2022 + C# 以外で Azure Functions を作成することが増えてきました。その場合は主に Visual Studio Code を使ってプロジェクト作成からデプロイまで行うのですが、Azure Functions を実行する OS とデプロイに利用する方法で若干挙動が変わるので簡単にまとめました。

環境毎に確認した結果は以下の通りになるので、ここだけ読めば大体は問題ありません。ちなみに言語は全て Node.js (TypeScript) で試しています。

Windows ASP Windows FP Linux ASP Linux FP
Visual Studio Code Run From Package Run From Package Zip Deploy Zip Deploy
Azure Functions Core Tools Run From Package Run From Package Run From Package Run From Package (URL)
Azure CLI Zip Deploy Zip Deploy Zip Deploy Zip Deploy
GitHub Actions (Publish Profile) Zip Deploy Zip Deploy Zip Deploy Zip Deploy
GitHub Actions (RBAC) Run From Package Run From Package Zip Deploy Zip Deploy

言語によっては多少変わりますが、基本はこのような挙動になります。ASP は App Service Plan で FP は Functions Premium のことを指しています。Consumption Plan に関しては実質的に Run From Package 強制なので省略しています。

一目で分かるように Windows の場合は安定して Run From Package が使われますが、Linux ではまちまちになっているので注意が必要です。Run From Package は Azure Functions では公式に推奨される方法となっていますし、アトミックなデプロイが行えパフォーマンス的にもメリットも多いです。

Zip Deploy と Run From Package といった Azure Functions が提供するデプロイ API については、何回か書いているので説明は必要ないと思いますが、公式ドキュメントが良い感じにまとまっているので紹介します。

Visual Studio Code や GitHub Actions からデプロイする際には、基本的にアプリケーションを ZIP にしたものをプッシュする Zip Deploy が使われます。

ここで若干わかりにくい部分があって、Zip Deploy には以下の 3 つのバリエーションが存在しています。

  • Zip Deploy
    • Remote Build (Kudu / Oryx でビルド)
    • Local Build (開発環境 / GitHub Actions でビルド)
      • Run From Package

実際には Remote Build でも一部の OS と言語の組み合わせでは、自動的に ZIP が作成されて Run From Package として実行されるケースもありますが、複雑になるので扱っていません。

Zip Deploy を行っていても、実際にはリモートでビルドが行われる場合があります。Python の場合はパッケージにプラットフォームに依存するものが含まれている可能性があるので、デフォルトで大体この挙動です。

ドキュメントにあるように Zip Deploy + Run From Package は全ての App Service Plan と OS の組み合わせで推奨されています。ただし App Service Plan と OS の組み合わせでは自動で無効化されています。

Local Build を行っていても明示的に WEBSITE_RUN_FROM_PACKAGE を設定していない限りは、デプロイ時に unzip されて wwwroot に展開されます。Node.js では unzip 後に npm install が実行されます。Run From Package では追加の処理を必要としないポータブルな形で ZIP を作成する必要があります。

ここから先はデプロイ方法毎での詳細になるので、興味のある方のみ読んでもらえれば良いです。

Visual Studio Code

Azure Functions へのデプロイを Visual Studio Code の拡張機能で行うと、Windows 向けの場合は自動的に Run From Package が有効になりますが、Linux 向けでは普通の Zip Deploy のままになります。

Windows とは異なり Linux では自動で WEBSITE_RUN_FROM_PACKAGE は追加されません。

Linux の App Service Plan と Functions Premium の場合は、Visual Studio Code からのデプロイ前に WEBSITE_RUN_FROM_PACKAGE の設定を追加しておくと、VS Code が自動的に設定を読み取って Run From Package としてデプロイしてくれます。

Windows と同じ感覚でいると Run From Package になっていないことが多いので注意が必要です。

Azure Functions Core Tools (func コマンド)

挙動に若干癖があるのが Azure Functions Core Tools を利用したデプロイです。通称 func コマンドを使ったデプロイには Azure CLI などで事前にログインしておく必要があります。

Azure Functions 専用ツールなのでオプションが非常に豊富ですが、デプロイ時の動作を理解しておかないと混乱してしまうと思います。Remote / Local Build を明示的に指定できるのが特徴です。

出来る限り Run From Package を使うようになっていますが、Linux の Functions Premium へのデプロイ時には何故か URL を指定した Run From Package になってしまいます。Functions Premium で URL 指定を使うメリットは無いため、明示的に 1 を設定してからデプロイするのがお勧めです。

Azure CLI

Azure CLI の Web Apps / Azure Functions コマンドに Zip Deploy 機能が用意されています。このコマンドは Azure Functions Core Tools の一部機能を持っていて、Remote / Local Build が指定可能です。

Remote / Local Build の設定は SCM_DO_BUILD_DURING_DEPLOYMENT が追加されるかどうかぐらいの差になっています。未指定の場合でも Linux の場合は追加されます。

それ以外の設定は特に変更されないため、Run From Package を行う場合には WEBSITE_RUN_FROM_PACKAGE 設定を事前に追加しておく必要があります。

Azure CLI だけあれば良いので、Cloud Shell からもデプロイ出来るお手軽さがあります。GitHub Actions や Azure Pipelines には Azure CLI が大体入っているので、CI/CD でも利用可能です。

GitHub Actions (functions-action@v1)

GitHub Actions を使ったデプロイでは認証情報は Publish Profile と RBAC (Service Principal) のどちらかで渡しているかによって挙動が変わります。理由としては Publish Profile には ARM レベルでの書き込み権限が無く、Kudu 経由のデプロイか設定の読み込みしか行えないためです。

Publish Profile ではなく RBAC (Service Principal) を使うと設定変更が行えるので、Windows 向けの場合は自動的に WEBSITE_RUN_FROM_PACKAGE 設定が追加されます。

実際の挙動としては Visual Studio Code を使ったデプロイと同じになるので、開発立ち上げ時には Visual Studio Code からデプロイを行い、実際の本格的な開発フェーズでは GitHub Actions から RBAC を使ってデプロイというのが整合性が取れていてわかりやすいです。

最近は GitHub Actions の OpenID Connect を使って、シークレットの管理無しに Service Principal が使えるようになっているので、多くのアプリケーションを管理する必要がある場合は Service Principal を作成して、RBAC で権限管理を行う方が簡単です。

Azure App Service の Regional VNET Integration が Basic SKU でも利用可能に

これまで実質的に Premium V2 / V3 が必要だった Regional VNET Integration が Basic でも利用可能になりましたが、例によって使えるかどうかはデプロイされている Scale unit に依存するので簡単にまとめます。

正確には Regional VNET Integration と Private Endpoint が Basic SKU で利用可能になりました。開発環境は Basic を使い、本番環境は Premium V2 / V3 を使うことでコストダウンが見込めます。

既に Regional VNET Integration は多くの場面で使われていると思うので説明はしませんが、興味がある方は以下のエントリを参照してください。今では App Service 最重要機能と言えます。

App Service Team Blog では Basic SKU で Regional VNET Integration が利用可能になる条件が記載されていませんでしたが、公式ドキュメントはアップデートされているので条件が追記されています。

公式ドキュメントには以下のように記載されています。要約すると Basic で Regional VNET Integration が利用可能になる条件は、今までの Standard で利用可能になるときと同じです。

つまり VMSS ベースの Scale unit にデプロイされている必要があります。

The feature is available from all App Service deployments in Premium v2 and Premium v3. It's also available in Basic and Standard tier but only from newer App Service deployments.

Integrate your app with an Azure virtual network - Azure App Service | Microsoft Docs

App Service における VMSS ベースの Scale unit について詳しく知りたい方は、以前書いた各エントリを参照してください。Cloud Services ベースと異なりシステムドライブ周りに違いが出ているのと、Cloud Services のリタイアに伴って VMSS に移行が行われると思うので、知っておいて損はありません。

現時点では App Service Plan のデプロイ時に VMSS ベースの Scale unit を確実に選択するためには、公式ドキュメントにもあるように Premium V3 を指定してデプロイするしか方法がありません。

Japan East は Premium V3 が指定できるので問題ないですが、Japan West は現時点で Premium V3 に対応していないリージョンとなるので問題が発生します。

If you want to make sure you can use the feature in a Standard App Service plan, create your app in a Premium v3 App Service plan. Those plans are only supported on our newest deployments. You can scale down if you want after the plan is created.

Integrate your app with an Azure virtual network - Azure App Service | Microsoft Docs

実際のところ Japan West は Premium V3 を指定できませんが VMSS ベースの Scale unit はデプロイされているので、運が良ければ Regional VNET Integration が Basic / Standard でも利用できます。

確認のために Japan West に Premium V2 を指定して新規にデプロイをしてみると、無事に VMSS ベースの Scale unit が割り当てられました。簡単な確認方法は Kudu でシステムドライブが C: になっているか、ホスト名で nslookup を実行して cloudapp.azure.com が返ってくるかを見ることです。

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

このデプロイでは VMSS ベースの Scale unit が割り当てられたので、Basic に下げた状態でも Regional VNET Integration を構成することが可能です。実際に以下のように問題なく VNET 設定が行えました。

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

Japan West については運の要素が出てきてしまうので、基本的には確実に Premium V3 を指定できる Japan East を中心に利用するのをお勧めしています。

最初に Premium V3 を指定してデプロイしてしまえば Basic に下げても VMSS のままになるので、Basic SKU で Regional VNET Integration を使うためには少し追加の作業が必要になります。ARM Template や Terraform を使っている場合は 2 回適用する必要があるので、コマンド 1 発でデプロイ可能とはいきません。

全てが VMSS ベースになってほしさしかないので、Cloud Services のリタイアを待ち続けましょう。

あえて触れてこなかった Private Endpoint に関しては Scale unit の違いによる影響はなく、全てのデプロイで既に利用可能な状態なので VMSS かどうかは関係なく利用できます。

App Service の一番安い占有プランである Basic で Regional VNET Integration と Private Endpoint がサポートされたことで、ARM Template や Terraform を使った場合にコストを最適化することが簡単になりました。

IaC を使って開発・本番でそれぞれ VNET を使った構成を用意するためには Standard 以上が必要だったので、上手く Basic を使って開発環境のコストを最適化していきましょう。

Azure Cloud Services (Classic) のリタイア予定と PaaS のインフラが VMSS へ移行されている話

PDC 2008 で Azure が発表された時から存在していた純粋な PaaS と言える Cloud Services (Web Role / Worker Role) ですが、ついにリタイアの日程が 2024/8/31 に決まったようです。

今では Cloud Services を直接利用しているケースはかなり少ないと思いますが、Azure のコアサービスを支える屋台骨として内部ではかなり利用されています。リタイアに伴って依存していたサービスも同時にリタイアするようなので、詳しくは世界のブチザッキを確認してください。

厳密には ARM に対応した Cloud Services (extended support) が存在していますが、ダラダラ塩漬けにするよりも気合入れて App Service や Container 対応した方がメリットが多いはずです。

今回リタイアが発表されていないサービスでも、内部では Cloud Services にべったり依存しているものが存在します。大体は VMSS への移行が進んでいるので振り返っておきます。

Cloud Services から VMSS へ変わったもの

VMSS は単純な VM とは異なり、PaaS 寄りの機能を持っているので PaaS と IaaS の間ぐらいに存在しているサービスという認識です。直接使うのは面倒ですが、PaaS のインフラとしては良い感じです。

App Service (Web Apps / Azure Functions)

Cloud Services に依存しているサービスの代表格が App Service です。App Service も今となっては Azure の初期からあるサービスで、当時は VMSS なんて便利なものはなかったので Cloud Services ベースです。

とはいえ最近は Premium V3 の追加のタイミングでインフラ周りが大幅に更新されています。Premium V2 の時も Regional VNET Integration への対応でアップデートが行われましたが、Premium V3 は劇的な変更です。

既に何回か Cloud Services から VMSS に変更になったことを書いていますが、既存のリソースに対しては VMSS 対応が行われていないので、Cloud Services がリタイアする前には全てが VMSS ベースにアップグレードされるのでしょう。そうじゃないと困るので、この辺りはスケジュール公開待ちです。

VMSS ベースになることでシステムのドライブレターが変化するので、Cloud Services 前提で D:\ を触るようなコードを書いていると急に動かなくなる可能性もあります。ドライブレターを決め打ちにせずに、環境変数 SYSTEMDRIVE などを使うようにしましょう。詳しく知りたい方は以下のエントリを参照してください。

既に VMSS ベースの App Service は広く展開されてテストされているので、ドライブレターに依存したコードを書いていない限りは互換性に関して特に心配はないでしょう。

Cloud Services ベースから VMSS ベースへの切り替えが完全に自動的に行われるのか、それともタイミングをある程度制御できるのかは気になるところです。ぶっちゃけ個人的にはさっさとコスパに優れた Premium V3 に移行してしまうのをお勧めしています。

App Service Environment

意外にインパクトが大きいと思うのが App Service Environment v1 と v2 が同時にリタイアすることです。ASE v3 のリリースに時間がかかったこともあり、かなりのケースで ASE v2 を使っていると考えられますし、要件的にもアーキテクチャの大幅な変更が難しいケースで使っていることが多そうです。

アナウンスにも記載のある通り ASE v3 は VMSS ベースとなっています。従って構成はマルチテナントの App Service とほぼ同じになっていると考えて良さそうです。

ASE v3 はデプロイに 2 時間以上かかるのと、マルチテナントの App Service でネットワーク周りの機能が大幅に強化されたため試してはいません。完全に VNET 内に閉じる必要がある場合のみ検討する予定です。

自動マイグレーション機能が提供されていますが、ASE が必要なアプリケーションに対して気軽に実行できるものではないですし、そもそも日本リージョンに非対応なので新規に ASE v3 を作成して移行するのが安全でしょう。このタイミングで Availability Zones への対応や App Service への移行も検討して損はないです。

API Management

若干意外かもしれませんが API Management も Cloud Services に依存したサービスの一つです。

Consumption は App Service 上で動いているので、自動的に VMSS ベースに切り替わると思いますが、それ以外のプランでは作成した時期によって VMSS かどうかが決まります。

App Service は VMSS ベースに変更したい場合は新しく Premium V3 の App Service Plan を作成して、丸ごと作り直す必要がありますが API Management は設定を変更すると VMSS ベースの stv2 に移行できます。

若干移行の方法に癖があるので詳しくは公式ドキュメントに任せますが、ネットワーク周りの新機能を使おうとすると大体 VMSS になるようです。

手動で移行を行わなくても Cloud Services がリタイアする前に、自動で VMSS ベースにアップグレードが行われると思いますが、stv1 で作ってしまわないように ARM Template や Bicep で新規作成する際には API バージョンを注意しておきたいです。

おまけ : 最初から VMSS だったもの

当たり前ですが VMSS がリリースされた後に公開されたサービスは、大体が VMSS ベースになっているはずです。分かりやすいところでは AKS や Service Fabric は VMSS 上に構築されています。Cloud Services は Availability Zones に対応していないので、AZ 対応サービスは大体 VM か VMSS が使われています。

つまり AKS を基盤にしている Container Apps や Service Fabric を基盤にしているはずの Cosmos DB なども、同様に VMSS ベースで提供されていると考えられます。なので Azure のサービスは既に多くが VMSS ベースになっているっぽいです。

Azure の PaaS / Serverless を利用していると、インフラ側の進化に最小の労力で乗っかっていけます。今後も共同責任モデルに則って、良い感じに楽できる部分は全力で楽していきましょう。

新しい Azure Front Door が GA した件と Application Gateway との使い分けについて

1 年ぐらい Public Preview だった Azure Front Door の Standard / Premium がやっと GA しました。

ぶっちゃけ機能的には劇的な変化はないですが、長らく問題となっていた Subdomain Takeover 対策が標準で入ったことや、Private Link を使ってオリジンへの安全な接続が行えるようになっているのは大きいです。

これまでの Azure CDN (Microsoft) や Front Door からの移行は今後数か月以内に対応されるようなので、準備が整った時点でサクッと移行するのが良いでしょう。CDN 周りの移行は DNS レベルで簡単に切り替えできるので、さっさとやっておくのも手です。

Azure Portal から作成する際には新しい Front Door とこれまでの Front Door / Azure CDN が全て統合されているので、最初は若干混乱しそうです。新しい Front Door 推しなのが良く伝わります。

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

ちなみに Front Door Standard / Premium の GA と同時に Azure CDN (Microsoft) と以前の Front Door は統合されて Classic 扱いになりました。既存リソースの種類が変わっているので気を付けましょう。

新しい Front Door Standard / Premium とこれまでの Front Door の比較表が出ていますが、当然ながら機能的には全て上回っているので Classic となったサービスを使う長期的なメリットはないです。

気になる価格に関してですが、これまでの Front Door はルーティングルールの数でも課金されていましたが、Front Door Standard / Premium では時間当たりの固定料金 + リクエスト数 + 転送量という分かりやすいものになっています。運用していたルーティングルールの数によっては、かなり安くなることもありそうです。

特徴としては Standard でも WAF の料金が含まれているので、使わないと損した気分になりそうです。

Private Link サポートの変更

Front Door Standard / Premium が Public Preview になった時に Private Link 周りの確認をしていましたが、GA では利用できるリージョンの拡大と対象のサービスが限定されたようです。

リージョンに関しては Availability Zone に対応していれば良いみたいなので、もちろん Japan East では問題なく利用できます。

対象となるサービスは Azure Storage (Blob) / App Service / ILB となっていますが、Azure Portal からは ILB が選択肢に出てこないので若干怪しい気がしています。

Origin support for direct private end point connectivity is limited to Storage (Azure Blobs), App Services and internal load balancers.

Secure your Origin with Private Link in Azure Front Door Premium | Microsoft Docs

プレビュー中は Static Web Apps に対しても使えていましたが、若干制限が厳しくなっています。とはいえメインは App Service に対して使うことになる気はします。

Rule Engine の改善

これまでも Azure CDN と Front Door にも Rule Engine は用意されていましたが、Standard / Premium の Rule Engine は条件式に正規表現が使えるようになったみたいです。

後方参照などは使えないので、あくまでも条件式のマッチに使うだけという形です。とはいえ条件式をシンプルに書けるようになりそうです。

正直なところ GA アナウンスの記事を読んでいて、イマイチ意味が分からなかったのがサーバー変数のサポートでした。IIS の URL Rewrite を書いたことがある人にはすんなり理解されそうです。

今すぐに良い利用方法は思いついていないですが、簡単な文字列操作も出来るようなのでピンポイントで役に立つ時が来そうです。意外に使いどころが無さそうな予感は少しあります。

Application Gateway との使い分け

よく質問されるのが Front Door と Application Gateway のどちらを利用するべきかというものです。それぞれ L7 のロードバランサーという共通点はありますが、Front Door はグローバルサービスで Application Gateway は特定のリージョンの VNET にデプロイされるという大きな違いがあります。

Front Door の公式ドキュメントの FAQ でも使い分けについては記載されています。

Application Gateway は名前の通り VNET に閉じたアプリケーションに対して、外部からアクセス可能なエンドポイントを用意します。同時に VNET 内での負荷分散や TLS オフロードも行ってくれますが、あくまでも特定の VNET に紐づくサービスです。

Front Door が Private Link に対応したことで VNET に閉じたアプリケーションに安全にアクセスできるようになりましたが、グローバルサービスなので負荷分散という観点では主に複数リージョン間になります。

ちょいちょい App Service の前に Application Gateway を入れているアーキテクチャを見ますが、App Service 自体が Application Gateway に相当する仕組みを内部で持っているので、正直なところ無駄なリソースです。App Service の前に Front Door を入れる構成の方がほぼ間違いはありません。

基本的に Application Gateway は使わないようにして Front Door を使うようにしていますが、クライアント証明書認証が必須な場合には Application Gateway を利用する必要が出てきます。

クライアント証明書認証は App Service でも対応していますが、送信された証明書の妥当性の検証は自前で行う必要があるので、正しく実装するには証明書周りの知識が必要になります。

Application Gateway では CA 証明書をアップロードしておけば自動的に検証してくれるため、App Service に比べると安全かつ簡単に対応できます。