しばやん雑記

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

Azure Functions の Node.js v4 に追加された HTTP Streaming サポートを深掘りする

Azure Functions の Node.js や Python といった言語ワーカーが動作するタイプでは、これまで HTTP のリクエストとレスポンスを Stream として扱うことが出来なかったので、大容量ファイルのダウンロードやアップロードが効率的に行えないという制約がありました。

この制約は In-Process で動作する C# のみ適用されず、同じ C# でも .NET Isolated を使っている場合は Node.js や Python と同様の制約を受けていましたが、ASP.NET Core Integration の追加によって解消されました。仕組みとしては HTTP だけは Azure Functions Runtime が裏側の言語ワーカー対して、HTTP リクエストをそのままフォワーディングすることで実現しています。

以前の仕組みでは HTTP も一旦は gRPC に変換していたので、どう頑張っても Stream として扱うことが出来ないという事情がありました。ちなみに HTTP フォワーディングには YARP が使われているので信頼性とパフォーマンスの面でも安心です。

この新しい仕組みによって Azure Functions の言語ワーカーでも効率的に HTTP が扱えるようになりましたが、この度 Node.js でも同じ仕組みがサポートされるようになったという話です。現在はプレビュー中ですが、仕組みとしてはシンプルなので GA もそう遠くないでしょう。

C# では ASP.NET Core Integration という形で導入されたので、HTTP リクエストとレスポンスを表す型が変更され移行が必要になりましたが、Node.js の HTTP Streaming サポートでは Node.js の Stream への対応に必要な最小限の変更だけが行われたので、オプションを有効化して Stream を返すだけで使えます。

早速複数のシナリオで試していくわけですが、まず HTTP Streaming を有効化するには app.setup を呼び出して enableHttpStreamtrue に設定する必要があります。

import { app } from "@azure/functions";

app.setup({ enableHttpStream: true });

ちなみに 1 回だけ呼び出せばよいので setup.ts のようなファイルを作成して、そこに上のコードを追加することで対応しました。これでグローバルに有効化されます。

ここから先に紹介するサンプルコードの注意点として、私自身が Node.js の Stream API に詳しいわけではないので効率の悪い実装になっている可能性はあります。

Blob を Streaming でダウンロード・アップロードする

まずはよくありそうなシナリオとして 1,2 位を争いそうな、Azure Storage の Blob に対して Stream でのダウンロードとアップロードを行ってみます。Blob SDK 自体は Node.js の Stream に対応しているので、HTTP Streaming を組み合わせると効率よく扱えるはずです。

用意したサンプルコードは以下のようになりました。HttpResponseInitbody に直接 readableStreamBody を渡すだけで良いと思っていましたが、TypeScript の型エラーが出てしまったので Readable.from を使って Readable に変換することで対応しました。

正直なところ Readable / ReadStream / ReadableStream の違いはよく分かっていません。

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
import { BlobServiceClient } from "@azure/storage-blob";
import { Readable } from "stream";

const blobServiceClient = BlobServiceClient.fromConnectionString(process.env.AZURE_STORAGE_CONNECTION_STRING);

export async function httpTrigger1(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    context.log(`Http function processed request for url "${request.url}"`);

    const containerClient = blobServiceClient.getContainerClient('streaming');
    const blobClient = containerClient.getBlockBlobClient('download.jpg');

    const response = await blobClient.download();
    const stream = Readable.from(response.readableStreamBody);

    return {
        body: stream,
        headers: { 'Content-Type': 'image/jpg' }
    };
};

app.http('httpTrigger1', {
    methods: ['GET'],
    authLevel: 'anonymous',
    handler: httpTrigger1
});

これまでのように arrayBufferblob として一度メモリに読み込んでから返すのと違い、 Stream はすぐに応答を返し始めるので TTFB が短縮されます。更にメモリに全データを読み込まないので、消費するメモリ量は非常に少なくなります。

次はアップロードを行っていきますが、Blob SDK では Stream 用の API が用意されているので、こちらもダウンロードと同様にサクッと対応可能です。

現在の HTTP Streaming 実装はリクエストのペイロードしか対応していないので、マルチパートで送信されたファイルでは HTTP Streaming が使えない点に注意が必要です。アップロードでも uploadStream に直接 request.body を渡すことが出来なかったので、一度 Readable.from を経由して解決しています。

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
import { BlobServiceClient } from "@azure/storage-blob";
import { Readable } from "stream";

const blobServiceClient = BlobServiceClient.fromConnectionString(process.env.AZURE_STORAGE_CONNECTION_STRING);

export async function httpTrigger2(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    context.log(`Http function processed request for url "${request.url}"`);

    const containerClient = blobServiceClient.getContainerClient("streaming");
    const blobClient = containerClient.getBlockBlobClient("upload.jpg");

    await blobClient.uploadStream(Readable.from(request.body));

    return { body: 'Done' };
};

app.http('httpTrigger2', {
    methods: ['POST'],
    authLevel: 'anonymous',
    handler: httpTrigger2
});

ダウンロードと同様にアップロードされたファイルが Stream で扱われるので、実行中の消費メモリ量が非常に少なく済むことを確認出来ました。パフォーマンスとスケーラビリティに大きく影響する部分なので、今後は可能な限り Stream を使っていきたいです。

Server-Sent Events を実装する

HTTP Streaming をサポートしたということは、ファイルを効率よく扱うだけではなく、Azure Functions でも Server-Sent Events が実装できるようになったことを意味します。公式ブログでも Azure OpenAI の Streaming が用途として挙げられていたように、処理中に順次応答を返すことが出来るようになりました。

WebSocket でも同じことは実現出来ますが、Azure Functions Runtime では有効化されていないようです。YARP 自体は対応しているので実装の都合という感じはあります。

しかしサーバーからクライアントに順次データを返すだけであれば Server-Sent Events が一番簡単です。

応答として Node.js の Stream を返せばよいだけなので、実現方法はいくつも考えられると思いますが、以下のサンプルコードでは Generator を作成して Readable.from で変換するようにして対応しました。

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
import { Readable } from "stream";

const sleep = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout))

export async function httpTrigger3(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    context.log(`Http function processed request for url "${request.url}"`);

    const generator = async function* (count: number) {
        for (var i = 0; i < count; i++) {
            yield `event: ping\ndata: ${new Date().toISOString()}\n\n`
            await sleep(1000)
        }
    };

    const stream = Readable.from(generator(10));

    return {
        body: stream,
        headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-store'
        }
    };
};

app.http('httpTrigger3', {
    methods: ['GET'],
    authLevel: 'anonymous',
    handler: httpTrigger3
});

このサンプルコードを EventSource を使って動かすと、以下のようにイベントが一定間隔でサーバーから送信されていることが確認出来ます。Generator が完了すると接続が切れて再接続が走るため、エラー発生時には明示的に close するコードを入れています。

C# で言うところの CancellationToken に相当するものがなさそうなので、現時点の実装では SSE の送信中に接続が切れたことを判別する手段が無さそうです。なのでかなり長い期間接続を保持して、ダラダラとイベントを送信するようなケースでは対応が難しそうです。

Azure OpenAI の Streaming API と組み合わせる

ここまでの確認で準備が整っているので、最後は Azure OpenAI の Streaming API を使って Chat Completions の実行結果を Streaming でクライアントに返す実装を試してみます。Azure OpenAI のドキュメントでも最適化として Streaming が推奨されているので、Azure Functions でサクッと実装できると嬉しい部分です。

コードとしては Server-Sent Events を実装した時とほぼ同じで、違いとしては Generator の実装が EventStream<ChatCompletions> を引数で受け取って for await で回すようにしているだけです。

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
import { OpenAIClient, AzureKeyCredential, ChatCompletions, EventStream } from "@azure/openai";
import { Readable } from "stream";

