しばやん雑記

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

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

今年も Ignite 2023 がシアトルで開催されて、様々な Azure のアップデートが発表されました。基本的には AI 系の話ばかりでしたが、App Service / Azure Functions / Container Apps についても、思ったよりもインパクトの大きなアップデートが発表されましたので、個人的に注目している点についてまとめておきます。

発表を見ると App Service などの PaaS / Serverless への投資がガンガンされているので、今後も安心して App Service や Azure Functions を使い続けていきます。

Azure App Service

最近は App Service 周りのアップデートが多くなってきた印象を持っていますが、Ingite 2023 でもインパクトの大きなアップデートが発表されました。AI の活用は Build 2023 でも発表されていましたが、それ以外にも重要となる足回りのアップデートが多いです。

総じて Build 2023 で間もなくと発表されていたものは Ingite 2023 で公開されたという印象です。

Build 2023 で発表された内容については、当時のエントリでまとめているのでこちらも参照してください。

今のところはリージョン限定の機能も多いですが、ようやく実際に触れるようになってきたので検証しつつ必要に応じてまたブログを書いていくことにします。

.NET 8 / Node.js v20 / Python 3.12 サポート

.NET Conf 2023 で .NET 8 が GA になったのとほぼ同時に、現在は Early Access という形で .NET 8 の正式版が Windows / Linux の両方で利用できるようになっています。

同様に Node.js v20 LTS と Python 3.12 のサポートも追加されています。Node.js v20 については Windows / Linux の両方で利用できますが、Python 3.12 は Linux 限定なのはこれまで通りです。

厳密には Windows にインストールされている Node.js v20 は LTS になる前のバージョンなので少し古いです。今後アップデートされるとは思いますが気を付けたい点です。

TLS 1.3 サポートと E2E 暗号化が Public Preview

予告されていた TLS 1.3 サポートが US リージョンから段階的に展開されていることが発表されました。US リージョンへの展開は 2024/1 に完了予定で、他のリージョンは 2024 年初頭ということなので US に比べると多少遅れそうです。

手持ちの West US 2 にデプロイしてある App Service を確認したところ、TLS 1.3 で通信されていることが確認出来ました。数日前は TLS 1.2 だったはずなので段階的な展開は実行中のようです。

Qualys SSL Labs でさらに詳細を確認したところ TLS_AES_256_GCM_SHA384TLS_AES_128_GCM_SHA256 が有効化されていることが分かりました。CCM 系はサポートされないのかもしれません。

TLS 系については Linux App Service の E2E 暗号化も公開されています。特にリージョンの制約は書いていないので、全リージョンで使える可能性があります。

E2E 暗号化が何を指しているのか App Service の内部アーキテクチャについて詳しくないと理解できないと思いますが、現在フロントエンドのリバースプロキシで TLS 終端を行っているのを止めて、Web Worker までトラフィックを TLS で保護するという機能です。つまり現状はリバースプロキシと Web Worker 間は TLS では保護されていないということです。

行っていなかった TLS の処理を Web Worker が行うので、これまでと比べて CPU 負荷が僅かに高まる可能性は否定できませんが、無視できる範囲だとは考えています。

今回の E2E 暗号化を有効化すると Web Worker まで TLS で保護されるようになりますが、個人的には HTTP/2 や gRPC 周りを見越した機能なのではないかと考えています。

単一サブネットへの複数 App Service Plan 統合が Public Preview

Builld 2023 で発表されていた単一のサブネットへ複数の App Service Plan を統合する機能がようやく Public Preview となりました。これまでは 1 つの App Service Plan に対して 1 つサブネットを確保する必要があったので、App Service Plan を増やすのが難しい問題がありましたが解消されそうです。

現在は West Central US と France Central のみで利用できるようですが、今年中にはさらに複数のリージョンでのサポートが追加されて、来年の初めには全てのリージョンで利用可能になる予定のようです。

Azure Portal でのサポートが無いため、Azure CLI や ARM Template などを使う必要があります。恐らく Terraform でも問題なく設定できると思います。

Linux App Service の Sidecar 拡張機能がアナウンス

Linux App Service 版の Site Extensions に近い機能の Sidecar 拡張機能がアナウンスされました。まだ試せる状態にはなっていませんが、セッション内でデモが行われていたのである程度は把握することが出来ます。

実体としては 1 つの App Service 内で複数の Container をホストする機能と言えるので、将来的には Docker Compose を利用した Multi containers 機能を置き換えるものになりそうです。New Relic や Dynatrace といった Observability を提供するサービス向けという説明が大半ですが、ネットワークを共有するので Redis などは動かすことが出来そうです。

同時に WebJobs の Linux App Service と Windows Containers でのサポートが発表されましたが、仕組みとしてはこれと同じものと考えています。

Azure Functions

Azure Functions のアップデートについてはブログの項目的には多いですが、半分以上は Ignite 前に発表されていたものなので目新しいものは Flex Consumption ぐらいになります。そして Flex Consumption が Azure Functions 史上最大のアップデートになりそうです。

.NET 8 対応については別途ブログを書く予定なので触れませんが、発表の中では Azure Functions Core Tools が正式に M1 Mac に対応したのは大きなニュースになりそうです。

Flex Consumption が Private Preview

個人的には Ignite 2023 で一番インパクトのある発表は、Azure Functions の Flex Consumption だと考えています。現在 Early Access Preview という名の Private Preview 中となります。

現時点では公開されている内容が少ないのですが、現行の Consumption Plan と同様の課金体系で VNET や Availability Zone へのサポートが追加され、同時実行制御もこれまで以上に高度な設定が可能になり、Cold Start を避けるオプションとして Functions Premium と同様の Always Ready Instances が追加されるようです。もう Flex Consumption だけで良いのではないかというレベルです。

もう少し詳細が出ないと判断できませんが、Functions Premium を使うシナリオはかなり少なくなりそうという印象です。アーキテクチャはこれまでの Linux Consumption の Service Fabric Mesh からガラッと変わっていそうなので、Public Preview になったタイミングでブログにまとめようかと考えています。

Dapr 拡張が Public Preview

主に Azure Functions on Azure Container Apps を利用している場合向けの Dapr 拡張が公開されました。Dapr が動いている環境であれば利用できるので、Azure Container Apps 以外でも動作はします。

Azure Functions を Azure Container Apps にデプロイした際には、簡単に Dapr 経由で Microservices を呼び出すことが出来ませんでしたが、この拡張を利用することで簡単に呼び出すことが出来るようになります。

既に Visual Studio 向けのテンプレートには Dapr 拡張が追加されているので簡単に始められます。

個人的には Azure Functions の Azure Containers Apps へのデプロイサポートが未だ貧弱なのでイマイチ使う気にはなれていませんが、機能が拡充されれば Container Apps に Azure Functions をデプロイしてアプリケーションを作るケースも増えそうです。

Azure Container Apps

最後は Container Apps ですが、昨今の AI ブームに乗っかった機能が少し追加されています。Add-on は別に Vector Database 専用というわけではないはずですが、以下のブログでは Vector Database のホスティングがアピールされています。

Container Apps も App Service と同様にイベント合わせで機能をリリースするのではなく、定期的に新機能は公開されているので比較的少なめです。

今回のアップデートの本命は GPU workload profile が追加されたことで間違いありません。後で触れますが AKS を使わざるを得ないシナリオがまた一つ減ったという認識です。

ポリシーベースの回復性設定が Public Preview

これまで多くはクライアント側で対応する必要があった Retry や Timeout、Circuit Breaker といった処理をポリシーとして Container App に定義しておけば Dapr がいい感じに対応してくれる機能が追加されました。

本来なら呼び出し先単位で Retry や Timeout といった設定値は異なるはずですが、クライアント側で個別に対応するのは割と面倒なので同じ設定でやってしまうことが多いと思いますが、この機能は呼び出される Container App 側にポリシーとして定義するので、常に最適な設定が行えるようになっています。

現時点では Dapr Service Invocation API では非対応のようなので、その点だけ気を付ける必要があります。

GPU workload profile サポートが Public Preview

かなり要望が多かったと思われる GPU インスタンスへのサポートが Workload Profile に追加されました。これで Container Apps を使って機械学習のモデル作成や推論といった GPU が必要な処理を、AKS を使うことなく簡単に行えるようになります。

インスタンスの種類は自由に選べるわけではなく、現時点では NC A100 v4 シリーズが利用可能になっています。サポートされているリージョンも West US 3 と North Europe の 2 つと少ないので、日本国内で完結させる必要がある処理などは AKS を暫く使い続ける必要があります。

例によって GPU インスタンスのクオータが引き上げられていないと利用できないので、自分の環境で検証するのは少し難易度が高いです。Container Apps で使う場合にはマルチインスタンス GPU に対応しているのかは重要な点な気がしますので、情報が出てくるのを期待したいです。

Azure API Management の新しい Basic v2 / Standard v2 が Public Preview になったので試した

突然 API Management に新しく Basic v2 / Standard v2 という Tier が Public Preview として追加されました。来月が Ignite なのでそこまで待っても良かったのではという気もしますが、App Service などの PaaS / Serverless のチームはイベントを気にせずアップデートしてきますね。

新しい Basic v2 / Standard v2 は高速なリソースのデプロイ、スケーリングが大きな特徴と言えます。そしてこれまで Premium でしか利用できなかった VNET Integration が Standard v2 では利用可能となっています。

現在サポートされていない機能としては Private Endpoint と Availability Zones が大きいです。特に Standard v2 では Availability Zones とマルチリージョンへのデプロイが GA までにサポートされる予定らしいので、Premium よりも圧倒的に安くそして早く高可用性の API Management が利用可能になります。

これらの特徴はプラットフォームの Cloud Services / VM から App Service への変更で実現されています。

新しい Basic v2 / Standard v2 の立ち位置について

現在提供されている Basic / Standard と、新しい Basic v2 / Standard v2 はプラットフォームが大きく変更されていますが、機能としてはほぼ同一のものが提供される予定のようです。それを物語るように v1 から v2 への移行機能も提供予定となっています。

