しばやん雑記

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

.NET Isolated 版の Durable Functions を使った開発を始める

Ignite のタイミングで Flex Consumption が GA したこともあり、そろそろ .NET Isolated 向けの Durable Functions を使いたい気持ちが高まってきたのでしっかりと確認しておくことにしました。以前調べたときは対応していない機能も多かった気がするのですが、今では API も整理されて完成度がかなり高まっています。

詳細は説明しないので以下の公式ドキュメントにも目を通しておくと吉です。

.NET Isolated 向けの Durable Functions を使いたいモチベーションの中でも最大なものとして、これも Ignite で発表された Durable Task Scheduler の存在があります。Durable Task Scheduler はこれまでの Durable Functions に対してスケーラビリティとオブザーバビリティを提供してくれます。

まだ絶賛 Preview 中ですが、自分が触っている感じではかなり Durable Functions の運用が行いやすくなっているので、早く GA して欲しいという気持ちしかありません。

Durable Task Scheduler のブログには書かれていませんが、Private Preview に参加している人間向けのドキュメントには .NET Isolated などの分離ワーカー向けのみ導入方法が書かれていました。将来的には .NET Isolated などの分離ワーカーに一本化されるので、何処かのタイミングで In-Process からの移行が必要なので、この機会にやっておくのは一つの手です。

今回 DTS を利用するために .NET Isolated 向けの Durable Functions を気合いを入れて試したという話になります。後述しますが DTS は In-Process でも利用できると知ったのはこのブログをほぼ書いた後でした。

.NET Isolated 版の Durable Functions は In-Process からクラスやインターフェース名が変更されたぐらいで正直大きな違いはありません。内部的には In-Process と .NET Isolated で同じライブラリが使われているので挙動についても一貫性があるはずです。

とりあえず Visual Studio を使って新しい Durable Functions のプロジェクトを作成すると、テンプレートで以下のようなオーケストレーターとアクティビティが生成されます。

[Function(nameof(Function1))]
public static async Task<List<string>> RunOrchestrator(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    ILogger logger = context.CreateReplaySafeLogger(nameof(Function1));
    logger.LogInformation("Saying hello.");
    var outputs = new List<string>();

    // Replace name and input with values relevant for your Durable Functions Activity
    outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo"));
    outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Seattle"));
    outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "London"));

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

[Function(nameof(SayHello))]
public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext)
{
    ILogger logger = executionContext.GetLogger("SayHello");
    logger.LogInformation("Saying hello to {name}.", name);
    return $"Hello {name}!";
}

正直なところ、このテンプレートを見るだけで .NET Isolated 版の Durable Functions については大体理解出来るはずです。多少メソッドの引数で取るクラスの型が変わっていますが、アクティビティの呼び出しについてはこれまで通りなので悩むことは無いでしょう。

既存の Durable Functions の移行については後述しますが、大体は .NET Upgrade Assistant を使ってコンパイルエラーを潰していくという流れで大体問題ありません。現時点では Durable Functions には未対応となるため、規模によりますが多少の手作業は発生します。

テンプレートで生成されたレベルのコードでは .NET Isolated へ移行したメリットがあまりないので、次は Source Generator を使って Durable Functions のよくあるミスをコンパイル時に検出できるようにします。

Source Generator を有効化する

実は In-Process の Durable Functions でもプレビューで公開されていた Source Generator ですが、今回 .NET Isolated では公式ドキュメントで扱われるレベルになりました。この Source Generator はアクティビティ毎に専用の拡張メソッドを生成してくれます。

使い方はドキュメントに書いてある通りですが、以下の NuGet パッケージをインストールするだけなので非常にお手軽です。まだプレビューなのが少し不安になりますが、複雑なコード生成を行っているわけではないので問題なさそうです。

Durable Functions を使った開発でよくあるミスは、アクティビティのシグネチャを変更しても呼び出し側の修正を忘れてしまい、実行時に型エラーになるというものがあります。これは Durable Functions のアクティビティ実行はキューを用いた間接呼び出しなことに起因します。Source Generator が出るまでは回避が難しい問題でしたが、ようやく安全にアクティビティを呼び出せるようになります。

関数ベースで利用する

Durable Functions の Source Generator には 2 つの機能が用意されていて、1 つは前述したようにアクティビティ毎に専用の拡張メソッドを作成する機能です。

Source Generator の NuGet パッケージをインストールしてビルドすると、以下のように IntelliSense でアクティビティ用の拡張メソッドが表示されるようになります。

重要なのは Source Generator がアクティビティのシグネチャを読み取って、適切な型の拡張メソッドを生成してくれるという点です。これによりアクティビティの型を変えた場合には拡張メソッドの型も変更されるので、もちろんコンパイルエラーになるため安全です。

生成された拡張メソッドは以下のようにシンプルなものですが、自動で生成されるというのが最高です。

Source Generator によって生成された拡張メソッドを使うと、以下のように非常にすっきりとオーケストレーターを書くことが出来ます。拡張メソッドによって型安全なだけではなく、アクティビティの名前を間違えることもなくなるので積極的に使っていきたい機能です。