export async function httpTrigger4(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    context.log(`Http function processed request for url "${request.url}"`);

    const client = new OpenAIClient(process.env.AZURE_OPENAI_ENDPOINT, new AzureKeyCredential(process.env.AZURE_OPENAI_KEY))

    const events = await client.streamChatCompletions(
        'gpt-4',
        [
            { role: 'system', content: 'You are an AI assistant that helps people find information.' },
            { role: 'user', content: 'Azure Functions について分かりやすく説明してください' }
        ],
        { maxTokens: 1024 }
    );

    const generator = async function* (events: EventStream<ChatCompletions>) {
        for await (const event of events) {
            for (const choice of event.choices) {
                if (choice.delta?.content === undefined) {
                    continue;
                }
                console.log(choice.delta?.content);
                yield `event: text\ndata: ${choice.delta?.content}\n\n`
            }
        }
    };

    const stream = Readable.from(generator(events));

    return {
        body: stream,
        headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-store'
        }
    };
};

app.http('httpTrigger4', {
    methods: ['GET'],
    authLevel: 'anonymous',
    handler: httpTrigger4
});

このコードを実行してみると、応答が開始されるまでは少し時間がかかりますが、その後は Server-Sent Events として GPT-4 の応答が Streaming で流れていることが確認出来ます。Azure Functions をベースとしているので必要なコードが少なく実現出来ているのは大きなメリットです。

特に Azure OpenAI を利用したアプリケーションは多くの API が必要となるケースが少ないため、Azure Functions の HttpTrigger を使ってサクッと用意するのが手っ取り早いです。

今回は Azure にデプロイまでは試していませんが、ほぼ全てのリージョンで Azure Functions Runtime の 4.30.0 以上がデプロイ済みなのですんなりと動作するはずです。あまり長い接続を作ろうとすると Azure Load Balancer のタイムアウトに引っかかる可能性はあるので、その辺りは注意が必要になります。

ちなみに Static Web Apps の Managed Functions にも 4.30.0 以上がデプロイされているはずなので、SWA でも利用できる可能性があります。Azure OpenAI と SWA の相性は良いと思っているので、組み合わせて使っていきたいです。

独自のバックエンドとリンクした Azure Static Web Apps のプレビュー環境を自動で作成する

Azure Static Web Apps で何も考えずに GitHub で Pull Request を作成すると、標準で提供されているプレビュー環境が自動的に作成されるので便利です。Managed Functions も SWA とセットでデプロイされるので、フロントエンドだけではなくバックエンドも含めてプレビュー出来るのが特徴です。

簡単なアプリケーションであれば Managed Functions で十分なケースも多いはずですが、Web API の数が多い場合には Azure Functions ではなく App Service や API Management を API リンクで利用するケースがあります。SWA の Standard Plan が必要になりますが、App Service / API Management / Container Apps などをバックエンドに利用できるので SWA の利用範囲が広がります。

SWA へのバックエンドのリンク機能については以下のエントリでまとめていますので、使ったことがない方はこちらも参照してください。

しかし、バックエンドに独自の App Service などで実装した API をリンクすると、SWA のプレビュー環境がフロントエンド側でしか動作しなくなるため、実質使い物にならなくなります。ドキュメントにもバックエンドのリンクを利用すると Pull Request でのプレビュー環境が使えなくなると明記されています。

Backend integration is not supported on Static Web Apps pull request environments.

API support in Azure Static Web Apps with Azure App Service | Microsoft Learn

こればかりはバックエンドがどのようにホストされているかに依存するので、SWA 側ではどうしようもない部分です。SWA で独自のバックエンドを利用する場合の制約となるのですが、非常に便利な機能なのでやはり活用したいです。

ドキュメントには Pull Request のプレビュー環境はバックエンドのリンクを利用すると使えないと書いてありますが、実は名前付きプレビュー環境ではバックエンドのリンクが利用できるので、それを利用して構築を行なっていきます。

App Service や Azure Functions には Deployment Slot が用意されているので、SWA 側のプレビュー環境と App Service の Deployment Slot を一対一に紐づけると綺麗に解決できます。以前に App Service で Pull Request プレビュー環境を作成する方法を書いているので、こちらも参考にしてください。

組み込みでのサポートは用意されていないので、基本的に GitHub Actions と Azure CLI を利用して実現していきますが、以下の 3 ステップで済むため意外に簡単です。

  1. API デプロイ用の Web Apps の Deployment Slot を作成
  2. Static Web Apps のプレビュー環境を作成
  3. Static Web Apps の API リンクで Web Apps を関連付け

SWA 側で App Service をリンクする際には Deployment Slot 名を指定できるので、そういう意味では想定された使い方かもしれません。この方針で Azure CLI を使ってプレビュー環境を構築していきます。

Web Apps でプレビュー環境を作成

まずは Web Apps に対して新しい Deployment Slot を作成して、そこにプレビュー表示したいブランチのバックエンドをデプロイします。デプロイの処理自体は変わらないので省略します。

Azure CLI を使って Deployment Slot を作成するには az webapp deployment slot create を使います。

Deployment Slot を作成するときのオプションとして、コピー元の Slot の指定があるので常に Production Slot の設定をコピーして作るようにします。引数としては --configuration-source が該当しますが、Production Slot を指定する際には Web App 名を指定します。

az webapp deployment slot create -n ${{ env.BACKEND_APP_NAME }} -g ${{ env.RESOURCE_GROUP_NAME }} -s ${{ env.PREVIEW_ENV_NAME }} --configuration-source ${{ env.BACKEND_APP_NAME }}

App Service 名などは他の部分でも必要となるので環境変数して追加しておくと楽です。このコマンドが成功すると新しい名前で Deployment Slot が作成されます。

ちなみにこのコマンドは既に同名の Deployment Slot が存在している場合でも実行でき、更にその時点でのコピー元の最新の設定を適用してくれるので、多少実行に時間はかかりますがアプリケーションデプロイの度に毎回呼び出したほうが安全です。

Static Web Apps でプレビュー環境を作成

次は Static Web Apps 側のプレビュー環境を作成します。SWA のプレビュー環境は Azure CLI でも作成可能ですが、アプリケーションデプロイの際になければ自動で作ってくれるのでそれを利用します。

ただし公式の SWA デプロイ Action を使うと Pull Request 上の場合に特殊処理が走ってしまうので、デプロイには SWA CLI を使うようにして対応します。SWA CLI を使ったデプロイ方法は以下のエントリで紹介しているので、こちらも参照してください。

SWA CLI を使ったデプロイの際にプレビュー環境名を指定すれば、名前付きプレビュー環境が作成されます。App Service のように Deployment Slot とデプロイを個別に行う必要がないためシンプルです。

swa deploy ./dist -n ${{ env.FRONTEND_APP_NAME}} --env ${{ env.PREVIEW_ENV_NAME}} --no-use-keychain

これで SWA 側に指定した名前でプレビュー環境が作成されます。この方法で作成したプレビュー環境には独自のバックエンドをリンク出来るので、後は Azure CLI を使ってリンクを行うだけとなります。

バックエンドのリンク

最後は作成した Web App と Static Web App をリンクするのですが、これも Azure CLI を使って行えるので GitHub Actions に載せることが容易です。ただし渡すパラメータに癖があるので注意が必要です。

Azure CLI で SWA へのバックエンドのリンクを行う機会は正直ほぼ無いと思いますし、そもそも SWA を Azure CLI で管理する機会もほぼ無いとは思うので az staticwebapp というコマンド自体があまり知られていない気がしますが、意外に多くのコマンドが用意されています。

バックエンドのリンク自体はコマンド 1 つで終わるのですが、コマンドのパラメータとしてリンク先のリソース ID とリージョン名が必要となるのが最大のネックです。リソース ID はともかく、リージョン名が必要になるのは謎です。

バックエンドのリソース ID とリージョン名を取得するために、今回は az webapp show コマンドを使うことにして、以下のような処理でバックエンドのリンクを実現しました。