プレビュー中の機能制限としては、まだかなり多くの機能が残っていますが Standard v2 が Premium の機能を多く取り込んでいるように見えます。Premium の機能でサポートされそうにないのは VNET 内にインスタンスをデプロイする機能ぐらいです。

恐らく v2 では Premium あるいは VNET 内にインスタンスをデプロイする機能は提供されないでしょう。その代わりに Private Endpoint は Basic v2 / Standard v2 でサポート予定なので、それを使う形になると思われます。基本的には Private Endpoint と VNET Integration で大体の要件は満たせるはずです。

気になる価格ですが v1 と v2 で同じ Tier の場合はほぼ同じ金額で設定されています。v2 固有の特徴としては API の呼び出し回数での課金が追加されていることと、追加のインスタンスは割安に設定されていることにあります。Basic v2 / Standard v2 でそれぞれ API 呼び出し回数の無料枠が含まれていますが、使われ方次第では v1 よりも割高になる可能性もあります。

このような課金体系からも、将来的には Basic と Standard の v1 については v2 へ一本化されることが想像つきますね。安くなるといったことはないですが、App Service ベースになったことによりデプロイとスケーリングの高速化が得られるのはかなり大きいですし、将来的にはオートスケールもサポートされる予定なので価値はあると考えています。

ここまでの内容を Tier 毎に簡単にまとめておきました。Consumption は一足早く App Service ベースで構築されていたので、試金石だったのかもしれませんね。

  • Developer / Basic / Standard / Premium
    • Cloud Services / VM ベースのアーキテクチャ
    • 2024/9 で Cloud Services のサポートが切れるので stv1 がディスコン、stv2 はそのまま
    • API の呼び出し回数が非常に多い場合にはコストメリットが出る可能性
  • Consumption / Basic v2 / Standard v2
    • App Service ベースのアーキテクチャ
    • 今後のメインストリームは全て v2 になるものと思われる
    • API の呼び出し回数での課金が追加されているので、v1 よりも高くなる可能性

ここから先は実際に Standard v2 の API Management をデプロイして、一番使われるであろう VNET Integration 周りの確認をしておきます。

これまで VNET Integration を使いたくても Premium しかなく価格的に諦めていたケースも多いと思いますが、v2 では Standard から使えるようになっているので、気になっている方は多いはずです。

API Management をデプロイする

まずは Standard v2 の API Management をデプロイするところから始めます。API Management の Tier が多くなったので、将来的には UI が整理されそうな気もします。

UI 上は Japan East / Japan West が指定できますが、残念ながら Basic v2 / Standard v2 は現時点では日本リージョン非対応なのでエラーとなります。しかし経験上 App Service 上に展開されているサービスはリージョンの拡充が早いので、比較的早くに Japan East には来るのではないかと考えています。

プレビュー中は例によって SLA は提供されていませんが、ベースとなっている App Service の SLA が 99.95% になっているので、GA 後には Basic v2 / Standard v2 も 99.95% になるようです。

デプロイ自体は 1 分程度で完了するので、これまでの API Management の Standard に比べると異次元の早さです。これだけでも Standard v2 を使うメリットになっているレベルです。

VNET Integration を有効化する

API Management の Standard v2 デプロイが完了した後は、適当に Virtual Network と Subnet を作成して API Management で VNET Integration を設定します。この辺りは完全に App Service の VNET Integration と同じになるので、あまり独特な挙動はないように見えますね。

設定画面や Subnet Delegation の設定値も App Service と同じなので、一度でも App Service で VNET Integration を設定したことがある人は悩むことはないでしょう。但しマルチリージョン対応のために UI は少し異なっている部分があります。

現時点では Azure Portal で Virtual Network を選択した後に Subnet の一覧が出ない不具合があるようなので、Azure Portal では VNET Integration の設定が行えませんでした。

仕方ないので ARM Explorer を利用して VNET Integration 設定を追加することで対応しました。具体的には ARM Explorer の Raw モードを利用して、以下のように Subnet のリソース ID を渡してあげるだけです。

PATCH /subscriptions/000/resourceGroups/xxx/providers/Microsoft.ApiManagement/service/xxx?api-version=2023-05-01-preview
{
  "properties": {
    "virtualNetworkConfiguration": {
      "subnetResourceId": "/subscriptions/000/resourceGroups/xxx/providers/Microsoft.Network/virtualNetworks/xxx/subnets/xxx"
    },
    "virtualNetworkType": "External"
  }
}

この作業を行うと Azure Portal 側でも API Management の VNET Integration 設定が完了したことが確認出来るようになります。Azure Portal 上は完了までに時間がかかると記載されていますが、実際には 1 分弱で完了するので App Service は偉大だと感じました。勿論 Public VIP が変化することもありませんでした。

Azure Portal のメッセージは v2 向けになっていない部分が多いので、あまり気にせず進めるのが良いです。

非常に素早く API Management のデプロイから VNET Integration の設定まで行えましたが、検証を行っていると VNET Integration を有効化中には、スケーリングの設定変更が必ず失敗することに気が付きました。

あくまでも一時的な問題であり、VNET Integration を有効にしているとスケーリングが行えないということではありませんので注意してください。App Service では対応している設定が、それを利用している API Management では非対応というのはあり得ません。

Service Endpoint で保護された API へ接続する

ここまでの作業で API Management の VNET Integration 設定が完了したので、これからは実際に VNET 経由での安全なアクセスが行えるのかを確認します。まずはシンプルに Service Endpoint を使って保護された API へのアクセスが行えるのか試します。

API は ASP.NET Core のテンプレートで生成したものを App Service にデプロイして試していきます。まずは全てのアクセスを拒否する設定を追加して、API Management からアクセスできないことを確認します。これは App Service の Access Restriction から Unmatched Rule Action を Deny にするだけで対応できます。

API Management の Test を使ってアクセスしてみると 403 Ip Forbidden が返ってきます。想定通りですね。

確認後に API Management が VNET Integration している Virtual Network の Subnet からのアクセスを許可する設定を追加します。Azure Portal 上には記載されていませんが、これは App Service の Service Endpoint を使っている機能です。

再度 API Management の Test からアクセスしてみると、今度は 403 Ip Forbidden が返ってくることなく 200 OK となり、API が正しくペイロードを返しているのを確認出来ます。

これで API Management の Standard v2 でサポートされた VNET Integration は、Service Endpoint で保護された API に対して正しくアクセスできることが確認出来ました。多くのケースでは Service Endpoint を使ったアクセス制限が出来れば十分です。

Private Endpoint で保護された API へ接続する

今度は Private Endpoint で保護された API へのアクセスが行えるのか確認していきます。準備として App Service で Private Endpoint からのアクセスのみ許可する際には、Allow Public Access をオフにしておく必要があります。オンになっているとインターネットからのアクセスが可能になるので、Private Endpoint を使っている意味がなくなってしまいます

Public Access をオフにした後に App Service に対して Private Endpoint を追加します。少しデプロイに時間はかかりますが、Connection state が Approved になれば完了です。

デプロイ完了後に Service Endpoint の時と同様に Test からアクセスしてみると、API へのアクセスが正しく行えることが確認出来ました。App Service では Private Endpoint を利用する際には旧称 Route All 設定*1を有効化する必要がありましたが、API Management ではデフォルトで有効化されているようです。

結果としては問題なく動作したのですが、Private Endpoint の設定直後は API Management から API にアクセスできない事象が発生しました。原因は分かっていませんが、API Management の設定変更を行い、内部的に再起動させることで解消したように見えました。

GA までに挙動が安定することを期待しますが、現時点では Private Endpoint の設定直後はキャッシュなどの影響を受けてしまう可能性がありそうです。

*1:現在は Outbound internet traffic という設定になっている

Dev Container / GitHub Codespaces を利用した Azure Functions 開発環境のベストプラクティス

昔にも Dev Container を利用して Azure Functions の開発環境を構築する方法を書いたのですが、その後 Dev Container の機能強化と Azure Functions のアップデートによってベストプラクティスが変わってきたので、現時点でのベストプラクティスを確認しておきました。

Windows 環境であれば Visual Studio 2022 を利用しておけば Azure Functions + C# の開発環境は一発で構築できますが、それ以外の言語で特に Python の場合は Dev Container を利用した方が良いケースが多いです。最近では Visual Studio Code を使う人も増えていますし、Dev Container を用意しておくと最悪でも GitHub Codespaces 上でブラウザベースの開発が出来るので便利になります。

Azure Functions 向けの Dev Container は以前はテンプレートを利用して作る流れでしたが、現在公開されている Dev Container 向けのイメージは 1 年以上更新がされていないため、もはや使い物になりません。Dev Container のページでも以下のように表示されています。

古いイメージを使ってしまうと Python V2 / Node.js V4 といった新しいプログラミングモデルが利用できないので、Azure Functions Core Tools のバージョンは常に新しいことが重要になります。

Visual Studio Code を使った Azure Functions の開発には Azure Functions Core Tools がインストールされていれば問題ないので、公式で用意されている各言語向けイメージに Core Tools を自動でインストールするのがシンプルです。作成した Dev Container のテンプレートは以下に公開しておきました。

ここから先はテンプレートの内容を簡単に説明しているだけなので、Dev Container を今すぐ使いたい人はあまり読む必要はないです。C# 向けの Dev Container を利用した例となります。

Dev Container のベースとなるイメージを選択する

Visual Studio Code を使うとドロップダウンでベースとなるイメージを選べますが、以下の公式ページでも一覧は確認出来るので、こちらの方が目当てのイメージを探すのが楽です。

今回は C# を使うので .NET のイメージを選択します。imageVariant は適当に選択しておきます。

重要になるのは devcontainer.jsonimage に書かれた値だけなので、この devcontainer.json をコピペして作っても、新しく作成して image を指定する方法でもどちらでも良いです。