[Function(nameof(Function1))]
public static async Task<List<string>> RunOrchestrator(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    ILogger logger = context.CreateReplaySafeLogger(nameof(Function1));
    logger.LogInformation("Saying hello.");
    var outputs = new List<string>();

    // Replace name and input with values relevant for your Durable Functions Activity
    outputs.Add(await context.CallSayHelloAsync("Tokyo"));
    outputs.Add(await context.CallSayHelloAsync("Seattle"));
    outputs.Add(await context.CallSayHelloAsync("London"));

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

既存の In-Process な Durable Functions から .NET Isolated へ移行する際には確実に使っておきたい機能の一つです。このように拡張メソッドが提供されるだけなので、アプリケーションの構造を大きく変えることなく組み込めるのもメリットです。

クラスベースで利用する

もう一つ Source Generator に用意されている機能が、オーケストレーターとアクティビティをクラスベースで定義する機能です。個人的にはこの機能が一番気に入っています。

正直あまりイメージ出来ないと思いますが、要するにオーケストレーターとアクティビティを以下のように TaskActivity<TInput, TOutput>TaskOrchestrator<TInput, TOutput> を実装したクラスとして定義できると言うものです。独立したクラスとして実装できるの DI 周りもシンプルに出来るのと、実装をクラスに閉じ込めることが出来るので扱いやすいです。

[DurableTask(nameof(Orchestration))]
public class Orchestration : TaskOrchestrator<string, List<string>>
{
    public override async Task<List<string>> RunAsync(TaskOrchestrationContext context, string input)
    {
        var logger = context.CreateReplaySafeLogger<Orchestration>();
        logger.LogInformation("Saying hello.");
        var outputs = new List<string>();

        // Replace name and input with values relevant for your Durable Functions Activity
        outputs.Add(await context.CallSayHelloAsync("Tokyo"));
        outputs.Add(await context.CallSayHelloAsync("Seattle"));
        outputs.Add(await context.CallSayHelloAsync("London"));

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

[DurableTask(nameof(SayHello))]
public class SayHello(ILogger<SayHello> logger) : TaskActivity<string, string>
{
    public override async Task<string> RunAsync(TaskActivityContext context, string input)
    {
        logger.LogInformation("Saying hello to {name}.", input);
        return $"Hello {input}!";
    }
}

オーケストレーターとアクティビティはそれぞれ独立して動作するので、実装クラス自体も分かれていた方が管理しやすくなります。依存関係もシンプルに出来るのでかなりお気に入りの機能です。

このクラスベースの Durable Functions ですが残念ながら Azure Functions SDK の最適化によって、現在のバージョンではデフォルトの設定では正しく動作しなくなってしまいました。現時点での最新バージョンをインストールして実行してみると、以下のようにオーケストレーターとアクティビティが Azure Functions Runtime によって認識されていないことが分かります。

これは少し前の Azure Functions SDK のアップデートで組み込まれた Source Generator の影響です。以下の Issue で色々と話がされていますが、現時点では SDK が複数の Source Generator を受け付けられるような拡張ポイントを用意するという流れになっています。

SDK に組み込まれた Source Generator の影響で利用できなくなっているので、以下のような設定を csproj ファイルに追加して Source Generator を無効化してしまえば動作するようになります。

  <PropertyGroup>
    <FunctionsEnableWorkerIndexing>false</FunctionsEnableWorkerIndexing>
    <FunctionsEnableExecutorSourceGen>false</FunctionsEnableExecutorSourceGen>
  </PropertyGroup>

上の設定を csproj に追加して再度実行してみると、今度は正しくオーケストレーターとアクティビティが認識されています。この状態で HttpTrigger を実行するとクラスベースで定義したオーケストレーターとアクティビティが実行されます。

実際に使っていますがクラスベースで書けるのはかなり快適なので、Azure Functions SDK の Source Generator を無効化しなくても使えるようになって欲しいですが、拡張ポイントの実装にそれなりに時間が掛かりそうです。回避策はあるので気長に待とうと思います。

既存のプロジェクトを移行する

最後に In-Process の Durable Functions から .NET Isolated の Durable Functions への移行について簡単に書いておくことにします。基本的には以下の公式ドキュメントに基づいて修正して貰えれば問題ないはずです。前述したように .NET Upgrade Assistant は Durable Functions に対応していないので、クラス名の修正などは自分で行う必要がありますが、そこまで難しいものではありません。

Durable Entities についても同様で、クラス名が変わっているぐらいなので対応は簡単です。

.NET Isolated 版に移行しても内部で使われている実装は In-Process と同じですので、Azure Storage や Task Hub はそのまま使いまわすことが出来ます。心配であれば変更しても問題ないですが、実装からして互換性には問題ないと考えています。

移行する最大のモチベーションだった Durable Task Scheduler ですが、ドキュメントには .NET Isolated などの分離ワーカーへの導入方法しか書かれていませんでしたが、先日 DTS を作っている Chris に直接聞いたところ実は In-Process 向けのパッケージも用意されているらしく、それを使うと DTS を既存の In-Process な Durable Functions でも利用できることまで確認しました。

従って Durable Task Scheduler を使うためのモチベーションとしては消滅してしまいましたが、インターフェースが再設計されて分かりやすくなっているので移行する価値はあると考えています。

Azure AD B2C を本番で使う場合にはカスタムドメインを設定してほしいという話

最近は Azure AD B2C を利用したサービスを目にすることが増えてきましたが、デフォルトのドメインである b2clogin.com をそのまま利用しているケースが多いようです。もっともドメインがデフォルトなので Azure AD B2C を使っていると分かるのですが、本番でも b2clogin.com を使っているとパスワードマネージャーとの相性が悪いという問題があります。

具体的にはパスワードマネージャーはドメイン名*1でグルーピングされていますが、Azure AD B2C のデフォルトドメインで運用されている場合には b2clogin.com でグルーピングされてしまっています。

従って全く関係のないサービスが Azure AD B2C で構築されていて、更に b2clogin.com で運用されている場合には以下のように別のサービスのメールアドレスとパスワードが選択肢に上がってくることになります。

パスワードマネージャーが Public Suffix List に従っているならば、根本的には b2clogin.com が Public Suffix List に含まれていないことが問題なのですが、ブランディングの観点からも Azure AD B2C にカスタムドメインを設定した方がベターでしょう。

意外に Azure AD B2C をカスタムドメインで運用されている例が少なそうなので、実際に Azure AD B2C にカスタムドメインを設定して今回の問題が回避できることまで確認してみます。

まずはデフォルトのドメインで運用している Azure AD B2C のログインフローを実行すると、以下のようにブラウザーのパスワードマネージャーが関係のない b2clogin.com で運用されているユーザー情報を入力しようとしてきます。ここまでは今回意図した通りの挙動です。

Azure AD B2C へカスタムドメインを設定するには、Front Door を作成する必要があるので既に Front Door を使っている場合にはエンドポイントを新しく作成すれば、追加コスト無しで実現出来るのでお得です。

カスタムドメインを設定する方法については今回は触れないので、以下のエントリや公式ドキュメントを参照してください。Azure AD B2C のテナント側でカスタムドメインを追加してしまえば、後は Front Door の一般的な設定方法と同じなので難しくはありません。

設定した後に上手くアクセスできない場合は Azure AD B2C 側でカスタムドメインの検証が成功しているか確認します。若干確認プロセスに癖があった記憶がありますので、ドキュメントをしっかり読んで手順をスキップしないのがコツです。以下のように Verified になっていれば OK です。

Front Door のカスタムドメイン設定は特に難しいことはありませんが、TLS 証明書が必要になるので Managed Certificate などを使って発行しておきます。Azure AD B2C 向けに login サブドメインを割り当てることが多いと思うので、ワイルドカード証明書を持っている場合は Key Vault を使って割り当てます。

ここまでの設定を行うことでカスタムドメイン経由で Azure AD B2C が利用できるようになるので、実際にカスタムドメインでのログインを試してみるとパスワードマネージャーが反応しなくなります。これで他の Azure AD B2C を使っているサービスのパスワードが自動入力されることを避けることが出来ました。

今回の目的はこれで果たせたのですが、折角 Front Door を導入したのでついでにカスタマイズした HTML テンプレートについても Front Door 経由で配信することで、CORS を回避しつつアセットのキャッシュが行えるようにしておきます。

以下の公式ドキュメントにあるように、通常 HTML テンプレートは Azure Storage にホストされるため Azure Storage 側で CORS の設定を行う必要がありますが、Front Door を使って同一オリジンからルーティングで Azure Storage に振り分けてしまえば不要になります。

今回は例として、以下のように Azure Storage の layout というコンテナーに保存された HTML テンプレートを Front Door から配信するように変更します。

手順としてはシンプルに Front Door に対して Azure Storage の Origin Group を追加して、ルーティングを以下のように /layout/* に対して設定します。これで /layout/* 以下のアクセスは Azure Storage に、それ以外のアクセスは Azure AD B2C にルーティングされるようになります。

最後に Azure AD B2C のユーザーフローやカスタムポリシーを変更して、利用する HTML テンプレートのパスをカスタムドメイン経由のものに置き換えます。

修正したユーザーフローを使ってログインを試してみると、以下のように配信元が全てカスタムドメイン経由になり同一のオリジンになっていることが確認出来ます。CORS を回避できますし、同一オリジンから配信されているのはユーザー的にも安心感がありますね。

Azure AD B2C が思ったよりも広く使われているのは個人的にかなり嬉しいのですが、カスタムドメインが割り当てられていないケースも意外に多そうだったので、是非分かりやすいドメインを割り当ててもらえればと思います。個人的なお勧めはやはり login サブドメインです。

*1:恐らく Public Suffix List に従っている

Ignite 2024 で発表された Azure Cosmos DB for NoSQL のアップデート

シカゴで開催された Ignite 2024 が終わってから少し時間が空いてしまいましたが、今年も Cosmos DB for NoSQL 周りで大きなアップデートがありましたのでまとめておきます。

公式まとめは以下のブログにまとまっているので、これを読んでおけば大体は把握できるはずです。

今回の Ignite 2024 は公式的には Cosmos DB for MongoDB (vCore) の新機能がかなり推されている印象ですが、個人的には使う気がしないので Cosmos DB for NoSQL の新機能に絞って扱っていきます。

Vector Search と DiskANN が GA

今年の Build で Public Preview として発表された Vector Search と DiskANN が Ignite では無事に GA となりました。よくある単純に SLA が付いただけの GA とは異なり、新機能もしっかりと含まれていました。

今回 GA のタイミングでこれまで利用できなかった継続的バックアップが有効な環境でも、問題なく Vector Search が使えるようになっています。これは結構重要なポイントです。

Cosmos DB for NoSQL の Vector Search と DiskANN は Public Preview 中に検証しているので、詳細は以下のエントリを参照してください。

若干扱いが地味ですが、更に GA のタイミングで Vector Index 作成時のオプションとして quantizationByteSizeindexingSearchListSize が追加されました。

両方とも Vector Search の精度に関わる設定なので、ユースケースに合わせて最適な値を設定するのも検討しても良いでしょう。上手くコストとのバランスを取る必要がありますが、RU に依存しているのでコントロールしやすい部分ではあります。

Full-Text Index と Hybrid Search が Public Preview

これまで AI Search の独壇場だった Full Text Search ですが、なんと Cosmos DB for NoSQL にも専用の Full-Text Index が追加され、これで Cosmos DB だけで Vector Search と Full Text Search の両方を使った Hybrid Search が実現出来るようになりました。

Full Text Search は Vector Search の時と同様に専用のインデックスと関数が追加されているだけで、Cosmos DB 自体の機能としては変わっていないのが大きな特徴と言えます。そのような仕組みなので使い方は Vector Search の時とほぼ変わりません。

インデックスさえ作成してしまえば、後はそのプロパティに対して検索を行う関数を呼び出す SQL を書くだけなので分かりやすいです。SQL ベースなので Azure Portal からも試せます。

現時点での制約として Full-Text Index の Analyzer は English のみ指定可能という点です。将来的には他の言語向けの Analyzer も追加されると思いますが、Preview 開始時点では英語のみサポートです。

そして Hybrid Search は RRF 関数と ORDER BY RANK が追加されているので、Vector Search と Full Text Search を行う関数が返すスコアを使えば実現出来ます。API としては低レベル寄りですが、その分コントロール可能な部分が多くなっています。

まだ Preview ではありますが、確実に AI Search よりもコストを下げつつ信頼性を高めることが出来るので、今後のエンタープライズ向け RAG のバックエンドとしては Cosmos DB for NoSQL が重要な選択肢です。

VS Code 拡張が Public Preview

Azure Portal から利用できる Data Explorer のような操作感の VS Code 拡張が Public Preview です。Azure Database 拡張の更に拡張機能という形になっていて、クエリの実行からデータの編集まで VS Code 上で行えるようになります。

Cosmos DB へのアクセスを Private Endpoint で絞っている場合には Azure Portal の Data Explorer が利用できませんが、VS Code 拡張であればネットワーク内から接続できるはずなので、本番環境のメンテナンス向けにも良さそうですね。

Linux-based Emulator が Public Preview

地味に大きなアップデートとなるのが Cosmos DB の新しい Linux 向け Emulator が Public Preview として公開されたことです。

これまでも Cosmos DB Emulator は Windows と Linux の両方で実行出来ていましたが、Linux 向けは M1/M2 などの Arm ベースの macOS に対応できておらず、パフォーマンス面でも Windows 版に比べて不利な部分がありましたが、今回の新しい Linux 向け Emulator では解消されています。

Docker Image を確認したところ、amd64 と arm64 の両方に対応した Multi-arch image となっています。Arm64 に対応したので Windows on Arm デバイスでも Prism エミュレーション無しに実行出来そうです。

ローカル開発環境だけのメリットではなく、GitHub Actions 上で Cosmos DB Emulator を使ったテストコードを実行しているケースでも、今回新しくリリースされた Linux 向け Emulator は CI 時間の短縮やリソースの削減で役に立ちそうです。Arm64 対応によってコスパの良い Arm ベース Runner を使えるため、CI コストをさらに下げることも出来そうです。

M2 MacBook Air で Dev Container を使った Azure Functions 開発が行えない問題を直す

これまでも何度か取り上げた Azure Functions の開発を Dev Container / GitHub Codespaces で行う方法ですが、公開している Dev Container Template が結構使われているみたいで作って良かったという気持ちです。

GitHub Codespaces を使う場合は例外ですが C# や Node.js 向けの場合は Dev Container を使わずに開発しているので、Dev Container を使うシナリオは専ら Python 向けという感じです。

これまで問題なく Dev Container Template を使って Python の Azure Functions 開発を行ってきたのですが、最近になって手持ちの M2 MacBook Air では func コマンド実行時に謎のエラーが出て失敗するようになりました。同じ定義を WSL 2 や GitHub Codespaces で動かしてもエラーにはなりません。

これまでの経験的に実行毎にメモリアクセス周りのエラーが発生するのは、エミュレーションが原因だと当たりが付いていたので Docker Desktop の設定で Rosetta を無効化して試すことにしました。

デフォルトでは以下のように Apple Virtualization framework と Rosetta が有効になっていますが、Rosetta を x86/x64 のエミュレーションに使わないようにチェックを外して試しました。

この設定で Rosetta を使わないように変更すると QEMU が使われるようになるようです。ちなみに Azure Functions 向けの Dev Container Template では Azure Functions Core Tools が linux-arm64 向けにリリースされていないため、明示的に linux/amd64 で動くように指定しています。

Rosetta を無効にすることで func コマンドは通るようになりましたが、肝心の func start コマンドを実行するとこれまでと同様に実行時エラーとなってしまい完全には解消できませんでした。

ということを Twitter で呟いてみると、Microsoft の中の人からヒントが飛んできました。この辺りには全く詳しくないので GPT-4o に解説してもらいましたがよくわかりませんでした。とにかく Rosetta はダブルマッピングを正しく扱えないため Rosetta 上で動いている場合には無効化するように変更されたようです。

Rosetta を無効化して QEMU を使っても失敗したのは気になりますが、とにかく .NET 9 の GA タイミングでは今回の問題は既に修正されているようです。

但し Azure Functions Core Tools は現在 .NET 8.0.8 を使ってビルドされているため、今回 macOS 上の Docker で実行しようとした際にエラーとなったという流れのようです。

今回の修正と同じ挙動になる設定は既に組み込まれているため、修正が組み込まれた Azure Functions Core Tools がリリースされるまでは、以下の Dockerfile のように環境変数に DOTNET_EnableWriteXorExecute=0 を追加することで今回の問題を回避可能でした。

FROM mcr.microsoft.com/devcontainers/python:1-3.11-bookworm

ENV DOTNET_EnableWriteXorExecute=0

この定義を使って Dev Container を再作成すると func start コマンドも問題なく動作しました。

ちなみに .NET 8.0.10 以降のバージョンに今回の修正がバックポートされているので、Azure Functions Core Tools のビルド環境が更新されていれば次のバージョンから修正される可能性が高いです。

根本的な解決方法としては Azure Functions Core Tools の linux-arm64 向けバージョンがリリースされることなのですが、何故か Windows と macOS 向けは Arm64 が用意されているのに Linux だけは滞っています。

Issue や Pull Request は上がっているので何時か対応されるのを期待するというのが現状です。

Ignite 2024 で発表された App Service / Azure Functions / Container Apps のアップデート

今年はシアトルから変わってシカゴで開催されている Ignite ですが、例によってキーノート開始してすぐに各種サービスのアップデートが発表されましたね。今年も例によって AI 周りの発表が多いのですが、しっかり App Service / Azure Functions / Container Apps 周りも重要なアップデートがあったので、自分の興味がある部分に特化して確認しました。

個人的には Flex Consumption の GA と Durable Task Scheduler の Limited Early Access が一番熱い発表で、この 2 つに関しては別途エントリを個別に書こうかと思っています。

Ignite 2024 で発表されたものだけではなく、その直前に公開されたものも含めていますが .NET 9 や Node.js v22 など言語サポートのアップデートは省いています。

App Service

前回同様に Ignite 2024 で発表された App Service のアップデートまとめブログが公開されているので、これを読んでおけば Ignite 前に発表された内容も一気に把握できます。

.NET 9 や Node.js v22 サポートの追加は Ignite 前に行われていたものです。Java 周りもアップデートがまとめられていますが、これも Ignite 前に発表があったものですね。

App Service on Linux の Sidecar が GA

Windows の App Service で言うところの Site Extensions に相当する Sidecar container 機能が App Service on Linux で GA になりました。APM やキャッシュに使うシナリオが多く紹介されています。

最近だと Phi-3 をホストした Sidecar をデプロイして、App Service から使うチュートリアルが結構話題になっていた気がします。今回 Sidecar が GA したため、Docker Compose ベースのマルチコンテナーは恐らく廃止になると思います。

E2E 暗号化と最小 TLS Cipher Suite が GA

デフォルトで App Service の Frontend から Web Worker までの通信は HTTPS で暗号化されていないのですが、その部分を含めて HTTPS で暗号化する E2E 暗号化オプションが Windows と Linux で GA しました。設定には Standard 以上の SKU が必要になります。

同時に最小 TLS Cipher Suite の構成についても Windows と Linux で GA しています。Frontend での対応なので Web Worker 側の OS には依存しない仕組みで、Kestrel ベースに書き直されたことで実現されています。こちらについては Basic 以上の SKU で利用可能になっています。

マルチテナントの App Service でも安全性の高い GCM を使う Cipher Suite を強制出来ます。

Unique Default Hostname が GA

Subdomain Takeover 対策となる Unique Default Hostname が App Service に関しては GA しました。このタイミングで Azure Functions でも Public Preview になりましたが、Azure Portal ではまだ指定できません。

Bicep や Terraform から使う方法については以下のエントリで書いていますので参考にしてください。

Unique Default Hostname を利用すると App Service 名が Global スコープから Region スコープに変わるため、別リージョンであれば同名の App Service が作れることに注意してください。

Availability Zones サポートの改善が 2025 年に

今回は予告だけになりますが 2025 年の初頭に App Service の Availability Zones サポートが大幅に改善され、App Service を作成した後からも AZ を有効に出来るようになります。そして SLA 99.99% の要件が 3 インスタンスから 2 インスタンスになるため、これまでよりも低いコストで可用性の要件を満たしやすくなります。

そして現在 2 インスタンス以上で運用している環境は、プラットフォーム側のアップデートによって Availability Zones が有効化出来るように変更されるようです。

これまで作成した後から Availability Zones の有効化が行えなかったので、最初から 3 インスタンス以上で構成する必要がありコスト面でのインパクトが大きかったですが、今後はサービスの成長に伴って AZ を有効化出来るようになります。

Azure Functions

毎回 Azure Functions は対応言語のアップデートが中心で、あまり大きなアップデートは発表されないのですが、今年は Flex Consumption と Durable Task Scheduler というかなり大きなアップデートがありました。

対応言語などについてはまとめブログを読んでおけば問題ありません。.NET 9 や .NET 8 の In-Process サポートが紹介されていますが、特に後者は Ignite 感はありません。

Flex Consumption が GA

今回のアップデートの目玉が Build 2024 で Public Preview になっていた Flex Consumption の GA です。新規に開発された Legion バックエンドにより非常に高速なスケーリングを実現しています。これまでの Consumption Plan の弱点であった VNET Integration にも対応しているため、今後は Linux で Azure Functions を動かす場合には Flex Consumption 一択になる予感です。

GA の時点では Availability Zones をサポートしていませんが、Flex Consumption の仕組みから考えると後から有効化出来るようなものになる気配です。2025 年初頭にサポートが予定されています。

既に Azure Portal では Flex Consumption が一番選びやすい位置に表示されているので、Azure 的にも Flex Consumption を強く推しているのが分かります。

GA で Flex Consumption はスケーリング上限がこれまで Stamp レベルに制限されていたのが、Region レベルに拡大されたようです。それによりバースト時のスケーリングでリソースが更に確保しやすくなっているようです。以下のブログでは Flex Consumption の技術的側面が一部解説されているため非常に面白いです。

Flex Consumption の詳細は公式ドキュメントもアップデートされているので、こちらも利用前には参照しておくと良いです。まだ制限がいくつか残っているため App Service Plan 感覚で使うとはまりそうです。

ただし現時点では Japan East / Japan West の両方で Flex Consumption が利用できず、直近の拡大予定リージョンにも含まれていないため時間がかかりそうです。非常に残念ですが Japan East / Japan West のキャパシティが足りていないのが原因と聞いています。

Durable Task Scheduler が Limited Early Access

もう一つの大きな Azure Functions アップデートは Durable Task Scheduler です。これまで Durable Functions を利用する際には Azure Storage / MSSQL / Netherite という 3 つのバックエンドから選択する必要があり、そのバックエンドにはパフォーマンス上の制限や個別にリソースを準備して設定する必要があり運用面で手間がかかる部分がありました。

そして Observability の観点では現状の Azure Portal で確認出来る Orchestrator ログは貧弱で、詳細を確認するには Durable Functions Monitor などを追加でデプロイする必要がありましたが、今回 Limited Early Access になった Durable Task Scheduler を使うと全て解決します。

Durable Task Scheduler は完全に Azure マネージドとなっているため、運用に関しては任せっぱなしでほぼ必要ありません。パフォーマンスに関してはこれまで最速の Netherite よりも良いため機能面ではメリットしかありません。ちなみに .NET 8 In-Process はサポート外のようです。

気になるのは価格がどのように設定されるかですが、恐らく Event Hubs のように Throughput Unit に近い概念が導入されて、その数によって課金されるのではないかと見ています。個人的には Azure Storage か Durable Task Scheduler の 2 択になると考えています。

Container Apps

最近あまり目立っていない Container Apps ですが、Serverless GPUs 始め大きなアップデートがありました。基本的には AI 文脈での新機能が多いですが、アプリケーション開発でも役立つ機能が追加されています。

これまでは AKS でしかサポートされていない機能が Container Apps にも実装されている感じなので、個人的には AKS を無理して使う機会はもう無いなという気持ちです。

Serverless GPUs が Public Preview

待望と言える Serverless GPUs が Public Preview となりました。これまでも Workload Profile を使うと GPU インスタンスをプロビジョニング出来ていましたが、今回の機能は Serverless と付いているだけあって、完全な従量課金で GPU インスタンスを利用できるようです。

利用するには最初に GPU クオーターの申請が必要になるため、正直なところそこが最初で最大のハードルになっています。Container Instances の GPU サポートよりも圧倒的に使い勝手が良いはずなので、Azure 上で GPU が必要な場合はこの方法が主流になっていくと思います。

Dynamic Sessions (Python / Custom container) が GA

信頼できないコードを実行するサンドボックス環境として利用可能な Container Apps の Dynamic Sessions が Python と Custom container を利用するケースで GA となりました。Code Interpreter と組み合わせて利用するのがメインのシナリオですが、ユーザーが入力したコードを安全に実行する場としても使えます。

Python と Custom container は GA しましたが、Node.js の Code Interpreter サポートが Public Preview で新たに追加されています。JavaScript の安全なサンドボックス実行環境も利用シナリオが多そうです。

Private Endpoint が Public Preview

正直なところこれまで使えなかったんだという感じですが、Container Apps の Private Endpoint 対応が Public Preview となりました。Workload Profile 環境が必須になるので注意が必要ですね。

元々 Internal な Container Apps は作成できてたじゃないかという話ですが、Private Endpoint が使えるようになると Front Door Premium から Container Apps へのセキュアな接続が実現できるメリットがあります。

主なユースケースとしては Front Door Premium からのセキュアな接続のようです。Internal な Container Apps との使い分けを上手くやっていく必要があります。

Planned Maintenance が Public Preview

コンピューティング系の PaaS では珍しめですが、Azure 側でのメンテナンスが行われる期間をある程度指定できる Planned Maintenance が Public Preview になっています。App Service などでは事前通知だけですが Container Apps は都合の良いメンテナンス期間を指定できます。

メンテナンス期間を指定してもクリティカルなアップデートについては随時行われるため、この機能に頼りっぱなしになるのではなくアプリケーションの実装やアーキテクチャでアップデート時の影響を最小限に出来るように作る必要があります。

HTTP Path-based Routing が Early Access

Container Apps に付いてくる HTTP Ingress で Path-based Routing を行う機能が Early Access になりました。Container App Environment 単位でルーティングを定義して、特定の Container App にトラフィックを流すことが簡単に出来るようになります。

まだ Azure Portal や Azure CLI でのサポートはありませんが ARM Template や Bicep では managedEnvironments/httpRouteConfigs というリソース作成すれば利用可能です。

これまで Container Apps で HTTP の Path-based Routing を実現するには Front Door や Application Gateway を前方において、バックエンドの Container App へリクエストを振り分けるといった実装が必要でしたが、今回のアップデートで将来的には Container Apps だけで実現出来るようになります。

Azure Functions が .NET 9 に対応したのでアップグレードする

先週開催された .NET Conf 2024 で .NET 9 が正式リリースされましたが、同じタイミングで App Service と Azure Functions についても .NET 9 GA 版への対応が行われたため、現時点では Flex Consumption 以外で利用可能になっています。

Azure Functions チームの中の人は Linux Consumption はロールアウト中とツイートしていましたが、今日確認すると Linux Consumption でも問題なく .NET 9 な Azure Functions をデプロイ出来ました。

Flex Consumption が .NET 9 に対応していないのは非常に残念ですが、恐らく今週開催される Ignite 2024 で Flex Consumption 自体の GA が発表される可能性が高いため、そのタイミングで何らかの動きがあるのではないかと見ています。今晩以降は要チェックですね。

.NET 9 サポートについては以下の Issue で随時進捗が共有されているので、アップデート方法含めてこちらを見ておくと間違いありません。Visual Studio と VS Code でのサポートについても書かれています。

VS Code の場合は Azure Functions 拡張によってテンプレートが自動的に更新されるようですが、Visual Studio の場合はオプションにある以下の画面からアップデートを行う必要があります。Azure Functions プロジェクトを作成するタイミングなどで自動更新される可能性がありますが、意外に古い状態のままになっているケースが多いので手動で行うと安心です。

Azure Functions の .NET 9 サポートは最新のテンプレートに更新されていれば、作成時に .NET 9 Isolated を選ぶと最新のパッケージを含む形で自動生成されるので楽ですが、既存の .NET 8 Isolated からのアップデートを行う際には SDK 含むコアパッケージを 2.0.0 に上げる必要があります。

コアパッケージ v2 については公式ドキュメントで紹介されているので、この辺りを参照すればアップデート方法は分かるはずです。大きな改善として初期化で ASP.NET Core で使われている Builder Pattern が導入されています。実質 .NET Aspire 対応という感じですが、扱いやすくなっています。

これまでの HostBuilder を使った方法と、コアパッケージ v2 で導入された FunctionsApplication を使った方法を載せておきますので、違いを見比べてみてください。最近の ASP.NET Core を使ったことのある人なら馴染み深いインターフェースになっているはずです。

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services =>
    {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

host.Run();
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;

var builder = FunctionsApplication.CreateBuilder(args);

builder.ConfigureFunctionsWebApplication();

// Application Insights isn't enabled by default. See https://aka.ms/AAt8mw4.
// builder.Services
//     .AddApplicationInsightsTelemetryWorkerService()
//     .ConfigureFunctionsApplicationInsights();

builder.Build().Run();

今回の変更には ASP.NET Core 周りを開発しているチームが協力しているようなので、今後も .NET Isolated については .NET チームがガンガン関与して良いものにして欲しい気持ちでいっぱいです。

ASP.NET Core と同じ Builder Pattern が導入されているので、Configuration や Logging 周りの設定は ASP.NET Core のドキュメントで紹介されている方法と同じものが利用できます。これまでのようにメソッドにラムダ式を渡して、そこで初期化する必要がなくなったので劇的に使い勝手が良くなっています。

アプリケーション側の .NET 9 対応が終われば、後は Azure 側で .NET 9 対応を行うだけです。前述したように既に Flex Consumption 以外は .NET 9 Isolated に対応しているため、Azure Portal で .NET バージョンを変更するだけで完了します。

.NET 8 Isolated から .NET 9 Isolated にアップデートする際には、まず Azure 側を .NET 9 Isolated に変更して、その後に .NET 9 にアップグレードしたアプリケーションをデプロイするとスムーズにいくはずです。

現在 .NET 6/8 の In-Process を利用している場合には .NET 9 にアップグレードすることは出来ないため、先に .NET Isolated への移行を行ってから .NET 9 にアップグレードするといった手順を踏みます。.NET Isolated への移行はドキュメントが用意されているので、こちらを読みながら進める形になります。

完全に対応しているわけではありませんが、Visual Studio に .NET Upgrade Assistant 拡張をインストールすると、右クリックメニューから .NET 9 Isolated への移行をある程度まで自動で行うことが出来るので、組み合わせて移行するのが最適解だと考えています。

何回か .NET Upgrade Assistant を使って .NET Isolated への移行を試してみましたが、Function の実装については 8 割ぐらいは自動的に移行していくれるという印象です。但し新しく追加された FunctionsApplication を使うようには書き換えてくれなかったので、その部分は手動での対応が必要でした。

Prompty を C# で扱うライブラリがリリースされたので試した

今年の Microsoft Build で突然 Seth が発表した Prompty ですが、これまで扱いが悩ましかったシステムプロンプト周りを上手く外部ファイルとして扱えるのと、VS Code 拡張を使うとファイルを直接 OpenAI で実行できるのでプロンプトの開発段階から、アプリケーションへの組み込みまで使えて便利です。

特に AI アプリケーションの開発ではプロンプトだけ修正することが多いので、Prompty を使って外部ファイル化しておけば VS Code で開発したプロンプトをそのままデプロイ出来るので、アプリケーションの改善を行いやすいメリットもあります。

しかし、これまで Prompty をアプリケーションに組み込むには Python 向けに用意されたライブラリか Semantic Kernel の Prompty 拡張しか選択肢が無く、C# のアプリケーションから扱いにくかったのですが最近 C# 向けライブラリがリリースされて組み込みやすくなりました。

名前から想像つくように Prompty.Core は Prompty ファイルの解析やレンダリングまでを行い、実際に OpenAI を叩くという部分は別ライブラリで提供する予定のようです。

ソースコードは Prompty のリポジトリで公開されているので、実装を確認することや Pull Request を送ることも可能になっています。実装としては Python 向けを踏襲しているようです。

ここからは実際に Prompty.Core を使って OpenAI で実行するところまでやっていきます。前述したように現在のライブラリは Prompty の解析とレンダリングまでしか行わないため、OpenAI にメッセージを投げる部分は別途用意する必要があります。

Prompty ファイルを読み込んで OpenAI に投げる直前までを行うコードは以下のようにシンプルです。最初に InvokerFactory.AutoDiscovery を呼ぶ必要がある点は注意が必要です。

using Prompty.Core;

InvokerFactory.AutoDiscovery();

var prompty = await Prompty.Core.Prompty.LoadAsync("Prompts/Basic.prompty");

var messages = await prompty.PrepareAsync(new
{
    firstName = "Tatsuro",
    lastName = "Shibamura",
    question = "Microsoft について教えてください"
});

やっている内容はシンプルで Prompty.LoadAsync を呼び出して Prompty ファイルを読み込み、更に PrepareAsync を呼び出して与えたパラメータを基にプロンプトのレンダリング結果を取得しています。

今回使用している Prompty ファイルは以下のような単純なものです。PrepareAsync に渡すオブジェクトは sampleinput で定義したものと同じ名前で渡す形になります。

---
name: Basic Prompt
authors:
  - shibayan
model:
  api: chat
sample:
  firstName: Tauchi
  lastName: Kazuaki
  question: 労働の意味は?
---
system:
あなたは AI アシスタントとして質問に回答してください。回答は完結に分かりやすくしてください。

# ユーザー情報
質問するユーザーの名前は {{firstName}} {{lastName}} です。回答には名前を入れるようにしてください。

user:
{{question}}

アプリケーション内で利用する Prompty ファイルは CopyToOutputDirectoryPreserveNewest などを指定して出力ディレクトリにコピーされるように設定する必要があります。

以下のようにワイルドカードを利用して定義しておくと、この例では Prompts ディレクトリ以下にある拡張子 .prompty は全て出力ディレクトリにコピーされるようになります。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Prompty.Core" Version="0.0.11-alpha" />
  </ItemGroup>

  <ItemGroup>
    <None Update="Prompts\*.prompty">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>

ここまでのコードを実行してみると、以下のように PrepareAsync の実行結果として Prompty をレンダリングしたものが返ってきていることが確認出来ます。Prompty の中で system と user で 2 つのロールを定義していますが、ちゃんと別のメッセージとしてレンダリングされています。

PrepareAsync の戻り値の型が object なので分かりにくいのですが、現状のライブラリは Microsoft.Extensions.AI ベースになっているので、そこで定義されている ChatMessage が返ってくるようになっています。つまり MEAI を使えば Prompty のレンダリング結果を簡単に実行できると言う訳です。

Microsoft.Extensions.AI についてはリリース時にエントリを書いたので参照してください。

上記のエントリで紹介した Azure OpenAI を利用するサンプルコードのプロンプト部分を Prompty に置き換えたコードが以下になります。Microsoft.Extensions.AI が AI 周りの基盤で使われているので Prompty から、Azure OpenAI の呼び出しまでシームレスに繋がっています。

using System.ClientModel;

using Azure.AI.OpenAI;

using Microsoft.Extensions.AI;

using Prompty.Core;

InvokerFactory.AutoDiscovery();

var prompty = await Prompty.Core.Prompty.LoadAsync("Prompts/Basic.prompty");

var messages = (ChatMessage[])await prompty.PrepareAsync(new
{
    firstName = "Tatsuro",
    lastName = "Shibamura",
    question = "Microsoft について教えてください"
});

var client =
    new AzureOpenAIClient(new Uri("https://***.openai.azure.com/"), new ApiKeyCredential("***"))
        .AsChatClient("gpt-4o");

var result = await client.CompleteAsync(messages);

Console.WriteLine(result.Message);

実行すると Prompty を VS Code で実行した時と同じような結果が返ってきます。簡単な Prompty なのでプログラムに埋め込むのとあまり差を感じないかもしれませんが、これが RAG シナリオのように複雑なコンテキストを含む場合には、Prompty の表現力の高さが力を発揮します。

Prompty クラスには ExecuteAsync メソッドが用意されているので、将来的に OpenAI 向けの Executor ライブラリが提供されれば以下のように ExecuteAsync を呼び出すだけで終わるようになります。

using Prompty.Core;

InvokerFactory.AutoDiscovery();

var prompty = await Prompty.Core.Prompty.LoadAsync("Prompts/Basic.prompty");

await prompty.ExecuteAsync(inputs: new
{
    firstName = "Tatsuro",
    lastName = "Shibamura",
    question = "Microsoft について教えてください"
});

現状では OpenAI 向けの Executor が提供されていないので、以下のようなエラーになります。独自の Executor を作成することも出来るので、SLM の利用シナリオでも活用できるかもしません。

これまでも Semantic Kernel 経由では Prompty を使うことは出来ましたが、今回リリースされた Prompty.Core を使うことで圧倒的に気軽に Prompty を組み込めるようになったのは、今後の AI アプリケーション開発での大きなメリットだと思います。

Azure Functions の Consumption Plan を利用していると稀に 404 が返ってくる問題を直す

タイトルの通りですが、最近 Azure Functions の Consumption Plan を利用しているアプリケーションが、リロードを繰り返すと稀に 404 を返す現象に遭遇しました。この時の 404 はアプリケーションが返しているものではなく、App Service が存在しない場合に返されるページです。

ランダムで発生するので調べたところ、この現象が発生する Azure Functions は IP アドレスが 2 つ返されることが確認出来、更に片方の IP アドレスが選ばれた場合に 404 となることが分かりました。

Azure Portal や Azure Resource Manager から確認出来る Inbound IP Address は 1 つだけですが、実は Azure Functions の Consumption Plan や Elastic Premium Plan (Functions Premium) は今回のように複数の Inbound IP Address を持つことがあります。

本来 App Service は 1 つの App Service Plan に対して 1 つの Stamp (Scale unit) が割り当てられるため、1 つの Inbound IP Address しか持たない仕組みになっていますが、Consumption Plan と Elasitc Premium Plan (Functions Premium) では複数の Stamp に割り当てられることがあるため、その場合には Inbound IP Address が複数返ってくるという仕組みです。

この挙動によって所属する Stamp のキャパシティが不足している場合でも、他の Stamp を利用することで Consumption Plan や Elastic Premium Plan のスケーリングが行えるようになりますが、特定の Stamp で正しく Function App がプロビジョニング出来ていない場合に 404 となります。

複数の Stamp を利用しなければ良いので、App Settings に WEBSITE_DISABLE_CROSS_STAMP_SCALE 設定を追加すると Inbound IP Address が 1 つになるため問題は解消します。

この設定は名前の通り複数の Stamp を使ったスケーリングを無効化するものなので、追加すると 1 つの Stamp だけを使ってスケールを行おうとします。無効化するとキャパシティが不足した場合にスケーリングに制限が出る可能性があります。

該当の Function App にこの設定を追加すると、以下のように Inbound IP Address が 1 つだけ返ってきます。

返ってきた Inbound IP Address は正しく Functions App がプロビジョニングされた Stamp となるため、リロードを繰り返しても 404 が返ってくることは無くなりました。複数の Stamp を利用してスケーリングが行われることを知っていると問題の特定が楽でした。

この複数 Stamp が利用される挙動については Azure Files の利用が必須になっているため、Azure Portal から Function App を作成する際に Azure Files を追加しなかった場合には無効化されるはずです。

この辺りについては以下のドキュメントで記載されていますが、複数 Stamp を利用する場合はコンテンツの共有に Azure Files が必須なのが理由です。Consumption Plan で Azure Files を使わない場合はスケーリングが制限されると明記されています。

Since Azure Files is used to enable dynamic scale-out for Functions, scaling could be limited when running your app without Azure Files in the Elastic Premium plan and Consumption plans running on Windows.

Storage considerations for Azure Functions | Microsoft Learn

但し Windows が対象になると書いてある通り、Linux には影響しないようです。Linux の Consumption Plan はアーキテクチャが Windows の Consumption Plan とは別物になっているのが理由だと思われます。

今回の問題とは関係ないのですが、複数 Stamp を利用する設定の場合は以前に以下のエントリで動作を検証した IPv6 対応が、現在では正しく動作しなくなっているようです。

複数 Stamp を使わない設定を入れると動作するようでしたので、以下の通りいくつかの組み合わせで動作確認を行ってみましたが、現状では IPv6 を有効化する場合は複数 Stamp を使わない設定を入れないと意図した動作になりませんでした。

WEBSITE_DISABLE_CROSS_STAMP_SCALE 設定なし + IPv4AndIPv6


WEBSITE_DISABLE_CROSS_STAMP_SCALE 設定あり + IPv4AndIPv6


WEBSITE_DISABLE_CROSS_STAMP_SCALE 設定あり + IPv6

7 月に確認した時点では特別な設定なしに正しく動作していたので、内部的なアップデートが行われている可能性もありそうです。App Service のスケーリング周りについて多少なりとも理解をしていないと、今回のように意外にはまることがあるということでした。

Microsoft.Extensions.AI のプレビューが公開されたので試した

昨夜 C# 向けの新しい拡張ライブラリとして Microsoft Extensions.AI のプレビューが公開されました。突然出てきた感が凄いですが、個人的には Microsoft.Extensions 名前空間に用意されたライブラリは有用なものが多いので気に入っています。

今回は AI 向けなのでどのレベルをサポートしているのか気になる部分ですが、各種 AI サービスの SDK と Semantic Kernel のようなフレームワークの間を埋めるような抽象化されたライブラリでした。

サンプルでも書かれているように開発中は Ollama を使うけども、本番では Azure AI Inference を使うといった処理が簡単に実現できます。各種 SDK の作法を知らなくてもアプリケーションに組み込めるのは良いですね。必要に応じてキャッシュなども組み込みやすそうな気配です。

正直なところ公式ブログを読むだけで十分といった感じなのですが、OpenAI については Azure OpenAI 向けではなく公式向けのコードになっていて、Azure OpenAI でも使えるのかという疑問もありました。

そこで公式ブログでは書かれていなかった部分について確認したので、メモがてら簡単に残しておきます。

Azure OpenAI を利用する

結論から書いてしまうと、問題なく Azure OpenAI を使うことが可能です。理由としては新しい Azure.AI.OpenAIOpenAI を継承して実装されているため、`OpenAIClient` 向けの拡張メソッドがそのまま利用可能なためです。

NuGet Gallery で Azure.AI.OpenAI の依存関係を確認すると OpenAI が存在していることがわかります。

Azure OpenAI 向けに利用する場合は、Azure OpenAI の SDK と OpenAI 向けのライブラリをインストールするだけで準備ができます。非常に簡単ですね。

実際に Azure OpenAI を使うサンプルコードは以下のようになります。公式ブログで書かれている OpenAI 向けコードとほぼ同じですが、AzureOpenAIClient を使っている部分が大きな違いです。Azure OpenAI の SDK は v2 で大きく使い方が変わったので、その部分で少し戸惑うかもしれません。

using Azure.AI.OpenAI;

using Microsoft.Extensions.AI;

IChatClient client =
    new AzureOpenAIClient(new Uri("https://***.openai.azure.com/"), new System.ClientModel.ApiKeyCredential("AOAI_API_KEY"))
        .AsChatClient("gpt-4o");

var response = await client.CompleteAsync("What is AI?");

Console.WriteLine(response.Message);

このサンプルコードを実行すると、以下のように回答が表示されます。使っている CompleteAsync は推論結果が出るまで待つ API になるため、表示までの待ち時間は長くなります。

最近はよく使われているストリーミングで返す API も用意されていて、以下のように CompleteStreamingAsync を呼び出すと IAsyncEnumerable<T> が返ってくるので await foreach で読み取ればお馴染みのストリーミング出力が簡単に行えるようになっています。

using Azure.AI.OpenAI;

using Microsoft.Extensions.AI;

IChatClient client =
    new AzureOpenAIClient(new Uri("https://***.openai.azure.com/"), new System.ClientModel.ApiKeyCredential("AOAI_API_KEY"))
        .AsChatClient("gpt-4o");

await foreach (var update in client.CompleteStreamingAsync("Microsoft について簡単に説明してください"))
{
    Console.Write(update);
}

実行結果のスクリーンショットでは動作はわからないと思いますが、素早く推論結果の出力が開始されるのでユーザー体験としてはよくなります。このライブラリは .NET チームが作っているようなので、最新の C# 言語機能をちゃんと使っている点もポイントが高いです。

Embedding についても同じように使えるので省略しますが、各種 SDK を直接叩くよりも簡単に Generative AI の機能を利用できるため、アプリケーションへ組み込みやすくなりそうです。

AI Toolkit 経由で SLM を利用する

MEAI で OpenAI SDK が使えるのなら、以前書いた AI Toolkit が公開している OpenAI 互換の REST API を経由して SLM を利用できるのでは?と思ったので実際に試してみました。AI Toolkit については以下のエントリを参照してください。

AI Toolkit のドキュメントでは古い OpenAI SDK ベースでカスタマイズするコードが載っていますが、現行の v2 では書き方が若干変わっているので修正する必要があります。

具体的には OverrideRequestUriPolicy の実装を Azure.Core から System.ClientModel を使うように変更しました。System.ClientModel は Azure SDK の知見から新しく実装された REST API を呼び出す SDK を作りやすくするライブラリです。

実際に用意したサンプルコードは以下のようになります。ほぼ OpenAI を使うコードと同じですがリクエスト URI をオーバーライドするために、追加のオプションを渡している点のみ異なります。

using Microsoft.Extensions.AI;

using OpenAI;

using System.ClientModel.Primitives;

OpenAIClientOptions clientOptions = new();
clientOptions.AddPolicy(new OverrideRequestUriPolicy(new("http://localhost:5272/v1/chat/completions")), PipelinePosition.BeforeTransport);

IChatClient client =
    new OpenAIClient(new System.ClientModel.ApiKeyCredential("unused"), clientOptions)
        .AsChatClient("Phi-3-mini-4k-directml-int4-awq-block-128-onnx");

await foreach (var update in client.CompleteStreamingAsync("Microsoft について簡単に説明してください"))
{
    Console.Write(update);
}

internal class OverrideRequestUriPolicy(Uri overrideUri) : HttpClientPipelineTransport
{
    protected override void OnSendingRequest(PipelineMessage message, HttpRequestMessage httpRequest)
    {
        httpRequest.RequestUri = overrideUri;
    }
}

AI Toolkit を起動させておいて、このコードを実行すると REST API 経由で SLM を使った推論が実行されるので、以下のように Phi-3 を使ったローカルでの実行が簡単に行えます。

推論結果については SLM の性能に依存するのでどうしようもないのですが、ONNX Runtime を使わなくても簡単にローカルで検証できるのは AI Toolkit の OpenAI 互換 API を使うメリットですね。

そして MEAI を使うことで、LLM と SLM の両方を同じコードで実行できるというのもかなり熱いです。

Azure Cosmos DB for NoSQL の DiskANN Preview が使えるようになったので試した

以前に書いた Cosmos DB の Vector Search サポート検証の続きで、先日 Public Preview となった DiskANN を使った Vector Index を作成してみました。

DiskANN は Microsoft Research が開発した、低レイテンシかつコスト効率よく Vector Search を実行するためのアルゴリズムです。Cosmos DB には数種類の Vector Index のオプションが用意されていますが、DiskANN は用意された中で大規模なデータセット向けに最適化されています。

以下のドキュメントでも言及されていますが、Cosmos DB で他に用意されている Vector Index の種類 quantizedFlat はコンテナー内の Vector 数が 10 万以下の場合に推奨されているため、それ以上の場合は DiskANN を使うのが最適ということになります。

以前は DiskANN を使うのに申請が必要でしたが、現在は Public Preview に移行しているため Cosmos DB のコンテナー作成画面で Index Type として DiskANN が選択可能になっています。

今回は検証として、前回 Vector Search の検証に利用したのと同じデータ構造とデータセットを使って、DiskANN を利用した際の挙動を確認してみました。前回検証した内容は以下のエントリにまとまっています。

検証用の作成したコンテナーの Indexing Policy は以下のような構造です。シンプルに quantizedFlat を指定していた部分を diskANN に変更しただけです。Vector を格納するプロパティに対して excludedPaths で除外設定するのは忘れないようにしましょう。

{
    "indexingMode": "consistent",
    "automatic": true,
    "includedPaths": [
        {
            "path": "/*"
        }
    ],
    "excludedPaths": [
        {
            "path": "/contentVector/*"
        },
        {
            "path": "/\"_etag\"/?"
        }
    ],
    "vectorIndexes": [
        {
            "path": "/contentVector",
            "type": "diskANN"
        }
    ]
}

作成した Indexing Policy が以下のようになっていることを確認します。Vector Indexes は後から修正することができないので、間違うとコンテナーから作り直しになるため手間がかかります。

Vector Indexes 以外は quantizedFlatdiskANN で全く違いはありませんので、前回利用したコードを使ってこのブログのエントリを Vector 化して、全件 Cosmos DB に投入してデータを用意しました。

実際に前回と同じクエリで Vector Search を実行したところ、DiskANN では消費 RU が 21.69 RU となりました。前回は 10.3 RU でしたので DiskANN の方が 2 倍も RU を消費しています。

今回は使用したデータセットが 1500 程だったため、DiskANN が有利となる Vector 数以下となり、通常の Quantized Flat の方が性能が良かったようです。大規模なデータセットを利用して 10 万 Vector 以上に対して検索を行う場合には DiskANN が有利となると考えられます。

正直なところ 10 万 Vector 以上のデータセットを探すのが難しいので今回の検証はここまでにしますが、以下のブログを参考に環境を構築するとかなり大規模なデータセットに対して DiskANN を使って Vector Search を行うデモが体験出来るようです。

ただし必要とする RU が非常に大きいので、油断すると超高額請求につながる可能性があるため注意が必要です。個人的にはシンプルに 10 万以上になる可能性がある場合には、無条件で DiskANN を選択するというポリシーで問題ないと考えています。