read -d "" backendId location <<< $(az webapp show -n ${{ env.BACKEND_APP_NAME }} -g ${{ env.RESOURCE_GROUP_NAME }} -s ${{ env.PREVIEW_ENV_NAME }} --query "[id, location]" -o tsv) || true
az staticwebapp backends link -n ${{ env.FRONTEND_APP_NAME }} -g ${{ env.RESOURCE_GROUP_NAME }} --environment-name ${{ env.PREVIEW_ENV_NAME }} --backend-resource-id "$backendId" --backend-region "$location"

read -d を使った呪文のようなコマンドは、az webapp show が返した複数の値を変数に格納するために使っています。以下のドキュメントで紹介されていた処理を流用しているだけですが、今回のように複数の値が必要な場合に時間のかかる ARM REST API の呼び出しを最小限にできます。

これでプレビュー環境同士をリンク出来ました。Azure Portal で確認するとプレビュー環境にバックエンドがリンクされているのが確認出来ます。

必要なくなったプレビュー環境を削除

プレビュー環境の作成はここまでの処理で完成したので、最後に Pull Request が閉じられた際にプレビュー環境も削除する処理を追加する必要があります。削除は非常にシンプルで Web App と Static Web App のそれぞれで削除コマンドを実行するだけです。

厳密にはどちらのコマンドが失敗した場合のリカバリについても考慮する必要がありますが、削除が失敗することは可能性としては低いため以下のように単純なコマンドで実現します。

az webapp deployment slot delete -n ${{ env.BACKEND_APP_NAME }} -g ${{ env.RESOURCE_GROUP_NAME }} -s ${{ env.PREVIEW_ENV_NAME }}
az staticwebapp environment delete -n ${{ env.FRONTEND_APP_NAME }} -g ${{ env.RESOURCE_GROUP_NAME }} --environment-name ${{ env.PREVIEW_ENV_NAME }} --yes

このコマンドを Pull Request が閉じられた時に実行するようにすると、プレビュー環境と Pull Requst のライフサイクルを一致させることができます。

実際の動作例

ここまでのプレビュー環境の自動作成とデプロイを組み合わせたサンプルリポジトリを作成して、実際に動作確認を行いましたが Pull Request を作成すると、自動でバックエンド付きのプレビュー環境が作成され、素早く動作確認が行えるようになりました。

作成したサンプルでは GitHub の Environment も利用しているので、以下のように Pull Request からプレビュー用の URL に素早く飛べるようになっています。

実際にバックエンドに新しい API を追加して、プレビュー環境上で動作確認を行いましたが問題なく動作しました。実際のプロダクト開発にこの仕組みを実装して、しばらく運用していますが非常にスムーズにレビューが行えるのでかなり便利に使っています。

プレビュー環境で問題となりがちな認証については、今回は Entra ID のワイルドカードを利用してプレビュー環境に対しても本番と同等の Entra ID 認証を追加することで綺麗に解決することが出来ています。

Entra ID 認証がワイルドカードで利用出来ることが確認できたのが、今回の独自のバックエンドをリンクした SWA でプレビュー環境を構築するモチベーションになっています。

実際のプロダクト開発に組み込んだ話を含めて、どこかで喋る機会があれば共有したいなと思っています。

Azure Static Web Apps のデプロイに Static Web Apps CLI を利用する

Azure Static Web Apps へアプリケーションをデプロイする際には GitHub Actions を使うことが多いはずですが、公式で用意されている Action はビルドまで行うように設計されているため、ビルドは自前で行ってデプロイだけを行いたいケースでは若干使いにくいです。

それ以外のケースでも、ちょっとした検証で GitHub からデプロイするように構成するのは非常に面倒なので、最近は Static Web Apps CLI を使ってデプロイをすることが多くなりました。

Static Web Apps CLI はローカルでの開発をサポートする機能だけではなく、そのまま Azure にデプロイする機能まで備えているため扱いやすいものとなっています。最近は個人で開発しているアプリで Static Web Apps へのデプロイは、あえて GitHub Actions 上で Static Web Apps CLI を使うようにしています。

公式の Action ではデプロイの認証情報として Deployment Token のみ利用可能ですが、Static Web Apps CLI では Deployment Token だけではなく Service Principal も利用可能になっているため、他の Azure リソースへのデプロイと認証情報を共有できるメリットもあります。

単純に Static Web Apps CLI のデプロイと言ってもパターンがあるので簡単にまとめておきます。

Deployment Token を利用したデプロイ

一番シンプルなデプロイ方法は Deployment Token を利用するものです。SWA CLI では --deployment-token パラメータか SWA_CLI_DEPLOYMENT_TOKEN 環境変数を使って渡すようになっています。Deployment Token の値でどの Static Web Apps にデプロイするか特定されるため、名前を指定する必要がありません。

swa deploy ./dist --env production --deployment-token ***

Azure Portal から GitHub を選択すると Deployment Token は自動で Secret に登録されますが、SWA CLI を使う場合は Azure Portal から Deployment Token をコピーする必要があります。

ローカルでの swa deploy コマンドの実行時に毎回 Deployment Token を指定するのは手間なので、環境変数に登録してから実行する方法が扱いやすいです。

対話型ログインを利用したデプロイ

Deployment Token を利用したデプロイはシンプルで扱いやすいですが、Deployment Token 自体の管理が非常に手間かつリスクになります。SWA CLI は Azure にログインして Deployment Token を管理する必要のないデプロイに対応しているため、更に扱いやすいものです。

最低限 swa deploy に渡す必要があるのは Static Web Apps の名前だけです。

swa deploy ./dist --env production --app-name ***

後は必要に応じて SWA CLI が Azure へのログインを行い、テナントやサブスクリプションの選択まで促してくれます。swa deploy に渡すパラメータで明示的にテナントとサブスクリプションを指定することも出来ますが、以下のように対話型で行った方が便利です。

最初にログインとサブスクリプションの選択を行うと、その情報は .env ファイルに保存されるため、次からはサブスクリプションを選択する必要なくデプロイが行えます。

Service Principal を利用したデプロイ

公式の Action でも要望が多く寄せられていた Service Principal を利用したデプロイも、SWA CLI は標準で対応しているので Static Web Apps のデプロイとバックエンドの App Service などのデプロイを同じ認証情報で行えるようになっています。

Service Principal を使う場合は swa deploy に渡すパラメータは一番多くなり、Client Secret の管理が少しネックになりますが、他と認証情報を共有できる強みは大きいです。

swa deploy ./dist --env production --app-name *** --tenant-id *** --subscription-id *** --client-id *** --client-secret ***

Deployment Token の時と同様に Service Principal の値は以下の環境変数を経由して渡すことも出来ます。

  • AZURE_TENANT_ID
  • AZURE_SUBSCRIPTION_ID
  • AZURE_CLIENT_ID
  • AZURE_CLIENT_SECRET

この環境変数は SWA CLI が内部で使っている Azure Identity が要求するものとなっています。なので他のアプリケーションでも使われている可能性があるのでバッティングには注意が必要です。

環境変数を設定するとデプロイは Static Web Apps の名前だけで済むようになるので楽です。

swa deploy ./dist --env production --app-name ***

Service Principal を使ったデプロイでは Static Web Apps への Contributor 権限が必要になります。App Service のようにリソース固有のロールは用意されていないので、強めの権限を与える必要があります。

GitHub Actions で SWA CLI を使ったデプロイフローを組む

Static Web Apps CLI を利用するとデプロイのみを簡単に実行できるので、GitHub Actions でのデプロイ時に利用するとビルドとデプロイを分離できるため、Oryx に依存しないアプリケーションに最適な方法でビルドを行えるようになります。

以下は Deployment Token を利用したシンプルなデプロイフローの例です。SWA CLI をインストールする手間は発生しますが、ビルドとデプロイを分離することで見通しも良くなっています。

name: Azure Static Web Apps CI/CD