Azure Functions Core Tools を自動インストールする

ベースとなるイメージの選択はシンプルですが、Azure Functions Core Tools のインストールは公式ドキュメントの通りにすると Dockerfile を用意してインストールすることになるので手間です。

最近の Dev Container では Features という形で Dev Container に対して Azure CLI などのツールをインストール出来るようになっているので、これを利用して Dockerfile を用意することなく Azure Functions Core Tools をインストールします。

非常に有難いことに、既に Azure Functions Core Tools をインストールする Feature が公開されているので、これを使うだけで任意の言語 + Azure Functions Core Tools の環境が出来上がります。

使い方も devcontainer.json ファイルの featuresghcr.io/jlaundry/devcontainer-features/azure-functions-core-tools:1 を追加するだけなのでシンプルです。これだけで最新の Core Tools v4 がインストール出来ます。v3 が必要な場合は version を指定します。

現時点の問題としてベースイメージに Debian 12 (bookworm) を指定すると Feature のインストールに失敗します。これは以下の Issue で指摘されている通り、まだ Azure Functions Core Tools が Debian 12 向けに公開されていないことあります。

対応されるまでは Debian 11 (bullseye) のベースイメージを使う必要があります。意外にはまる部分なのでバージョン指定は気を付けたいですね。

拡張機能を自動インストールする

Visual Studio Code で Azure Functions 開発を行うには専用の拡張機能が必要になるので、Dev Container が立ち上がったタイミングで自動的にインストールされるように設定します。

ローカルで拡張機能がインストール済みであれば、歯車ボタンを押して表示されるメニューから Add to devcontainer.json を選びます。これで devcontainer.json に設定が追加されます。

後は言語向けの拡張機能をインストールするようにしておきます。C# の場合は GitHub Codespaces にライセンスが含まれている C# Dev Kit をインストールするようにしておけば、一通りの機能が揃うので簡単です。

これで Azure Functions のプロジェクト作成と、その対応した言語でコードを書く準備が出来ます。後は Azure Functions の実行に必要なストレージアカウントを用意するだけです。

Azurite をバックグラウンドで自動起動する

Visual Studio 2022 には Azurite が組み込まれているので、Azure Functions のプロジェクトを作成すれば自動的に Azurite が起動するので、特に設定の必要なしで Azure Functions アプリケーションの開発が行えていましたが、VS Code ではそこまで高度な統合は用意されていないので、自前で何とかする必要があります。

一応 VS Code 向けの Azurite 拡張は用意されていますが、コマンドパレットで起動や停止を行う必要があるのでシームレスではありません。Azurite は Dev Container の起動と同時に立ち上がっていて欲しいので、今回は Docker Compose を利用して同時に Azurite のコンテナーを立ち上げるようにします。

Dev Container では Docker Compose 定義を使うことも出来るので、今回のように同時にデータストア系のコンテナーが必要な場合にも応用出来ます。

特に Dev Container だから書き方が変わるということはないですが、Dev Container 用のコンテナーがすぐに終わらないように command で無限にスリープするように指定しておくぐらいです。

version: "3"

services:
  app:
    image: mcr.microsoft.com/devcontainers/dotnet:0-6.0-bullseye
    volumes:
      - ..:/workspace:cached
    command: sleep infinity
    network_mode: service:azurite

  azurite:
    image: mcr.microsoft.com/azure-storage/azurite
    restart: unless-stopped

Azurite 用のコンテナーはデータを揮発させるつもりなので、特にボリュームの指定をしていません。永続化が必要な場合はマウントを追加して、Codespaces などの揮発しないストレージに保存させればよいです。

この docker-compose.yml を利用すると Dev Container と Azurite の両方が同時に起動します。

完成した devcontainer.json の例

最後に作成した devcontainer.json を載せておきます。dockerComposeFileservice の指定は Dev Container として Docker Compose を使う際に必要なものです。forwardPorts に Azurite のポートを追加しているので、ローカルの Azure Storage Explorer からアクセス可能です。

{
  "name": "Azure Functions (.NET)",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",
  "forwardPorts": [
    7071,
    10000,
    10001,
    10002
  ],
  "otherPortsAttributes": {
    "onAutoForward": "ignore"
  },
  "features": {
    "ghcr.io/devcontainers/features/azure-cli:1": {},
    "ghcr.io/jlaundry/devcontainer-features/azure-functions-core-tools:1": {}
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-azuretools.vscode-azurefunctions",
        "ms-dotnettools.csdevkit"
      ]
    }
  }
}

この定義を使って Dev Container を起動すると、以下の通り Azurite が利用しているポートが稼働していることが分かります。GitHub Codespaces を使ってブラウザで実行している場合には外部からアクセスできませんが、ローカルの VS Code で開くと localhost にフォワーディングされるので、これまで通り Azure Storage Explorer からアクセス可能となります。

Azure Functions を VS Code から作成すると、デフォルトで用意された local.settings.json 内の AzureWebJobsStorage が空になっていますが、コマンドパレットから設定コマンドを実行するとエミュレーターを選択できます。意外に忘れがちなので注意します。

これで Azure Functions の開発環境が Dev Container / GitHub Codespaces 向けに構築出来ました。新しく Azure Functions のプロジェクトを作成して F5 を押せばスムーズにデバッグ実行まで行えることが確認出来ます。後はこの定義をリポジトリに含めておけば完成です。

今回は .NET 向けでしたが、最初に紹介したリポジトリには各言語向けのサンプルも用意しているので、そちらも参照してください。とはいえ考え方は .NET の時とほとんど同じで、重要なのは Azurite になります。

カスタム Docker イメージを利用する場合

.NET や Node.js ではカスタム Docker イメージを使う機会はほぼ無いのですが、Python の場合はビルドが必要なパッケージがあり、コンテナー側にもインストールが必要なケースも多いのでカスタム Docker イメージを使うことがあります。構成についてはドキュメントを確認してください。

Dev Container や GitHub Codespaces のようなコンテナーベースの環境では、Feature として ghcr.io/devcontainers/features/docker-in-docker:2 を追加するとコンテナーの中でも Docker CLI が使えるようになるので、Azure Functions のカスタム Docker イメージを使った開発が行えるようになります。

公開したリポジトリでは Python 向けの Dev Container 定義のみ対応しています。

Azure Functions の .NET 8 向けアップデートが発表された

.NET 8 の GA が 2 カ月後に迫ってきたこのタイミングで、Azure Functions での .NET 8 向けアップデートが本格的に発表され始めました。そろそろ .NET 6/7 で Isolated を利用しているケースではアップデートを検討しても良いでしょう。.NET 6 の In-Process を利用しているケースは後述しますが急ぐ必要はありません。

そして .NET 8 とは直接関係ないですが、.NET 8 から主流になるはずだった Isolated Worker モデルの改善が同時に公開されました。ようやく In-Process に近い開発体験が得られるようになってきましたが、正直パフォーマンスと信頼性の面ではまだまだという印象です。

しかしアップデート内容としては興味深いものがあるので .NET 10 での Isolated への移行を見越して確認しておきました。.NET 10 がリリースされる頃には十分成熟したものになる予感です。

.NET 8 Isolated への対応が Preview

今回のアップデートのメインはこれです。Azure Functions v4 で .NET 8 Isolated への対応が Preview として公開されました。Azure Functions Core Tools のバージョン 4.0.5312 以降がインストールされていれば、プロジェクト作成時に net8.0 が選べるようになっています。

Visual Studio Code では最新の Azure Functions 拡張と .NET 8 SDK がインストールされていれば、コマンドパレットから新規プロジェクト作成時に .NET 8 Isolated Preview が選べるようになります。

Visual Studio 2022 ではバージョン 17.8 Preview と .NET 8 SDK がインストールされていて、最新の Azure Functions のツールセットとテンプレートに更新済みであれば、プロジェクト作成時に .NET 8.0 Isolated が選べるようになります。17.7 でも .NET 8 SDK が入っていれば選べるかもしれませんが未確認です。

Visual Studio 2022 の場合は WinGet などでインストールした Azure Functions Core Tools が使われず、Visual Studio が管理しているバージョンが使われるようになっているので、選択できない場合にはオプションから Azure Functions のツールセットとテンプレートを更新しておきます。

意外に知られていないので、ワークショップなどで Visual Studio 2022 を使って Azure Functions 開発を行う際には、事前にチェックしておいた方が良いですね。意外にダウンロードに時間かかることがあります。

Azure Functions チームのツイートでは Linux で .NET 8 Preview のサポートが追加されたとありますが、既に Early Access で Windows でも利用可能になっています。

これまでの Early Access と同様にコールドスタートのパフォーマンスはあまりよくないですが、GA すると自動的に最適化されるようになっています。

.NET 8 での In-Process サポートが追加予定

以前のロードマップでは .NET 8 からはこれまで主流だった In-Process のサポートがなくなり、既存の Azure Functions は全て Isolated Worker への移行が必要となるはずでしたが、最新のロードマップでは .NET 8 でも In-Process サポートが追加されることになりました。

但し .NET 8 が GA されてすぐに対応するのは Isolated Worker だけで、In-Process のサポートは少し間が空くようです。しかし .NET 6 のサポートは来年まで続くので多少の遅れは問題になりません。

Semantic Kernel が最新バージョンでは Isolated を選ばないと動作しないことは以前お話ししましたが、アセンブリのバージョン問題が解消されるため .NET 8 の In-Process がリリースされると問題なく利用できるようになります。.NET 8 が最新のうちは安定して動作するはずです。

ただし .NET 9 がリリースされると .NET 5 がリリースされた時と同様に、またバージョン問題が発生するので流石に .NET 10 では Isolated への移行を真剣に検討したいと思います。

少なくとも Isolated Worker のパフォーマンスと信頼性が高まり、本番での運用が可能なレベルに達するまでの時間は十分あるはずです。それを待ってからでも遅くないでしょう。

ASP.NET Core Integration が GA

