しばやん雑記

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

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

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

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

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

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

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

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

Dockerfile 付きで Durable Functions を作成

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

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

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

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

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

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

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

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

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

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

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

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

必要な Storage Account と App Insights を作成

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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