on:
  push:
    branches: [ master ]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Use Node.js 20
      uses: actions/setup-node@v4
      with:
        node-version: 20

    - name: npm ci and build
      run: |
        npm ci --prefix frontend
        npm run build --prefix frontend

    - name: Deploy to Static Web App
      run: |
        npm i -g @azure/static-web-apps-cli
        swa deploy ./frontend/dist/ --env production --deployment-token ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}

実際に運用しているアプリケーションでも SWA CLI を使ったデプロイを既に組んであるので、こちらのワークフローも参考にしてください。それぞれの処理を分けておくとデプロイに失敗した場合に、失敗した処理だけリトライ出来るため効率的です。

そして Service Principal を利用したデプロイフローのサンプルは以下の通りになります。基本的には Deployment Token を利用する場合と同じですが、swa deploy に渡すパラメータを減らすため環境変数に Secrets の値を設定している点が異なっています。

name: Azure Static Web Apps CI/CD

on:
  push:
    branches: [ master ]

env:
  AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
  AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
  AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
  AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Use Node.js 20
      uses: actions/setup-node@v4
      with:
        node-version: 20

    - name: npm ci and build
      run: |
        npm ci --prefix frontend
        npm run build --prefix frontend

    - name: Deploy to Static Web App
      run: |
        npm i -g @azure/static-web-apps-cli
        swa deploy ./frontend/dist/ -n <SWA NAME> --env production --no-use-keychain

これまで説明していない --no-use-keychain についてですが、GitHub Actions 上ではキーチェーンが使えず問題となるため指定しています。

実際にデプロイフローを動かしてみると、以下のように Service Principal を使って Azure にログインを行い、Static Web Apps へのデプロイまで完了していることが確認出来ます。

Service Principal は権限がある Static Web Apps であれば自由にデプロイできるので、複数の Static Web Apps へのデプロイも 1 つの認証情報で実現出来ます。これは管理面からみると大きなメリットとなります。

Federated Credentials (OIDC) には未対応

既に GitHub Actions を使って Azure へのデプロイを組んだことのある人なら、絶対に Federated Credentials (OIDC) を使いたくなると思うのですが、残念ながら現時点の SWA CLI では対応していないため Client Secret の管理から逃げられません。

Federated Credentials については今回は説明しませんので、以下のエントリを参照してください。

SWA CLI は内部的に Azure Identity ライブラリを使用している関係上、Azure CLI から認証情報を取得出来れば綺麗に解決可能です。現在は Federated Credentials への対応のため Pull Request を作成してレビューを待っている状態です。

この Pull Request がマージされると、ローカルや GitHub Actions 上で Azure CLI が持っている認証情報でデプロイ可能となるため、より安全なデプロイフローを実現できます。

既に GitHub に関連付いたものを SWA CLI に変更する

新規に Static Web Apps を作成した時に GitHub を関連付けて作成した場合は、SWA CLI からデプロイしようとするとエラーとなるケースがあります。解消するには Azure CLI を使って az staticwebapp disconnect を実行して関連付けを解除する必要があります。

Azure CLI を利用すると再び GitHub 関連付けることも可能なはずですが、試したところ謎のエラーが出て動作しませんでした。一応 az staticwebapp update を使うと GitHub の設定以外は行えるので、後は手動で対応する形になりそうです。

Azure Static Web Apps のプレビュー環境で Microsoft Entra ID 認証を有効化する

Azure Static Web Apps のプレビュー環境は、GitHub を利用した開発中に Pull Request を作成したタイミングで自動的にデプロイされるので、変更点の動作確認やレビューが行いやすくなる便利な機能なのですが、カスタム認証を使ったアプリケーションでは利用が難しいものでした。

カスタム認証を使うとプレビュー環境を使うのが難しい理由は、URL が別で発行されるので OpenID Connect のようにコールバック URL を登録する必要がある場合に、何らかの方法で登録する必要があるためです。

以下のドキュメントでも Microsoft Entra ID を使った認証では、/.auth/login/aad/callback というパスの絶対 URL を Entra ID 側に登録する必要があると記載されています。

実際に Entra ID でのログインを Static Web Apps に組み込む際には、Entra ID アプリケーション側で以下のように Static Web Apps へのコールバック URL を登録する必要があります。

Static Web Apps の本番向け URL はリソースを再作成しない限り固定されるので、シンプルに設定できます。

本番向けだけであれば、後は staticwebapp.config.json に必要な設定を追加すれば、Entra ID を使ったログインを実装出来ます。画像だと説明が難しいですが、Entra ID を使ったログインを問題なく行えています。

この状態で Pull Request などをトリガーに作成されるプレビュー環境を使って試してみます。プレビュー環境の URL は Pull Request の場合は ID とデプロイ時に指定したリージョン名が追加で含まれます。

本来なら GitHub と連携させてデプロイをさせることでプレビュー環境を作るのですが、動作検証中に毎回 GitHub と連携するのは手間なので Static Web Apps CLI を使ってローカルからデプロイして検証しました。

この辺りは別途ブログに書こうと思っていますが、Static Web Apps CLI には swa deploy コマンドが用意されていて、簡単に任意のディレクトリをデプロイできるため非常に便利です。

Azure Portal から Deployment Token をコピーしてくれば、以下のようなコマンドでサクッとデプロイ可能です。--env を指定することで任意の名前のプレビュー環境へのデプロイも可能です。

swa deploy ./dist --env staging --deployment-token ***

これで作成したプレビュー環境に対してアクセスを行ってみると、Entra ID 側に登録されているコールバック URL が一致しないというエラーが発生して、ログインに失敗することが確認出来ます。このコールバック URL は Entra ID アプリケーション側に登録をしていないので、エラーが発生するのは当然です。

Static Web Apps がリリースされた当初から URL が Pull Request 毎に変わる結果、Entra ID のように外部に URL を登録するサービスが使いにくいと言われていて、その対応策としてプレビュー環境の拡張としてブランチ環境や名前付き環境が導入されました。

詳細は以下のドキュメントを参照していただくとして、このブランチ環境や名前付き環境を使うと、URL の固定化が可能となるためコールバック URL を登録するのは容易になります。

これでステージング環境のように予め決められたプレビュー環境であれば、Entra ID 認証を有効化出来るようになりましたが、やはり Pull Request 単位でのプレビュー環境の需要は多いので実現したいです。SPA のレビューの度にローカルで実行するのは正直厳しいです。

今回の場合は Entra ID アプリケーションのリダイレクト URL を可変に出来れば解決するので調べると、なんと Entra ID アプリケーションのリダイレクト URL は条件付きながらワイルドカードが利用できることが分かりました。これは完全に求めていた機能でした。

サブドメイン部分をワイルドカードで登録できるので、今回のプレビュー環境であれば問題なく利用できるはずです。但しドキュメントにもあるようにワイルドカードは推奨されておらず、絶対 URL が強く推奨されている点は頭に入れておく必要があります。

Though it's possible to set a redirect URI with a wildcard by using the manifest editor, we strongly recommend you adhere to section 3.1.2 of RFC 6749. and use only absolute URIs.

Redirect URI (reply URL) restrictions - Microsoft identity platform | Microsoft Learn

そしてワイルドカードが利用できるのは組織向けのアプリケーションだけとなっているため、ユースケースとしては今回のように開発中に利用することが想定されていると考えられます。

全く推奨されてはいない関係上、通常の UI ではワイルドカードを含む URL は登録出来ません。ドキュメントにもあるようにマニフェストを直接編集することで対応します。

注意点としてはワイルドカードはサブドメインに対して指定できるため、挙動としてはワイルドカード証明書と同じ仕様になっているようです。つまり https://*.example.com は指定できますが https://test-*.example.com のような指定は出来ませんし、ワイルドカードの対象となるのはその階層だけです。

従って Static Web Apps のプレビュー環境向けにワイルドカード設定を追加する際には、以下のようにプレビュー環境向けのコールバック URL を追加する形で対応します。

これで本番向けとプレビュー環境向けの両方に対応した Entra ID アプリケーションの設定が出来ました。