Isolated でも In-Process と同じように ASP.NET Core 関連クラスを使い、簡単に HttpTrigger を実装できる ASP.NET Core Integration が GA しました。

パッケージのバージョンは 1.0.0 になっていますが中身は 1.0.0-preview4 と同じです。

利用方法については以下のエントリで書いたので、気になる方はこちらを参照してください。

ASP.NET Core Integration と同時に Azure Functions Host 側にも新しい HTTP パイプラインが実装されたので、まだ安定性の面では不安が残る部分がありますが、ストリーミング周りが正しく動作するようになったので、パフォーマンス面では有利になってきます。

コールドスタートの最適化が Preview

Azure Functions の永遠の課題でもあるコールドスタートの高速化ですが、構造上 In-Process よりも不利な Isolated Worker について複数の最適化が追加されました。ドキュメントに利用方法がまとまっています。

今回追加されたのは Placeholders と Optimized executor の 2 つになります。ReadyToRun は以前から利用可能で、何なら In-Process でも有効化出来るものです。

  • Placeholders (Preview)
  • Optimized executor (Preview)
  • ReadyToRun

ReadyToRun は既に何回も触れているので、詳細は以下のエントリを参照してください。

ここからは Placeholders と Optimized executor の 2 つについて確認した範囲で書いていきます。現時点でも効果の測定中ですが、今のところそんなに大きな違いはなさそうです。

Placeholders について

Placeholders は Azure Functions の Consumption Plan に用意されている機能で、予め Azure Functions の Runtime をユーザーコードが入っていない状態でプロビジョニングしておいて、コールドスタートにかかる時間を短縮するものです。

仕組みについて公式な説明はないのですが、Azure Functions チームの牛尾さんが Linux については仕組みを簡単に書いているので、気になる方はこちらを参照してください。Windows も同じような仕組みです。

既存の In-Process の Azure Functions ではデフォルトで有効化されているので、Consumption Plan を使っている場合には意識することなく恩恵に預かっています。ちなみに設定でオフにすることも可能です。

Placeholders が有効化されていると、環境変数などで特殊なパスに展開されているのが確認できます。Placeholders で起動されて、CPU bitness まで専用に確保されているのが分かりますね。

このような仕組みなので Isolated でも Placeholders を有効化することで、コールドスタートにかかる時間の短縮が期待できるわけです。ただし新しくプロセスを立ち上げる必要はあるので、その部分で時間がかかってしまうとあまり効果は出ないです。

Optimized executor について

今回のアップデートのメインは Optimized executor だと考えています。理由としては Isolated じゃないと不可能な最適化なのと、将来的には Native AOT によって劇的なコールドスタートの改善が期待できるからです。

具体的には C# の Source Generator を利用して、リフレクションを使って実行時に Function を初期化して呼び出すのではなく、ビルド時に Function の初期化と呼び出すコードを生成してしまう仕組みです。そのためリフレクションのコストを無くせるので効率的になります。

Optimized executor の設定を有効化してビルドしたアセンブリを dotPeek などで確認すると、DirectFunctionExecutor というクラスが含まれていることが分かります。

実装を見るとリフレクションを使って Function の初期化と呼び出しを行うのではなく、特定の Function 名の場合は実装されたクラスをインスタンス化して、メソッドを直接呼び出すというコードが生成されています。

外部から文字列で与えられた値で、任意のメソッドを呼び出すのは結構な高コストなので、最近はこの分野では Source Generator を使うケースが増えていますね。

面白い点としては IFunctionExecutor を実装して DI に登録すれば、自由に Function 実行部分をカスタマイズできるようなので、独自に最適化するという方法も取れそうです。

当然ながら開発チームとしても Native AOT も視野に入れているようなので、以下のような Issue で Native AOT に必要なタスクがまとめられています。

Native AOT が有効化されるとコールドスタートは劇的に改善するはずなので、出来れば .NET 10 ぐらいまでには実装されていて欲しいですね。

Azure Functions (.NET Isolated Worker) に追加された ASP.NET Core Integration を一通り試した

今年の 11 月にリリース予定の .NET 8 と同時に .NET 向け Azure Functions は、これまでの In-Process モデルから Isolated Worker Process というモデルに統一されるのですが、正直なところ完成度が低いのと In-Process からの移行を全く考慮していない SDK などの要因で .NET 6 の LTS が切れるまでは In-Process でギリギリまで粘るつもりでした。

流石に Azure Functions の開発チームも In-Process からの移行が全然進んでいないことを気にしているのか、In-Process で対応していた機能を Isolated 側でも実装するようになってきました。このあたりについては以下の Issue にまとまっています。

正直なところ HttpTrigger での ASP.NET Core 関連クラスのサポートがなくなり、完成度の低い HTTP 抽象化モデルになってしまったのが最悪でしたが、ASP.NET Core Integration という形で現在プレビュー段階ですが利用できるようになりました。

利用方法は公式ドキュメントにまとまっていますので、テンプレートで生成されたコードを少し変更するだけで有効化出来ます。使い勝手としては In-Process とほぼ同じです。

これまでは全ての Function 実行が gRPC 経由で行われていましたが、今回 ASP.NET Core Integration のために HTTP の場合は直接バックエンドの Worker にプロキシする機能が追加されたようです。アーキテクチャ図が以下の Issue に載っているので分かりやすいです。

HTTP に対しても gRPC に一度変換していたことで、余計なオーバーヘッドや Stream が使えないといった問題が発生していたのが、ASP.NET Core Integration と同時に追加された仕組みで綺麗に解消しそうです。

軽く実装を見た感じではリバースプロキシには YARP が使われているようなので信頼性は非常に高そうですし、パフォーマンス面でも問題は発生しないでしょう。仕組みとしては .NET 専用というわけではないので、将来的には他の言語向けにも追加される可能性が高そうです。*1

ASP.NET Core Integration を有効化する

ここまでは .NET Isolated Worker の問題点と ASP.NET Core Integration での変更について簡単に書いてきましたので、ここからは実際に ASP.NET Core Integration を触っていくことにします。

Azure Functions のプロジェクトは当然ながら .NET Isolated で作成する必要がありますが、.NET のバージョンは 6 か 7 の好きな方を選べばよいです。現時点では特に .NET のバージョン違いで差はないようですが、今から .NET 7 を選ぶ理由はほぼないので .NET 6 を選んでいます。

プロジェクトを作成したら NuGet から Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore をインストールします。まだプレビューなので可能な限り最新版を使うようにします。

パッケージをインストールすれば Program.cs を以下のように ConfigureFunctionsWebApplication を呼び出すように変更します。これだけで HttpTrigger 内で ASP.NET Core のクラスが使えるようになります。

using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .Build();

host.Run();

後はテンプレートで生成された HttpTrigger の実装を In-Process のように変更するだけです。一般的には HttpRequest を受け取って IActionResult を返すようにするのが分かりやすいと思います。Function の実装が In-Process と全く同じになったので、ようやくスムーズに移行が出来そうな気がしてきましたね。

public class Function1
{
    private readonly ILogger _logger;

    public Function1(ILogger<Function1> logger)
    {
        _logger = logger;
    }

    [Function("Function1")]
    public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
    {
        _logger.LogInformation("C# HTTP trigger function processed a request.");

        return new OkObjectResult("Welcome to Azure Functions!");
    }
}

これでコード側の修正は完了ですが、動作させるためには追加の設定が必要になります。

ASP.NET Core Integration は新しいアーキテクチャ上に構築されているので、現状は local.settings.jsonAzureWebJobsFeatureFlagsEnableHttpProxying を設定する必要がありますが、将来的に Azure Functions Host が 4.24.3 以上に更新されると必要なくなります。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AzureWebJobsFeatureFlags": "EnableHttpProxying",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  }
}

ちなみに Azure 上にデプロイされているのは 4.24.3 なので設定は必要ありませんが、Core Tools の方は古いバージョンなので挙動が異なっています。こういうバージョンのミスマッチは困ったものです。

これで全ての設定が完了したので Visual Studio からデバッグ実行すると、HttpTrigger の動作を確認できるはずです。シンプルな HttpTrigger だとあまり差を感じませんが、実装は HTTP 向けに効率化されています。

Azure Functions Host は YARP を使ったリバースプロキシ、.NET Isolated 側は ASP.NET Core の Endpoint Routing を利用した Function 呼び出しというように、In-Process の時よりも圧倒的にシンプルになったので、謎の挙動に悩まされることが減る可能性が高いです。

例を挙げるとストリーミングを利用した際の挙動が大幅に改善されています。ASP.NET Core Integration を使わずにファイルのダウンロードを Stream で実装すると以下のようなコードになりますが、実行してみるとストリーミングで処理されません。

public class Function1
{
    private readonly ILogger _logger;
    private static readonly HttpClient _httpClient = new();

    public Function1(ILogger<Function1> logger)
    {
        _logger = logger;
    }

    [Function("Function1")]
    public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
    {
        _logger.LogInformation("C# HTTP trigger function processed a request.");

        var stream = await _httpClient.GetStreamAsync("https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.5198/func-cli-4.0.5198-x64.msi");

        var response = req.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add("Content-Type", "application/octet-stream");

        response.Body = stream;

        return response;
    }
}

メモリの使用量を見ると分かるように、GetStreamAsync で返ってきたデータを全てメモリ上に展開してから処理するので、ダウンロード開始までに時間がかかりますし、そもそもリソースの使用効率が悪いです。

この挙動は応答を gRPC として渡していたので Stream として扱えないことが根本原因です。

新しい ASP.NET Core Integration を利用すると FileStreamResult を使って簡単にファイルダウンロードを実装出来ます。HTTP はプロキシ経由でクライアントに渡されるので Stream のまま扱うことが出来ます。

public class Function1
{
    private readonly ILogger _logger;
    private static readonly HttpClient _httpClient = new();

    public Function1(ILogger<Function1> logger)
    {
        _logger = logger;
    }