早速、先ほどはコールバック URL のミスマッチエラーが発生したプレビュー環境にアクセスすると、今度は問題なく Entra ID 認証に成功してページが表示されることが確認出来ました。

GitHub Actions を利用したデプロイの設定を行って、実際に Pull Request を作成したタイミングで作成されるプレビュー環境にアクセスしてみましたが、こちらも問題なく Entra ID 認証が行われてページが表示できています。ワイルドカードなので Pull Request が複数作られても問題なく動作します。

これで Managed Functions を利用した Static Web Apps 向けのアプリケーションであれば、問題なく Entra ID 認証付きで Pull Request の変更内容をプレビュー出来るようになりました。

最後に本題とは関係ないですが、GitHub Actions で Static Web Apps に Pull Request のプレビュー環境を作成した際に、完了のコメントが書き込まれないケースがあるようです。これは GitHub Actions の権限のデフォルト値が変更され、Pull Request への書き込みの権限が外れたことが原因です。

以下の Issue にある通り、permissions で Pull Request への書き込み権限を付与すると解決します。

自動生成された GitHub Actions の Workflow ファイルには permissions が定義されていないようなので、デフォルト値が変更されたことを知らないとはまりそうです。

Azure Functions の Python V2 と Node.js V4 プログラミングモデルを利用する上での注意点

去年に GA してから時間が経過しましたが、Azure Functions の新しいプログラミングモデル Python V2 と Node.js V4 を使う機会が増えてきて、以前書いたエントリからの更新点も増えたので簡単にまとめます。

プレビュー中に書いたエントリは以下になりますが、基本的な部分は変わっていないはずです。

新しいプログラミングモデルは非常に扱いやすく、これまでのように function.json を手動で書く必要がないのでミスを減らすことが出来ますが、内部的には function.json と同様の仕組みが使われているのでエラーメッセージなどで混乱するかもしれません。実行時エラーで function.json に関するメッセージだった場合にはバインディングとトリガー周りの設定ミスと考えてください。

Python V2 と Node.js V4 について GA してドキュメントとツールのサポートがしっかりと整備されたので、まずはその辺りについて触れていきます。その後に注意点をいくつか紹介します。

Python V2 プログラミングモデル

Python V2 は今年の 5 月に GA していて、既に十分安定していると言っても良いレベルになっていますが、一部のトリガーのテンプレートやドキュメントは追従できていない部分があるため、V1 と V2 どちらのプログラミングモデル向けなのかを確認する必要があります。

Azure Functions を Python で作成する際には VS Code を使うことが大半だと思います。最新の Azure Functions 拡張を入れておくと以下のように新規作成時に Model V2 を選択できるようになっていますが、まだテンプレートは全てのトリガー向けに用意されていません。Azure Functions Core Tools を使っても同じことが可能になっていますが、こちらの方がテンプレートの種類が多い印象です。

VS Code にテンプレートが用意されていないバインディングやトリガーは、今のところはドキュメントを確認して手動で追加する形になると思います。特によく使いそうな Cosmos DB Trigger はまだテンプレートが用意されていないので面倒です。

ドキュメントも V1 と V2 でタブが用意されているので、確実に V2 が選択されていることを確認してから参照してください。書き方がかなり異なっているので間違えることは少ないと思いますが、検索で辿り着くと V1 向けで表示されることもあるので注意です。

Python については Blueprint という概念が追加されていて、少し他の言語とは毛色が異なっている部分もあるので、一度は実際に触って書き方を試しておくのが良いかと思います。

Node.js V4 プログラミングモデル

Python V2 に比べるとリリースが遅れた Node.js V4 ですが、こちらもプレビューを長い期間取っていたので、既にかなり安定していると感じています。書き味としては Express のようなスタイルなので、Node.js を使った開発に慣れている方ならスムーズに利用できると思います。

Node.js V4 も Python V2 と同様に VS Code と最新の Azure Functions 拡張を入れておけば、新規作成時に Model V4 を選ぶことが出来るので簡単に始めることが出来ます。

テンプレートについては Python V2 よりも Node.vs V4 の方が数が多いので、これまで V3 で開発していた人も困ることはないと思います。ドキュメントも V4 がデフォルトで提供されているので、こちらもバージョンを間違えないように注意して読んでください。

ちなみに Node.js V4 についてはマイグレーションのドキュメントが公開されているので、既存の Node.js V3 の Function があれば早めに Node.js V4 へ移行しておいた方が開発が楽になります。

ドキュメントでは V3 と V4 の例が紹介されているので、どのように書き換えればよいのか分かりやすくなっています。そして V4 でどれだけシンプルに書けるようになったかも一目瞭然なので、Azure Functions を Node.js で開発している方は必読だと思います。

最新の Azure Functions Runtime では AzureWebJobsFeatureFlags が不要に

これまで function.json というファイルを手書きする必要があったのを、実行時に各言語ワーカーから受け取る仕組みが追加されたことで、Python と Node.js の開発体験が劇的に向上したのですが、その仕組みがデフォルトで有効化されていないため AzureWebJobsFeatureFlags という設定を追加する必要がありました。

しかし、少し前のコミットでデフォルト有効化するコードが追加され、その実装はバージョン 4.28.0 に含まれているため、このバージョンより新しい Azure Functions Runtime では設定が不要になっています。

自分が確認した限り、大半のリージョンでは 4.28.3 がデプロイされているので設定なしで動作するようになっていますが、一部のリージョンでは 4.27.5 が残っているのと Azure Functions Core Tools が 4.27.5 ベースになっているので、暫くは AzureWebJobsFeatureFlags を明示的に指定しておいた方が安全です。

恐らく今月中には全てのリージョンに 4.28.3 がデプロイされるのではないかと思っています。

Cosmos DB Extension v3 と v4 の混在に注意

これまで Azure Functions ではバインディングやトリガーで使われている SDK のアップデートはあっても、破壊的変更は行われてこなかったのですが Cosmos DB Extension については SDK 側で破壊的変更が多かったため、Azure Functions 側でも Cosmos DB Extension の v3 と v4 の両方が扱えるように実装されました。

C# であれば以前ブログに書いた通り NuGet パッケージを更新して、コンパイルが通らなくなったプロパティ名を修正するぐらいで済むのですが、Python や Node.js では少し罠があります。

具体的に見ていくと、Python V2 の場合はこれまでのバインディングとトリガーでは Cosmos DB Extension の v4 向けの設定となっていて、新しく v3 向けにバインディングとトリガーを追加することでユーザー側がアップデートのタイミングをコントロールできるようになっています。

参考情報になりますが、この実装が追加された PR は以下になります。プロパティ名は Cosmos DB Extension v4 準拠になっているため、Collection が Container になっているなどいくつか変更されています。

Python V2 と同じように Node.js V4 でも Cosmos DB Extension v3 と v4 をユーザー側で選べるようになっていますが、実装が Union 型を使っているためプロパティ名によって型推論の結果が変化し、Cosmos DB Extension v3 と v4 が切り替わるようになっています。

入力補完の結果としては最初は同じようなプロパティが出てきて混乱しますが、最初に connectionconnectionStringSetting を指定するようにすると分かりやすくなります。具体的には前者を使用すると v4 になり後者を使用すると v3 になります。

この辺りの型宣言は以下のファイルで定義されています。この辺りは TypeScript の機能を使って上手く実装した感がありますが、評価が分かれそうな予感です。

こちらも Collection が Container に変更されているので、利用する際にはプロパティ名を間違えないように注意してください。TypeScript ではチェックが走るはずですが JavaScript では素通りになるはずです。

Cosmos DB Extension v4 への Python V2 / Node.js V4 側での対応は以上になりますが、v3 と v4 の切り替えには Azure Functions の Extension Bundle も深く関わっています。Function 実装側で Cosmos DB Extension v4 を使うようにしても、Extension Bundle が古いままでは実行時エラーとなり動作しません。v4 を使う際には Extension Bundle のバージョンも v4 以上に更新する必要があります。

{
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  }
}

一部の言語向けではテンプレートから作成すると Extension Bundle v3 が使われる設定になっているため、Cosmos DB Extension v4 向けの Function を追加すると実行時エラーとなります。

Extension Bundle に含まれている拡張機能のバージョンは GitHub のリリースから確認出来ます。Cosmos DB Extension v3 と v4 が共存できないため Extension Bundle のバージョンで切り替えを行う必要があります。

C# で開発している場合には意識することは無いので忘れがちですが、Python や Node.js では Extension Bundle がかなり重要なのでバージョンには気を付ける必要があります。

Static Web Apps の Managed Functions 上での利用について

通常の Azure Functions であれば Consumption Plan であっても、設定だけ追加してしまえば問題なく Python V2 / Node.js V4 で実装したアプリケーションをデプロイして実行できますが、Static Web Apps に付属している Managed Functions では AzureWebJobs から始まるキーの設定は禁止されているので、これまでは以下の Issue にもあるようにどう頑張っても実行できませんでした。

しかし前述した通り、Azure Function Runtime 側でデフォルト有効になるように変更されたため、特定のバージョン以降がデプロイされたリージョンでは Python V2 / Node.js V4 の実行をサポートしています。

確認した限りでは East Asia では 4.28.3.21820 がデプロイされているので、以下のような Node.js V4 を使った Function を Managed Functions として実行出来ます。

実際に上の Function をデプロイしてみましたが、以下のように問題なくデプロイが行えて Azure Portal からも認識されています。Azure Functions Runtime のバージョンが古かった時はデプロイは出来ても実行は出来ず、Azure Portal からもデプロイした Function が認識されないという状態でした。

全リージョンに展開されているのかは確認していませんが、恐らく一部のリージョンでは古いバージョンが残っていると思われるので注意は必要ですが、よく使われるであろう East Asia では問題なく使えそうです。

Azure OpenAI Service の GPT-4 Turbo with Vision と Azure AI Service Enhancement を試した

OpenAI 本家の提供から数週間遅れましたが、Azure OpenAI Service でも GPT-4 Turbo with Vision が一部リージョンで利用できるようになりました。本家とは異なり Azure AI Service Enhancement という Azure OpenAI 専用の機能も同時にリリースされています。

詳細は後述しますが、Azure AI Service Enhancement は現時点では若干挙動がおかしい部分があるので、利用には少し注意が必要になっています。

現時点では Japan East で利用できないため West US で利用するケースが多いと思います。レイテンシ的には West US の方が Australia East より有利な可能性があり、実際にネットワークのレイテンシは West US の方が Australia East より小さいです。

GPT-4 Turbo with Vision は GPT-4 Turbo が持っている機能全てがサポートされているわけではないので、ユースケースによっては追加のプロンプトを書く必要も出てきます。

実際に Azure OpenAI Service で GPT-4 Turbo with Vision を使う方法は以下のドキュメントに全てまとまっています。画像の入力方法は HTTP/HTTPS 指定あるいは Data URL を利用した Base64 での指定が必要です。

モデルのデプロイは vision-preview バージョンを指定して gpt-4 をデプロイします。vision-preview は通常のバージョンと独立してデプロイできるようになっています。

GPT-4 Turbo with Vision と Azure AI Service Enhancement は Azure OpenAI Studio を使うとサクッと試すことが出来ますが、最終的にはアプリケーションに組み込むので SDK から利用してみます。

C# SDK から GPT-4 Turbo with Vision を利用する

最新の Azure OpenAI SDK for .NET では GPT-4 Turbo with Vision の早期サポートが追加されているため、以下のパッケージをインストールあるいは更新すると利用できます。使い方もこれまでとほぼ同じなので経験があれば悩むことはないでしょう。

サンプルコードを載せておきますが、基本的にはテキストベースの時と変わっていません。テキストの代わりに画像の URL を指定する点が唯一の違いとも言えます。

using Azure;
using Azure.AI.OpenAI;

var openAIClient = new OpenAIClient(new Uri("https://****.openai.azure.com/"), new AzureKeyCredential("****"));

var response = await openAIClient.GetChatCompletionsAsync(new ChatCompletionsOptions
{
    DeploymentName = "gpt-4v",
    Messages =
    {
        new ChatRequestSystemMessage("You are an AI assistant that helps people find information."),
        new ChatRequestUserMessage(
            new ChatMessageTextContentItem("この画像を日本語で説明してください。"),
            new ChatMessageImageContentItem(new Uri("https://***.blob.core.windows.net/kazuakix.jpg")))
    },
    MaxTokens = 1024
});

Console.WriteLine(response.Value.Choices[0].Message.Content);

当然ながら URL で指定した画像はパブリックからのアクセスが可能である必要があります。

このコードを実行すると、指定した URL の画像を利用して結果が生成されます。非常に高い精度で画像の説明が生成されていることに驚きますね。

先ほどのサンプルではパブリックアクセス可能な Blob を用意して URL を指定しましたが、 実際の要件としては画像を外部からアクセス可能な状態にはしたくないケースの方が多いはずです。一応 SAS 付きの Blob URL を指定することでアクセス制限された Blob を使って応答を生成させる事は出来ましたが、ネットワーク的な制限がかかっている場合には対応出来ません。

そのようなケースでは Base64 化した画像をリクエストに含めて送信するのが解決策となりそうです。Data URL を生成する便利メソッドは用意されていないので、手動で組み立てる形になります。

using Azure;
using Azure.AI.OpenAI;

Uri ToDataUrl(string filePath, string contentType) => new($"data:{contentType};base64,{Convert.ToBase64String(File.ReadAllBytes(filePath))}");

var openAIClient = new OpenAIClient(new Uri("https://****.openai.azure.com/"), new AzureKeyCredential("****"));

var response = await openAIClient.GetChatCompletionsAsync(new ChatCompletionsOptions
{
    DeploymentName = "gpt-4v",
    Messages =
    {
        new ChatRequestSystemMessage("You are an AI assistant that helps people find information."),
        new ChatRequestUserMessage(
            new ChatMessageTextContentItem("この画像を日本語で説明してください。"),
            new ChatMessageImageContentItem(ToDataUrl(@".\kazuakix.jpg", "image/jpeg")))
    },
    MaxTokens = 1024
});

Console.WriteLine(response.Value.Choices[0].Message.Content);

Uri クラスで扱う URL は 65519 バイト以下である必要があるので、大きな画像は Uri クラスを利用している限り扱えません。この辺りは C# SDK の制約という感じです。

OpenAI としては長い会話の場合は URL を指定するように推奨しているのと、アップロード可能な POST サイズには制限がありそうなので、可能な限り URL で渡した方が効率的だと考えられます。

Vision Enhancement (OCR / Grounding) を利用する

基本的な GPT-4 Turbo with Vision の利用が出来たので、次は Azure AI Service Enhancement を試していきます。多少名称がブレている気がしますが、Azure AI Service Enhancement は Vision Enhancement とも呼ばれているようなので、どちらも正解だと思っています。

GPT-4 Turbo with Vision は画像内のテキストも割と認識してくれるので意外かもしれませんが、基本的にテキストを認識する機能は持ち合わせていないようです。特に日本語などは苦手としているので、その部分を Azure AI Service で補うのが Vision Enhancement となります。

今回はサンプルとして Azure OpenAI Service のモデル一覧のテーブルを Markdown のテーブルに変換する処理をやらせてみます。まずは Azure OpenAI Studio 上で Vision Enhancement を無効化した状態で試してみると、以下のように画像の解析は出来ないと断られてしまいました。

何回か試すと頑張ってくれるケースもあるのですが、実利用するには不安定なので難しいです。このあたりの挙動を Azure AI Vision (Azure Computer Vision) を利用することで改善可能です。

実際に画像に対して Vision Enhancement を有効化するサンプルコードは以下のようになります。少し OpenAI とは関係のない余計な実装が付いているのは後述します。