    [Function("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
    {
        _logger.LogInformation("C# HTTP trigger function processed a request.");

        var stream = await _httpClient.GetStreamAsync("https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.5198/func-cli-4.0.5198-x64.msi");

        return new FileStreamResult(stream, "application/octet-stream");
    }
}

同じように実行してみると、ASP.NET Core Integration の場合はメモリ使用量が増えていないことが確認できます。正しくストリーミングでダウンロードが行われているので効率的です。

ここまで ASP.NET Core Integration のメリットについても書いてきましたが、実際には HttpTrigger を扱う新しいアーキテクチャのメリットという感じです。非常にシンプルで信頼性の高いコンポーネントで構築されており、コントロール可能な部分が増えたので歓迎したいですね。

ここからは実際に ASP.NET Core Integration を試していた時に気になった点を書いています。

ASP.NET Core の HttpContext を取得する

古い HttpTrigger は gRPC ベースだったので HttpContext 自体が存在していませんでしたが、ASP.NET Core Integration を使うと HttpContext が内部的に存在しています。

しかし直接 HttpContext が DI やバインディングで渡されるわけではなく、FunctionContext 経由で取得する必要がある点には注意が必要です。FunctionContext はメソッドへのバインディングで取得出来ます。

以下のサンプルコードのようにバインディングで取得した FunctionContext に対して、GetHttpContext 拡張メソッドを使うと HttpContext を取り出せます。

public class Function1
{
    private readonly ILogger _logger;

    public Function1(ILogger<Function1> logger)
    {
        _logger = logger;
    }

    [Function("Function1")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req, 
        FunctionContext functionContext)
    {
        _logger.LogInformation("C# HTTP trigger function processed a request.");

        var httpContext = functionContext.GetHttpContext();

        return new OkObjectResult("Welcome to Azure Functions!");
    }
}

デバッグ実行してみると HttpContext が正しく取得出来ていることが確認できます。

In-Process では IHttpContextAccessor を DI 経由で受け取ることも出来ましたが、Isolated ではエラーになるのでそのままでは使うことが出来ません。利用するためには以下のように DI に登録して、Function Middleware で現在の HttpContext を設定してあげる必要があります。

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

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication(workerApplication =>
    {
        workerApplication.UseMiddleware(async (context, next) =>
        {
            var httpContextAccessor = context.InstanceServices.GetRequiredService<IHttpContextAccessor>();

            httpContextAccessor.HttpContext = context.GetHttpContext();

            await next();
        });
    })
    .ConfigureServices(services =>
    {
        services.AddHttpContextAccessor();
    })
    .Build();

host.Run();

これで DI 経由で IHttpContextAccessor を取得できるようになったので、以下のように Function の実装を行えば HttpContext を取得出来ます。IHttpContextAccessor は意外と必要になるケースが多いので、対応方法としては覚えておいて損はないと思います。

public class Function1
{
    private readonly ILogger _logger;

    public Function1(IHttpContextAccessor httpContextAccessor, ILogger<Function1> logger)
    {
        _httpContextAccessor = httpContextAccessor;
        _logger = logger;
    }

    private readonly IHttpContextAccessor _httpContextAccessor;

    [Function("Function1")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
    {
        _logger.LogInformation("C# HTTP trigger function processed a request.");

        var httpContext = _httpContextAccessor.HttpContext;

        return new OkObjectResult("Welcome to Azure Functions!");
    }
}

こちらもデバッグ実行してみると、正しく HttpContext が取得出来ていることが確認できます。

将来的には FunctionContextIHttpContextAccessor のように DI 経由で取得できるようになるかもしれませんが、まだ設計段階のようなので時間がかかりそうな気配です。

この辺りは自作ライブラリで利用するために念入りに確認した経緯があります。In-Process から Isolated へスムーズに移行するために、ライブラリで違いを吸収するように作ってあります。

ASP.NET Core Integration によって HttpTrigger の移行はスムーズに行えそうな手ごたえがあります。

Kestrel のオプションを変更する

Isolated の ASP.NET Core Integration は Kestrel と Endpoint Routing 上に実装されているので、In-Process とは異なり Kestrel の設定がカスタマイズ可能です。

試している範囲だとファイルアップロードが例によって 30MB ぐらいで失敗してしまったので、リクエストボディの上限値を変更する必要がありました。詳細は以下のドキュメントを参照してください。

ドキュメントでは ConfigureKestrel を使って設定をしていますが、Isolated では Configure<T> を使って KestrelServerOptions を直接設定するように書けば反映されます。以下のサンプルでは MaxRequestBodySize の設定を変えて、大きなファイルでもアップロード可能にしています。

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services =>
    {
        services.Configure<KestrelServerOptions>(options =>
        {
            options.Limits.MaxRequestBodySize = 300_000_000;
        });
    })
    .Build();

host.Run();

このサンプルでは一括で制限値を変更しましたが、シナリオによっては特定の Function のみ制限を緩くしたいといったケースもあります。そういった場合には Middleware を利用することで対応可能です。Function の Middleware については以下のドキュメントを参照してください。

ASP.NET Core にはリクエスト単位でボディのサイズを制限する IHttpMaxRequestBodySizeFeature が用意されていますので、これを利用して Function 単位で設定を変更することが出来ます。

以下のようなコードを書くと Function1 だけボディのサイズ制限をカスタマイズすることが可能です。

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication(workerApplication =>
    {
        workerApplication.UseWhen(context => context.FunctionDefinition.Name == "Function1", async (context, next) =>
        {
            var httpContext = context.GetHttpContext();
            var httpMaxRequestBodySizeFeature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();

            if (httpMaxRequestBodySizeFeature is not null)
            {
                httpMaxRequestBodySizeFeature.MaxRequestBodySize = 300_000_000;
            }

            await next();
        });
    })
    .Build();

host.Run();

ASP.NET Core では Features が良く出てきますが、.NET Isolated にも別途 Features が用意されているので混同しないように注意が必要です。具体的には FunctionContext にも Features が存在しますが ASP.NET Core の Features とは全く別物です。

ルーティングの挙動に In-Process と同様の問題あり

Azure Functions の In-Process の時からルーティングに癖があったのですが、何故か Isolated でも同じ癖を受け継いでいるようで困りました。具体的には以下のようなルーティングを書いた時に発生します。

public class Products
{
    [Function(nameof(Details))]
    public IActionResult Details(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Products/{id}")] HttpRequest req,
        string id)
    {
        return new OkObjectResult("Invoke Details function");
    }

    [Function(nameof(List))]
    public IActionResult List(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Products/List")] HttpRequest req)
    {
        return new OkObjectResult("Invoke List function");
    }
}

本来であれば Products/List の方が Products/{id} よりも具体的な定義なので、Products/List にアクセスされた場合は List の呼び出しが優先されるはずですが、Azure Functions のルーティング実装では何故かアルファベット順になっているようです。

以下は実際にアクセスした例ですが、意図した List への呼び出しではなく Details が呼び出されていることが分かります。ちなみに Details の Function 名を X などに変えると意図した通りの挙動になります。

この挙動は ASP.NET Core のルーティングとはドキュメントにもあるように当然ながら異なっていますし、利用する側としても自然なものではないため非常に扱いにくいです。

一応 Issue は作成していますが、重要性を理解してもらえない可能性が高いのであまり期待していません。何とか DI でカスタマイズして挙動を改善できないか検討している段階です。

折角 YARP と Endpoint Routing を使っているのに、最後の最後でいまいちな実装が残っていて残念です。

*1:特に Python は既に WSGI 対応が追加されているので対応するメリットが大きそう

Application Insights の Code Optimizations 機能を ASP.NET Core アプリケーションで試してみた

少し前から Application Insights の Performance を開くと、上部に Code Optimizations というボタンが表示されるようになっています。隣にある Profiler は以前からある機能ですが、Code Optimizations はひっそりと追加された新しい機能となります。

あまり注目されている気配を感じないのですが、上手く有効化出来ると継続的にアプリケーションのパフォーマンスを解析し、改善するというループを回しやすくなるはずです。ドキュメントがあるので、こちらも目を通しておくのが良いです。ちなみに現在サポートされているのは .NET のみです。

後で触れますが、Code Optimizations を利用するには Profiler を有効化する必要があります。この有効化方法が少し悩ましいので後半でまとめておきました。

Code Optimizations と Profiler で同じような機能を提供していますが、Profiler はトレースを収集するのがメインで、その結果は自分で解析・調査する必要があるのに対して、Code Optimizations は Profiler のトレースを AI ベースで自動解析してくれるので、パフォーマンス解析の知識が少なくても扱えるのが特徴です。

上手く有効化出来ていると Code Optimizations を開くと一覧が表示されるようになります。

自動的にグルーピングしてくれるので問題点を把握しやすいです。回数や影響度も同時に確認できるので、影響度の高いものから優先して対応するといったことも容易です。

一覧から項目を選ぶと更にドリルダウンして詳細を確認できます。スタックトレース込みで問題点についての詳細が AI ベースで生成されているので、アプリケーションのパフォーマンス改善に役立ちます。今回はサンプルなので ASP.NET Core 側の問題点が出てきましたが、通常のアプリケーションならユーザーコード側の問題点が検出されることが多いはずです。

影響度についてもグラフで表示してくれるので、検出された問題点にどのくらいのインパクトがあるのかをすぐに把握できます。閾値は AI によって自動的に判断されているようです。

Code Optimizations は Application Insights の機能であり、App Service の機能ではないため Profiler を有効化出来れば何処でも利用可能です。今回は App Service で試していますが、以下のドキュメントにもある通り Profiler 自体は様々な環境で有効化出来ます。

軽く触っただけですが、単なる APM ではなく AI ベースで問題の検出と解決策についても提案してくれる Code Optimizations は試してみる価値があると言えます。課題としては Application Insights Profiler の有効化が少し難しいというぐらいです。

ここから先は Code Optimizations というよりも Application Insights Profiler を有効化する方法をメモしておきます。特に App Service で利用する場合に悩むので、最適な設定を探る意味もあります。

ASP.NET Core 向けに Application Insights を有効化する方法

今回は ASP.NET Core 向けの話になりますが、App Service との組み合わせでは App Service に組み込まれた自動インストルメンテーションを使う方法と SDK をアプリケーションに組み込む方法があります。

詳細は以下のドキュメントに任せますが、それぞれの方法には特徴があるので最適なものを選んでいただければと思います。個人的には SDK をインストールする方法をフィルタリングなど高度なカスタマイズが可能なのでお勧めしています。

重要なのは Application Insights Profiler を有効化する方法は Application Insights の有効化方法に依存するという点です。混在はサポートされていないようなので注意が必要です。

App Service 組み込みの App Insights Profiler を有効化する

まずは非常にシンプルな App Service 組み込みの Application Insights Profiler を有効化する方法ですが、これはほぼ説明が要らない気がしますのでドキュメントを紹介するぐらいで行きます。

Application Insights 自体を App Service で有効化していれば、Application Insights Profiler も Azure Portal からポチっと有効化するだけで終わります。非常にシンプルですがカスタマイズは出来ません。

有効化後に Profiler の実行ログを確認して、正しくトレースが送信されているかを確認すれば終わりです。しばらく待てば Code Optimizations の方にも何かしらの検出結果が表示されるはずです。

もし正しく動作していないように見える場合は、以下のドキュメントにある通り診断ツールを使って、正しく Application Insights の Agent が読み込まれているか確認してください。

原因としてはアプリケーション側に Application Insights SDK が組み込み済みというのが多いですが、その辺りについても診断ツールは検出してくれます。

SDK をインストールして App Insights Profiler を有効化する

ASP.NET Core アプリケーションに Application Insights SDK をインストールするのはよく行われていると思いますが、この場合 App Service に組み込みの Application Insights Profiler は使えないので、NuGet で公開されている Profiler をインストールします。

この方法は Linux や Container 環境向けで紹介されていますが、Windows + SDK の組み合わせでも問題なく動作します。ドキュメントがあるのでこちらも参照してください。

設定方法は NuGet から Microsoft.ApplicationInsights.Profiler.AspNetCore をインストールすることから始めます。このパッケージに Profiler の実装が含まれているのでサイズは少し大きくなります。

パッケージをインストールした後は以下のように Startup.cs などで DI にサービスを追加するだけで終わります。Application Insights SDK がインストール済みの場合は AddServiceProfiler だけを追加します。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

// この 2 行を追加する
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddServiceProfiler();

var app = builder.Build();

これでデフォルトの設定で Profiler が有効化されて、ランダムで Profiling が実行されるようになります。

細かいカスタマイズ項目については GitHub でドキュメントが公開されているので、必要に応じて設定を追加してもらえれば問題ありません。この辺りも SDK を使う上でのメリットですね。

App Service にデプロイする際には Application Insights の接続文字列が設定されていることを確認します。App Service で Application Insights を有効化すると App Settings に大量のキーが追加されてしまいますが、SDK の場合は以下のようにシンプルな設定で済みます。

こちらも Azure Portal から Profiler の実行ログを確認すると、正しく動作しているのか分かります。デフォルトではランダムで動くようになっているので、そこそこログは表示されるはずです。

これで SDK を利用している場合でも Application Insights Profiler を有効化出来たので、同じように Code Optimizations を利用できるようになります。どちらの方法も簡単ですが、カスタマイズ性とアプリケーションに組み込み可能かの観点で選ぶのが良いです。

App Service と SDK で Profiler を有効化した際の違い

ここまで紹介した 2 つの方法はどちらを使っても Profiler と Code Optimizations は利用可能になりますが、実際には細かい違いがあります。特に分かりやすい点だと生成されるトレースのフォーマットが .diagsession.netperf で異なります。

App Service の方は Visual Studio で標準的に使われる .diagsession が出力され、SDK の方は dotnet-trace で使われる .netperf が出力されます。

VS Code などで開発している場合には .netperf ファイルの方がビジュアライズの方法が柔軟なので、こちらの方が良いかもしれません。VS を持っている場合は一応どちらも読めそうです。

後は地味に気になる Profiler の負荷ですが、雑に App Service を 2 つ作成して確認したところ、App Service で有効化するよりも SDK で有効化した方が CPU 負荷は低いようでした。

この差が付いているのは Profiler の実装方法の差によると考えていて、App Service で有効化すると Profiler のプロセスが常に立ち上がっているのと、Profiling 方式の違いで高めになっている可能性があります。

本番環境では CPU 負荷を出来るだけ下げたいので、そういう意味でも SDK で有効化した方が良さそうです。

App Service Authentication と Entra ID で保護された Web API にアクセス可能な Access Token を取得する

App Service Authentication (Easy Auth) は非常に便利な機能なのですが、Web API をホストしている場合には他のアプリケーションから Service Principal を利用してアクセスしたいことがあります。

直近では自分が開発している Key Vault Acmebot というアプリで Web API を公開していますが、Easy Auth を有効化したまま Web API を呼び出したいという要望が多くて、とりあえずサンプル用意するかと思ったら地味にやり方を忘れていたのでブログに書いています。

ドキュメントに書かれていない気もしますが Easy Auth はリクエストに Bearer Token を付けて投げると、正しく検証してクレームをデコードしてくれるようになっています。これを使うと色々楽になります。

アプリケーション側の実装は Easy Auth を使っているので手を入れる必要がないですが、Entra ID 側のアプリケーション設定をほどほどに弄る必要があるので理解していないとはまります。

追記 : Entra ID に非常に詳しい @watahani さんからフィードバックを貰ったので、Client Credentials Flow で必要のない部分について修正を行っています。ブログも書いてくださったので是非読んでください。

ちなみに Key Vault Acmebot というアプリでは App Role を検証する機能が付いているので、有効化すると誰でもアクセス出来る状態ではなくなります。

App Service Authentication を有効化

まずは App Service Authentication を有効化して Microsoft Entra ID でのログインを行えるようにします。設定方法はドキュメントもありますし、Azure Portal からであれば簡単に行えるので特に深く説明はしません。

現在の Azure Portal を利用すると Entra ID の v2.0 エンドポイントが使われるので問題ありませんが、手動で設定する際には openIdIssuer が v2.0 エンドポイントになっているか確認しておきます。

自動生成された Application 設定を更新

Easy Auth の設定時に Entra ID 側に新しい Application が追加されますが、デフォルトの設定のままでは必要な Access Token が取得できないので、いくつかの設定を弄る必要があります。具体的には API スコープの公開と Access Token のバージョン変更が必要です。

API スコープの公開は Expose an API から Application ID URI などを設定するだけなので分かりやすいです。作成直後では空っぽですが Edit を選ぶと推奨される値が自動設定されるので保存して終わりです。

Application ID URI を設定するとスコープの値にも正しく反映されます。デフォルトでスコープは 1 つ設定されているので、後でこのスコープに対する許可を Service Principal に対して追加します。

追記 : Client Credentials Flow の場合はスコープの許可を追加する必要はありません。

最後に Application のマニフェストを編集して accessTokenAcceptedVersion の値を 2 に設定します。この設定で発行される Access Token のバージョンが 2 に更新されてフォーマットが大きく変わるため、Easy Auth でも正しく検証できるようになります。

バージョンを未設定の場合には Easy Auth では検証できないフォーマットとなるので認証が通りません。地味にはまりやすいポイントなのでバージョンについては気を付けたいところです。

Service Principal を作成し権限を追加(追記あり)

これで Web API 側の Application 設定は全て完了したので、ここからは Service Principal 周りの設定を行って必要な権限を持った Access Token を取得出来るようにします。

追記 : Client Credentials Flow を利用する場合には Service Principal に対してスコープを追加する必要はなく、同一テナントに存在するアプリケーションであれば全てにアクセス可能になります。

Service Principal の作成方法は書かないので、以下のドキュメントを参照して作成しておいてください。

作成した Service Principal で必要な作業は API Permissions で Web API 側の Application のスコープを追加することです。Add a permission から Web API の該当スコープを探して追加すれば完了です。

後は Client Credentials Flow で利用するための Client Secret を生成しておくぐらいで完了です。

MSAL を利用して Access Token を取得

最後は作成した Service Principal と MSAL を利用して Web API にアクセス可能な Access Token を取得します。今回は Client Secret を利用して Access Token を取得するので Client Credentials Flow となります。

例によって MSAL の使い方は公式ドキュメントに任せるので、経験が無い方はこちらを参照してください。

コードとしては ConfidentialClientApplicationBuilder を使う方法なので比較的簡単ですが、スコープの扱いだけ注意が必要です。Access Token を取得して Web API を呼び出すサンプルは以下のようになります。

using System.Net.Http.Headers;

using Microsoft.Identity.Client;

var app = ConfidentialClientApplicationBuilder.Create("<client id>")
    .WithClientSecret("<client secret>")
    .WithTenantId("<tenant id>")
    .Build();

var token = await app.AcquireTokenForClient(new[] { "<application uri>/.default" }).ExecuteAsync();

Console.WriteLine(token.AccessToken);

var httpClient = new HttpClient();

httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);

var response = await httpClient.GetStringAsync("https://***.azurewebsites.net/api/certificates");

Console.WriteLine(response);

AcquireTokenForClient に Web API のスコープを渡していますが、今回のようなアプリケーション向けにトークンを取得する場合には .default で終わるスコープを指定する必要があります。

詳細は以下のドキュメントに記載されていますので、こちらを参照してください。

このコードを実行すると Access Token を取得して、Easy Auth で保護されている Web API が呼び出されます。以下は実行した例ですが API の呼び出しまで成功しています。

これで Service Principal を使って Access Token を取得し、Easy Auth で保護されている Web API を正しく呼び出せていることが確認出来ました。割と応用が利くので覚えておいて損はないはずです。

補足 : MSAL を利用せずに Access Token を取得する

今回は Access Token の取得に MSAL を使いましたが、Client Credentials Flow は token エンドポイントに POST リクエストを投げるだけなので、MSAL をわざわざ使う必要はありません。以下のドキュメントにもあるように、シンプルなリクエストを投げれば取得可能です。

重要なのは Web API 側でスコープを公開すること、Service Principal でスコープへの権限を追加すること、そして Access Token の取得時にスコープを指定することになります。

補足 : Easy Auth を使って Access Token を取得する

MSAL や HTTP リクエストを自分で叩いて Access Token を取得する以外にも、Easy Auth を使っている Web アプリケーションの場合は Azure Resource Explorer などでスコープを追加すればログイン時に必要な Access Token を同時に取得できます。

ちなみに Easy Auth で保護された Web App から同じく Easy Auth で保護された Web API を呼び出すサンプルは、珍しく公式ドキュメントにまとめられていますのでこちらを参考にするとよいです。

とはいえ考え方は Service Principal を利用する場合とほぼ同じで、ログイン時にアクセスしたい Web API のスコープを指定して Access Token を要求するだけです。スコープの指定方法だけが少し面倒ですが、コードを書く必要なく設定だけで済むためとても簡単です。

追記 : Easy Auth を使って Access Token を取得する場合にはユーザーがログインする側のアプリケーションに対して Web API のスコープを許可する必要があります。

Terraform と Azure DNS を使って Azure Front Door への Apex ドメイン割り当てを自動化する

Azure Front Door は Azure DNS のような Alias record set に対応したサービスと組み合わせると、Apex ドメインを割り当てて利用することが可能です。もちろん Managed Certificate として必要な証明書も自動で発行されるので HTTPS を無料で有効化できます。

手順は全て以下のドキュメントにまとまっているので、特に難しい作業なく有効化できます。

Azure DNS の Alias record set を利用することで以前問題となった Subdomain Takeover を防ぐことが出来るのでお勧めなのですが、Front Door の場合は Managed Certificate の自動更新が Alias record set を利用していると行われないという制約があります。

具体的には以下のように CNAME で Front Door のエンドポイントを指している場合のみ、Managed Certificate の自動更新が行われるとドキュメントにも記載されています。

Apex domains don't have a CNAME record pointing to an Azure Front Door endpoint, so the auto-rotation for managed certificate fails until the domain ownership is revalidated.

Apex domains in Azure Front Door | Microsoft Learn

流石にドキュメントが古いだけで、今は改善されているだろうと思って調べたのですが、以下の Issue にもあるように現時点でも挙動は改善されておらず、実際に半年前に割り当てた Apex ドメインの証明書が切れるという事象も確認されています。

自動更新が出来ない場合には、証明書の期限が切れる 45 日前から再検証が行えるようになるらしいです。もちろん Azure Portal からの手動での作業が必要となるので、証明書の期限切れの危険性が高まりますので、何らかの手を打つ必要があります。

同じように Alias record set を利用することで Apex ドメインと証明書の発行まで出来る Static Web Apps ではこのような問題は発生しないので、Front Door 側でも改善できるだろうと思っていますが、暫くはこのままだと思うので Terraform でのある程度の自動化を検証しました。

Terraform を利用して Front Door に Apex ドメインを追加

まずは最低限必要となる Terraform を利用した Front Door 関連リソースの作成を行っておきます。少し Front Door 周りの定義は癖がありますが、用語を理解しておけば何とかなるはずです。個人的にはカスタムドメインの割り当て周りで少しはまりましたが、最終的には以下のような定義に落ち着きました。

ちなみに Azure DNS 周りも Terraform で管理するようにしていますが、実際は DNS Zone を含めるのは少し難しいと思うので、DNS レコードのみ Terraform で管理するのが良さそうです。

provider "azurerm" {
  features {}
}

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

resource "azurerm_resource_group" "example" {
  name     = "rg-apex-demo"
  location = "West Europe"
}

resource "azurerm_cdn_frontdoor_profile" "example" {
  name                = "afd-apex-demo"
  resource_group_name = azurerm_resource_group.example.name
  sku_name            = "Standard_AzureFrontDoor"
}

resource "azurerm_cdn_frontdoor_endpoint" "example" {
  name                     = "fde-apex-demo"
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.example.id
}

resource "azurerm_cdn_frontdoor_origin_group" "example" {
  name                     = "apex-demo-origin-group"
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.example.id

  load_balancing {}
}

resource "azurerm_cdn_frontdoor_origin" "example" {
  name                          = "apex-demo-origin"
  cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.example.id
  enabled                       = true

  certificate_name_check_enabled = true

  host_name          = "shibayan.jp"
  http_port          = 80
  https_port         = 443
  origin_host_header = "shibayan.jp"
  priority           = 1
  weight             = 1
}

resource "azurerm_cdn_frontdoor_route" "example" {
  name                          = "apex-demo-route"
  cdn_frontdoor_endpoint_id     = azurerm_cdn_frontdoor_endpoint.example.id
  cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.example.id
  cdn_frontdoor_origin_ids      = [azurerm_cdn_frontdoor_origin.example.id]
  enabled                       = true

  forwarding_protocol    = "HttpsOnly"
  https_redirect_enabled = true
  patterns_to_match      = ["/*"]
  supported_protocols    = ["Http", "Https"]

  cdn_frontdoor_custom_domain_ids = [azurerm_cdn_frontdoor_custom_domain.example.id]
  link_to_default_domain          = false
}

resource "azurerm_cdn_frontdoor_custom_domain" "example" {
  name                     = "apex-demo-custom-domain"
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.example.id
  dns_zone_id              = azurerm_dns_zone.example.id
  host_name                = "e5c4p3.cloud"

  tls {
    certificate_type    = "ManagedCertificate"
    minimum_tls_version = "TLS12"
  }
}

resource "azurerm_dns_zone" "example" {
  name                = "e5c4p3.cloud"
  resource_group_name = azurerm_resource_group.example.name
}

resource "azurerm_dns_a_record" "example" {
  name                = "@"
  zone_name           = azurerm_dns_zone.example.name
  resource_group_name = azurerm_resource_group.example.name
  ttl                 = 3600
  target_resource_id  = azurerm_cdn_frontdoor_endpoint.example.id
}

resource "azurerm_dns_txt_record" "example" {
  name                = "_dnsauth"
  zone_name           = azurerm_dns_zone.example.name
  resource_group_name = azurerm_resource_group.example.name
  ttl                 = 3600

  record {
    value = azurerm_cdn_frontdoor_custom_domain.example.validation_token
  }
}

Front Door は出てくるリソースが多いので複雑そうに見えますが、ルーティングやオリジンの定義が多いだけで、一つずつ分解してみていくと大したことはしていません。Azure DNS 周りも Alias record set の作成と、ドメイン認証に利用する TXT レコードの作成ぐらいなのでシンプルです。

この Terraform 定義を使って Azure 上にリソースを作成して、DNS のネームサーバーを変更してしばらく待てば Front Door のプロビジョニングが完了して、以下のようにアクセスできるようになります。

証明書も 180 日有効なものが発行されているので、もちろん HTTPS での接続も問題なく行えます。

これで Azure リソースの構築はすべて完了していますが、180 日後には証明書の自動更新が行われず HTTPS で接続が出来なくなるという問題が発生してしまうので、Terraform 内で再検証に必要な処理を実行して、証明書の自動更新が行われるようにします

実現には AzureRM Provider だけでは不可能なので、ARM を直接扱える AzAPI Provider を利用します。

AzAPI Provider を利用して再検証 Action を実行

先に Front Door のカスタムドメイン再検証について簡単に説明しておくと、ARM 上はドメイン検証用トークンを再発行して、再度検証フローを実行する API が用意されています。

この API を実行するとカスタムドメインの紐づいている検証トークンが再発行されるので、後は TXT レコードの値を新しく発行されたトークンで更新してあげるだけですが、AzureRM Provider には ARM でいうところの Action に相当する機能が提供されていません。

Action を実行するために AzAPI Provider に用意された azapi_resource_action を使います。

詳細はドキュメントがあるので深くは説明しませんが、AzAPI Provider は非常に薄い ARM REST API のラッパーという感じなので、この Provider だけで全てのリソースを構築するというのは厳しいです。AzureRM Provider に足りない機能を補うために、ピンポイントで利用するのが適しています。

実際に azapi_resource_action を利用して検証用トークンの再発行を行う定義が以下になります。ぱっと見は Bicep っぽさがありますが、ARM REST API を直接叩いているので仕方ない部分です。

resource "time_rotating" "validation_token" {
  rotation_days = 150
}

resource "time_static" "validation_token" {
  rfc3339 = time_rotating.validation_token.rfc3339
}

resource "azapi_resource_action" "refresh_validation_token" {
  type        = "Microsoft.Cdn/profiles/customDomains@2023-05-01"
  resource_id = azurerm_cdn_frontdoor_custom_domain.example.id
  action      = "refreshValidationToken"
  method      = "POST"

  lifecycle {
    replace_triggered_by = [ time_static.validation_token ]
  }
}

time_rotatingtime_static を組み合わせることで、一定時間の経過後に azapi_resource_action が再実行されるようにしています。すなわち、150 日経過した後に Terraform を実行すると検証用トークンが再発行されるということです。

上で作成した Front Door をプロビジョニングする Terraform 定義にこのリソースを追加すると、最初は azapi_resource_action が存在しないので以下のように検証用トークンを再発行しようとします。

Action のリソースを作成すると検証用トークンを再発行し、検証フローを再度開始するので Azure Portal 上は Validation State が Pending に変わります。

そしてもう一度 Terraform を実行すると azurerm_cdn_frontdoor_custom_domain に紐づいている検証トークンの値が変化していることが Terraform によって報告されます。本来なら Action の実行時の 1 回だけで完結させたいですが、検証トークンの再発行が非同期で行われるため困難です。

そのタイミングで報告された Terraform のプランでは Azure DNS の TXT レコードが更新対象だと報告されます。先の Action 実行によって検証トークンの値が変化しているので、Terraform がそれに自動的に追従しようとしてくれていることが確認できますね。

そのまま Terraform から TXT レコードの値を更新し、暫く待つと Azure Portal 上の Validation State が Pending から Approved に変化して、証明書も発行されていることが確認できます。

Azure Portal で手動実行するよりは明らかに Terraform 上で全てを定義した方が、単純にコマンドを実行するだけで自動的に更新されるので安全ですが、期限切れ前に Terraform を実行しないと意味がないです。

この問題を回避するためには Drift detection と同じような考え方で、定期的に terraform plan を実行する仕組みが必要になりそうです。*1

*1:Terraform Cloud で Drift detection を有効化しておくと検出されるかもしれません(未確認

Azure OpenAI Service に追加された Content filters (Preview) の実行結果を取得する

少し前から Azure OpenAI Service に Content filters (Preview) が追加されたからなのか、たまに Chat Completion を使っていると以下のようなメッセージが返ってくることがあります。

メッセージ内容から Content filters に引っかかっていることは一発でわかるのですが、何が原因で引っかかったのかは分からないので Chat Playground 上では対処が結構難しいです。

とはいえ、Content filters で弾かれる内容は以下のドキュメントにもあるように 4 項目に対してなので、そこから想像は付くのですが確証がない状態です。

せめて 4 項目のうちどれに引っかかって、どういった表現に問題があったのかもデバッグ中には教えてもらいたいものですが、残念ながら Chat Playground にはそういう機能はありません。

更に現時点では Content filters のカスタマイズでも、これ以上フィルタを緩くする方向には設定できないので、プロンプト側でフィルタに引っかからないように出力を制御するしかありません。

Content filters の実行結果は Chat Playground では確認出来なくても、REST API を直接叩けば取れるだろと思って Postman を使って適当に叩いてみたところ、以下のような情報量がほぼゼロのレスポンスしか返ってきて来なかったので若干絶望しました。

絶対に取れる方法が用意されていると思っていたので調べたところ、以下のドキュメントが見つかりました。適切に API Version を設定すれば、問題なく Content filters の結果は返ってくるようです。

今回サンプル URL を Chat Playground から取得したので、Content filters に対応していない API Version が指定されていましたが 2023-06-01-preview を指定すると、以下のように詳細な情報が返ってくるようになりました。このスクリーンショットでは応答についての結果が含まれていますが、実際にはプロンプトに対しての結果も返ってきています。

Content filters の結果が返ってくることが分かれば、後は SDK 経由で結果が取れるかの確認ですが、残念ながら C# 向けの SDK では現時点では古い API Version 向けに生成されていたので、詳細な情報を取得することは出来ませんでした。

しかし最近の Azure SDK は全て AutoRest を使った OpenAPI 定義からの自動生成となっているので、暫くすると 2023-06-01-preview に対応したバージョンがリリースされると思います。

他の言語の SDK で対応していることを期待して調べてみると、Azure SDK for Python のリポジトリに Content filters 周りのテストコードが存在していることに気が付きました。

Python SDK ではレスポンスの型を定義しておらず、割と自由に参照できるようになっていたので API Version だけ対応したものを指定してやれば、特に問題なく扱えることが分かりました。

Azure SDK for Python のリポジトリにテストコードはありましたが、SDK の実体は OpenAI が提供しているものなので、利用を開始する際には以下のリポジトリを確認しておきます。

適当に Codespaces を立ち上げて Python 環境を作成し、その上で Content filters の応答を取得するサンプルコードを書いてみました。このサンプルコードの肝は前述した通り openai.api_version2023-06-01-preview を指定している部分です。

import openai

openai.api_type = "azure"
openai.api_key = "..."
openai.api_base = "https://***.openai.azure.com/"
openai.api_version = "2023-06-01-preview"

# create a chat completion
completion = openai.ChatCompletion.create(deployment_id="gpt-4", model="gpt-4", messages=[{"role": "user", "content": "Hello world"}])

# print the completion
print(completion.choices[0].message.content)

# print content filter results
print(completion.choices[0].content_filter_results)
#print(completion.prompt_annotations[0].content_filter_results)

このサンプルコードを実行すると、以下のように応答に対する Content filters の実行結果が出力されます。コメントアウトしている行を戻すと、プロンプトに対する結果も表示されます。

これで Content filters のどの項目に引っかかって、応答がフィルタリングされてしまったのかを把握することが可能となりました。開発中だけは応答のどの部分に問題があったのかも教えてほしくはありますが、最低限の情報は取れているので対応は出来そうです。

ASP.NET (.NET Framework) 向けの各 Session State Provider が大規模アップデート

ASP.NET (.NET Framework) を利用したアプリケーションで必要になることが多い Session State Provider ですが、今年になってから大規模なアップデートが多いので一通り確認しておきました。

現状 ASP.NET 向け Session State Provider は Cosmos DB / SQL Server 向けと Redis 向けで、以下のように別々のリポジトリで開発されています。個人的には AspNetSessionState 側に統一してほしいです。

今回は全ての Session State Provider でアップデートがあったので、InProc と StateServer 以外を使っているアプリケーション全てに関係します。パフォーマンス面に影響のあるアップデートも含まれています。

共通のアップデート

Session State を実装している IIS Module 自体も 2.0.0 にアップデートされて、各 Session State Provider に共通した機能が増えています。ただし Redis に関してはアップデートが未だ行われていません。

アップデート内容を要約すると以下の 2 点になります。両方ともパフォーマンスに影響する更新です。

  • セッションの TTL 更新を行わないオプション skipKeepAliveWhenUnused が追加
    • Session State が必須になっている時だけ TTL を更新する
  • IIS Module の preConditionmanagedHandler が追加
    • これまでは静的ファイルなどでも Session State を通っていた

これまでの ASP.NET の Session State Module 実装では、Session State をページやコントローラー単位で明示的に無効化しても、セッションの TTL 更新のためにリクエストが必ず発行されていたのですが、新しく追加されたオプションではその挙動をオフに出来るようになりました。これにより大半でセッションが不要なアプリケーションで信頼性とパフォーマンスを改善することが可能です。

以前 Azure 上で ASP.NET の Session State を使うベストプラクティスについて書いたエントリで触れた部分が改善されています。設定の有効化前に読んでおいてもらえると参考になるかと思います。

IIS Module については破壊的変更が存在していないので、Redis を使っている場合でも明示的にインストールすると新機能が使えるはずなのですが、手元で試した限りでは確認できませんでした。

SQL Server 向け

主にオンプレ向けで利用されていると思われる SQL Server 向けの Session State Provider は、大幅なアップデートを含む形で 2.0.0 が公開されました。アップデート内容的にも更新は必須です。

アップデート内容は大体以下の通りになります。アップデート内容は多いですが、一番大きな点は使用している SDK が Microsoft.Data.SqlClient に更新されたことです。これにより信頼性とパフォーマンス改善だけではなく、Managed Identity への対応も容易になりました。

注意点としては既に SQL Server を Session State Provider として使っている場合には、互換性を保つために repositoryTypeFrameworkCompat を指定する必要がある点です。デフォルト値が FrameworkCompat にはなっていますが、明示的に指定した方が安全です。

デフォルトで参照されている Microsoft.Data.SqlClient は最新ではないので、必要に応じて明示的にインストールしてバージョンアップしておくと安心です。

Managed Identity を利用する際には接続文字列を変えるだけで問題ないので、以下のエントリを参照してください。古いクライアントと比べると Microsoft.Data.SqlClient では劇的に簡単になっているので、SQL Server での Managed Identity 利用を推し進めていきたいですね。

Microsoft Account では利用できないですが、大体のケースでは組織アカウントを使っているはずなので、ローカル開発時でも問題となることは少ないと思います。

Cosmos DB 向け

Azure 上での利用が多いはずの Cosmos DB を利用した Session State Provider についても、大幅にアップデートされた 2.0.0 が公開されています。こちらも更新は必須と言えます。

今回のアップデートではこれまでの謎設定が綺麗に一掃されて、デフォルトで Cosmos DB のパフォーマンスを最大限利用できるようになりました。特にパーティションキー周りの設定については、これまでの Provider では完全に無駄な実装がありました。

  • パーティションキーが /id 固定に変更
    • partitionKeyPathpartitionNumUsedByProvider の廃止
  • 使用している SDK が Microsoft.Azure.Cosmos に変更
  • プロパティの命名が collectionId => containerId に変更
  • consistencyLevel の追加
  • connectionProtocol の廃止

中でも一番大きいアップデートは SDK が Cosmos DB SDK の v3 ベースになっていることです。これまでの Provider は廃止が予定されている v2 ベースでしたので、来年にはサポートされなくなるところでした。

ただし、こちらも SQL Server の Provider と同様に SDK バージョンが既に古くなっているので、明示的にパッケージをインストールしておくのをお勧めしています。

特に Cosmos DB SDK については推奨される最小バージョンが公開されていて、それが現在 3.33.0 となっているので SDK の明示的なアップデートは必須と言えます。

今回から Session を保持する Container のパーティションキーは /id 固定となっているので、現在使用している Container のパーティションキーをカスタマイズしてしまっている場合には、既存セッションは失われてしまいますが作成しなおしてしまうのがお勧めです。

Redis 向け

Cosmos DB や SQL Server とは開発チーム自体が別のようで、微妙に Session State Module のアップデートには追従できていないようなのですが、今年になってから 5.0.0 が公開されています。

破壊的変更が含まれていますが、アップデート内容としては少なめです。要約すると他の Session State Provider の実装に近付けられたという感じです。

例によって、こちらもベースとなっている StackExchange.Redis のバージョンが古くなっているので、明示的にアップデートしておくのがお勧めです。

Redis 向けについては Azure チームが開発していて、それ以外は ASP.NET チームが開発しているようなのですが、どこかで AspNetSessionState リポジトリ側に開発がマージされて欲しいですね。