using Azure;
using Azure.AI.OpenAI;

using Mono.Reflection;

Uri ToDataUrl(string filePath, string contentType) => new($"data:{contentType};base64,{Convert.ToBase64String(File.ReadAllBytes(filePath))}");

AzureChatExtensionsOptions CreateExtensionsOptions(AzureChatEnhancementConfiguration enhancementConfiguration)
{
    var extensionOptions = new AzureChatExtensionsOptions();

    var property = typeof(AzureChatExtensionsOptions).GetProperty(nameof(AzureChatExtensionsOptions.EnhancementOptions));
    var field = property.GetBackingField();

    field.SetValue(extensionOptions, enhancementConfiguration);

    return extensionOptions;
}

var openAIClient = new OpenAIClient(new Uri("https://****.openai.azure.com/"), new AzureKeyCredential("****"));

var options = new ChatCompletionsOptions
{
    DeploymentName = "gpt-4v",
    AzureExtensionsOptions = CreateExtensionsOptions(new AzureChatEnhancementConfiguration
    {
        Grounding = new AzureChatGroundingEnhancementConfiguration(true),
        Ocr = new AzureChatOCREnhancementConfiguration(true)
    }),
    Messages =
    {
        new ChatRequestSystemMessage("You are an AI assistant that helps people find information."),
        new ChatRequestUserMessage(
            new ChatMessageTextContentItem("この画像に含まれているテーブルを Markdown に変換してください。"),
            new ChatMessageImageContentItem(ToDataUrl(@"***.png", "image/png")))
    },
    MaxTokens = 1024
};

var response = await openAIClient.GetChatCompletionsAsync(options);

Console.WriteLine(response.Value.Choices[0].Message.Content);

先程のコードとの違いは AzureExtensionsOptions を追加している部分だけです。余計なコードが付いているのは、現状の Azure OpenAI SDK に不具合があるため、その部分をリフレクションで無理やり回避しているためです。Issue は作成済みなので次のバージョンで直ることを期待しています。

サンプルコードの実行結果は以下となりますが、ちゃんとテキストを認識して Markdown として出力されていることが確認できます。テーブル構造の認識に失敗していますが、結合されたセルの扱いは苦手のようです。

次は日本語も含んだ大きなテーブルを試してみます。例として App Service の料金テーブルを画像として保存して、同じように Markdown に変換させてみた結果が以下の通りです。

問題なく日本語も認識していますが、空のセルがある場合に正しく認識されていないことが分かります。Azure AI Vision によって OCR された結果が悪いのか、OpenAI に渡すタイミングで構造が失われているのかは分かっていませんが、配置などの情報は渡されていない予感です。

ここまで Vision Enhancement を試してきましたが、実は Azure AI Vision の利用に必要な設定を追加していないことに気が付かれた方もいるかもしません。

Azure OpenAI Studio 上ではトグルスイッチをオンにして、Azure OpenAI Service と同じリージョンにデプロイした Azure AI Vision を指定していたように、SDK から利用する場合も同じ手順が必要になるはずですが、現状は指定せずとも動作しています。

ドキュメント上も Azure AI Vision が必要と書かれていますが、未指定で OCR などの設定を有効化するだけで動作しています。Azure OpenAI Service だけでは不可能な Grounding も動作していることを確認しましたので、Azure AI Vison が裏側では使われているようです。

To use Vision enhancement, you need a Computer Vision resource, and it must be in the same Azure region as your GPT-4 Turbo with Vision resource.

How to use the GPT-4 Turbo with Vision model - Azure OpenAI Service | Microsoft Learn

逆に Azure AI Vision を利用する設定を追加して、リクエストを投げてみましたがメトリックを見る限り呼び出されている形跡がありませんでした。ドキュメントとは異なる挙動なので不具合なのだと思いますが、正直なところ別リソース用意するのではなく、今のように内部でよしなにやっていてほしいです。

特にネットワーク制限が必要な環境の場合は、Azure OpenAI Service から Azure AI Vision へのアクセスが問題となるケースも発生するため、内部で閉じておいて貰った方が扱いやすくなります。

REST API を直接利用する際の注意点

通常の GPT-4 Turbo with Vision を利用する際にはこれまで通りの REST API エンドポイントで問題なく動作するのですが、Azure AI Service Enhancement を利用する際にはエンドポイントが変わっているので、間違うと常にバリデーションエラーが出ることになります。

ドキュメントのエンドポイントを注意深く見るとパスに extensions が追加されたものになっています。

Azure AI Service Enhancement 無し
https://{RESOURCE_NAME}.openai.azure.com/openai/deployments/{DEPLOYMENT_NAME}/chat/completions?api-version=2023-12-01-preview

Azure AI Service Enhancement 有り
https://{RESOURCE_NAME}.openai.azure.com/openai/deployments/{DEPLOYMENT_NAME}/extensions/chat/completions?api-version=2023-12-01-preview

2 つのエンドポイントがあることはドキュメントからは読み取りにくいため、バリデーションエラーが出た場合はエンドポイントのミスを疑ってみてください。

Azure App Service の OS が Windows Server 2022 にアップデートされ始めているので確認した

先日の Ignite 2023 では App Service の TLS 1.3 対応が発表されて、一部のリージョンでは既に有効化されていることを以下のエントリで触れましたが、TLS 1.3 対応と同時に App Service の OS が Windows Server 2022 へとアップデートされ始めています。

App Service のマルチテナント版だけではなく、App Service Environment v3 でも TLS 1.3 対応がデプロイされるようです。流石にメンテナンス通知は来ると信じています。

先日の GitHub Universe 2023 と Microsoft Ignite 2023 のキャッチアップイベントでも少し話しましたので、興味があれば以下のアーカイブを参照してください。

6 年振りに App Service の Windows Server がアップデートされるので、Windows Server 2016 へのアップデートが行われた時のエントリを拾ってきました。2016 へのアップデートは HTTP/2 の有効化が目玉と言えますが、今回の 2022 では TLS 1.3 の有効化が目玉だと言えます。

HTTP/2 は明示的に有効化しない限り、自動的に有効にはなりませんでしたが、TLS 1.3 についてはクライアント側が対応していれば自動的に使われるようです。設定としては最低の TLS バージョンしか用意されないので、TLS 1.3 の無効化は出来ないと考えています。

今回のアップデートも完全にインプレースかつ Scale unit 単位で段階的にで実行されるようなので、気が付いたら稼働中の App Service の OS が 2022 になっているはずです。このあたりのアップデートについてはドキュメントが少しだけあるので、興味があれば参照してください。

ちなみに既に Web App for Container (Windows) は Windows Server 2022 にアップデートされていました。しかし Frontend や Web Worker は Windows Server 2016 のままだったので、その部分が今回アップデートされようとしているようです。

App Service の Frontend は Windows / Linux / Container で全て共通なので、今回のアップデートで App Service for Linux などでも TLS 1.3 が自動的に有効になるはずです。Windows を使っていなくても影響のある部分なので少し気を付けたいです。

意外に App Service が使われているサービスは多いので影響範囲は広そうです。Azure Functions や Logic Apps で使われていることは広く知られていると思いますが、API Management の Consumption や Basic v2 / Standard v2 でも App Service が使われているので、自動的に TLS 1.3 が有効化されると考えています。

互換性への影響

インプレースでアップデートされるので、既に App Service で本番環境を運用している場合には互換性が心配だと思いますが、結論から言うとほぼ気にする必要はないと考えています。Windows Server 2019 / 2022 でのアップデート内容は以下のドキュメントで公開されていますが、App Service で使われている機能に対しての大きな変更などはありません。

互換性に一番影響しそうな .NET Framework のバージョンについても、これまで通りの 4.8 となっていましたのでアップデートされたことに気が付かないまま Windows Server 2022 になっていそうです。

但し Windows Server 2022 は .NET Framework 4.8.1 のインストールに対応しているため、将来的に .NET Framework 4.8.1 にアップデートされる可能性はあると考えています。とはいえ互換性に影響しそうなアップデートはないので、ほぼ気にする必要はなさそうです。

正直なところ一番互換性に影響しそうなのは、TLS 1.3 が自動的に有効化される点でしょう。通常の環境ではまず影響しないはずですが、謎のプロキシがある場合には TLS 1.3 が通らないというケースもありそうです。

以下は Free プランで動かしている App Service ですが、TLS 1.3 が有効化されていることを確認しました。

何も設定をしていなくても、App Service の Frontend がアップデートされたタイミングで TLS 1.3 が有効になるのは、頭の片隅にでも入れておいた方が良さそうです。

クライアント証明書を有効化している場合、TLS 1.3 では利用できない機能があるらしく TLS 1.2 になるという話が公式ブログで紹介されていました。利用している場合には早めに検証しておいた方が良さそうです。

現在の Windows Server バージョンを確認

最後に App Service の Windows Server バージョンを確認する方法を何種類か紹介しておきます。一番簡単な方法は Azure Portal の Console から ver コマンドを実行する方法です。

定番の方法としては Kudu の Environment にある System info から OS Version を確認するというものもあります。この表示は C# の Environment.OSVersion を使っているので若干大雑把な表示です。

最後は Kudu が提供する Runtime API を利用する方法です。呼び出すと以下のように OS バージョンが細かいレベルまで返ってきますが、何故か OS 名は Windows Server 2016 のままになるので、ビルド番号を確認するのが正確な方法になります。

6 年振りの Windows Server バージョンのアップデートなので経験したことが無い方も多いと思いますが、基本は内部のテスト用環境から段階的にロールアウトされていくので、OS バージョンに依存した処理を書いていない限りは問題ないので安心してください。

日本リージョンにデプロイされるのは恐らく来年になると思うので、年内は US リージョンでテストされて問題があらかた潰れたタイミングでアップデートされることになるでしょう。

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

先日シアトルで開催された Microsoft Ignite 2023 では Azure Cosmos DB のアップデートも Build 2023 程ではありませんが多く発表されました。例によって AI を意識したものが大きく扱われていますが、スケーラビリティの改善に繋がる機能もしっかり追加されています。

発表については公式ブログでまとまっているので、基本はこちらを参照すれば問題ないです。

ちなみに Build 2023 で発表された Cosmos DB のアップデートは以下の通りなので、興味があれば参照してください。Build 2023 で Private Preview だった機能が Ignite 2023 で Public Preview になったものもあります。

例によって個人的に気になっている機能について簡単にまとめます。一部の機能については詳細な検証が必要だと感じたら、別途検証結果をブログに書く予定です。

MongoDB vCore と Vector Search が GA

昨今の AI ブームと共に到来した Vector Search ブームに乗って、Cosmos DB の MongoDB vCore と Vector Search が GA しました。これまで MongoDB vCore がプレビューだったことを認識していませんでした。

MongoDB vCore の Vector Search については紹介記事が多いはずなので特に深堀りはしませんが、個人的には Ignite のセッションで発表された NoSQL API での Vector Search サポートの方を注目しています。

恐らくは Function として実装されると考えていますが、この辺りは Preview が始まるのを待ちたいと思います。少し不安なのは 1536 次元の配列を読み書きするのに必要な RU が、思っているよりも大きくなりそうな点です。Cosmos DB の仕組み上は Native 実装が追加されても RU が減ることはないはずなので、コスト面は注意したいと考えています。

Dynamic Scaling Per Region and Per Partition が Public Preview

今回のアップデートの中で個人的に一番注目しているのがこの Dynamic Scaling Per Region and Per Partition です。基本は Autoscale を有効化しているコンテナーに対しての機能で、マルチリージョンや 50GB 以上のコンテナーを運用している場合には有効化するだけで、全体的なコスト最適化が期待できます。

マルチリージョンで Write と Read を分けているケースは分かりやすいのですが、物理パーティションが 2 つ以上存在するケースでは理解が難しいかも知れません。具体的な例はドキュメントに記載されているので、基本はこちらを読み込んでおけば問題ありません。

Cosmos DB は設定した最大 RU は物理パーティションの数だけ均等に分割され、一番多く RU を消費したパーティションの値を使って全体で消費した RU の計算が行われますが、Dynamic Scaling が有効な場合は物理パーティション毎の最大値を使った集計が行われるという仕組みです。従って物理パーティションが常に全て同じ RU を消費しているというケース以外では確実にコストダウンが見込めます。

Autoscale を利用している場合には確実に有効化しておきたい機能ではありますが、現時点では 11/15 以降に作成した Cosmos DB アカウントでのみ有効化が可能です。あくまでも Preview 中の制約だと考えていますので、GA 時には既存のアカウントでも有効化出来るようになると思います。

Cosmos DB を活用する上で RU からは逃げられないため、Autoscale と Burst Capacity そして今回の Dynamic Scaling を活用してコストを最適化しつつパフォーマンスとスケーラビリティを高めましょう。

Priority-based execution が Public Preview

Priority-based execution は Build 2023 で発表された時は Private Preview 扱いで、有効化するためにチームとのやり取りが必要だったのですが Ignite 2023 のタイミングで Public Preview に移行しました。

Cosmos DB では設定した RU 以上を必要とすると 429 を返してリトライという流れになりますが、Priority-based execution を使うとリクエスト単位で優先度を指定できるので、ユーザー操作に紐づくリクエスト以外は優先度を下げることで、応答性を改善するといった実装が可能になります。

デフォルトでは全てのリクエストは優先度 High に設定されていますが、このデフォルト値は Azure CLI などを使うと変更可能なので、デフォルトを Low にしつつ必要なリクエストだけ High にするというのも手です。

利用するには最新のプレビュー版 SDK のインストールが必要になるので、Azure Functions の Cosmos DB 拡張などを使っている場合は利用できません。

この機能は既存のアカウントでも有効化出来るようなので、比較的気軽に試せるものとなっています。但し Serverless には対応していないので Provisioned Throughput で作る必要があります。

Priority-based execution を有効化すると、Azure Portal にある Data Explorer からの操作がデフォルトで Low になるのが地味に便利そうです。本番向けに調査用のクエリを投げたい場合には、Data Explorer のせいでアプリ側で 429 が多発することがありました。

設定で変更できますが、基本は Low のままで問題ないはずです。正式版がリリースされた際には Azure Functions の CosmosDB Trigger を Low にして実行させる検証を行いたいと思っています。Change Feed は Checkpoint を持っているため 429 が発生した際のリトライが行いやすく、処理内容としても Priority-based execution と相性が良いはずです。

Cross-account container copy が Public Preview

これまで同一アカウント内でのコンテナーのコピーは Preview で公開されていましたが、アカウント間でのコピーも Public Preview となりました。これまで Data Factory を使うことが多かった Cosmos DB のコピーですが、Azure CLI だけで完結するようになりました。

Azure Portal からもコピー出来るようになって欲しいですが、同一アカウント内であってもサポートされていないため時間がかかりそうです。少し権限周りの割り当てが手間になりそうですが、Change Feed を使ってコピーするため安心感があります。

Change Feed を使って読み取りを行うということは、アプリケーションが稼働していて書き込みが発生するとデータの一貫性が損なわれる可能性があるということなので、オフラインにしてから処理を行う必要がある点には注意しておきたいですね。

Microsoft Copilot for Azure in Azure Cosmos DB

Ignite では Microsoft Copilot for Azure が発表されましたが、Cosmos DB についても Data Explorer 上でのクエリ作成を支援してくれる Copilot が追加されました。名前が異常に長いのが気になります。

Cosmos DB の SQL は T-SQL とも異なっているので多少癖がありますが、その辺りを上手く Copilot で書いてくれると楽は出来そうな気がします。しかし最近はクエリ作成に Copilot が必要となるようなデータ構造を設計しないので、個人的にはあまり使わない予感です。

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 という設定になっている