Firewall によりロックダウンされた Azure Functions や App Service に対するコードのデプロイ失敗について

# 前提：App Service と Firewall を組み合わせる
App Service の通信先を制限する方法として、[App Service を VNet 統合](https://learn.microsoft.com/ja-jp/azure/app-service/overview-vnet-integration) し、 [ネットワーク セキュリティ グループ(NSG)](https://learn.microsoft.com/ja-jp/azure/virtual-network/network-security-groups-overview) を使用して、通信の宛先を制限する方法があります。

一方で、NSG を使用した送信先の制限では、通信先として IP アドレスや[ネットワーク サービスタグ](https://learn.microsoft.com/ja-jp/azure/virtual-network/service-tags-overview) を指定することは可能ですが、特定の URI などへの通信許可など細かい設定を行うことはできません。
こうした細かい通信制御を行いたい、脅威の検知などの目的で、すべての通信を Azure Firewall を経由した構成とする場合があり、ドキュメントにも [Azure Firewall を使用して送信トラフィックを制御する](https://learn.microsoft.com/ja-jp/azure/app-service/network-secure-outbound-traffic-azure-firewall) というシナリオでの設定手順が公開されています。


一方で、App Service, Azure Functions を VNet 統合し、通信先を制限した場合、アプリケーションのデプロイや App Service 上でのビルドに失敗するという事象が起きます。
この記事では、App Service と Azure Functions からの通信の宛先を Azure Firewall を経由する場合の注意事項について示します。


# App Service, Azure Functions へのコードのデプロイに失敗する理由
App Service と Azure Functions に対するコードのデプロイに失敗する、HTTP ステータスコード 500 が応答される原因は大きく分けて 2 つあります。
- Firewall で Functions への通信がブロックされている
- Firewall で App Service, Functions ビルドの動作が妨げられている

それぞれ例を挙げます。

## Firewall で Functions への通信がブロックされている
使用する言語やデプロイの方法によりますが、デプロイ時のエラーメッセージに HTTP ステータスコード 500 で "An error occurred while sending the request." というメッセージを含んでいる場合があります。
```
[ERROR] Status code 500, "{"Message":"An error has occurred.","ExceptionMessage":"An error occurred while sending the request.","ExceptionType":"System.Net.Http.HttpRequestException"
```

Azure Functions に対するデプロイでは、(1) デプロイするクライアントから Azure Functions (正確にはデプロイを行うビルドエンジン [Kudu](https://github.com/projectkudu/kudu)) に対する要求に対する応答、(2) デプロイ先の Azure Functions 上の処理結果についての応答の 2 種類が応答されます。
上記の例では、HTTP ステータスコードは (1) のデプロイ先の Kudu が応答しており、[HTTP ステータスコード 500](https://developer.mozilla.org/ja/docs/Web/HTTP/Status/500) は多くの場合何らかの例外により処理が中断された可能性を示しています。
Kudu 上でデプロイが失敗した際の例外の内容として "An error occurred while sending the request.", "System.Net.Http.HttpRequestException" といったメッセージが返ってきています。

該当の時間帯の Azure Firewall のログを確認すると、Azure Functions が使用している VNet のさサブネットから自分自身の SCM サイト (*.scm.azurewebsites.net) の呼出が遮断された形跡が見つかりました。
```
HTTPS request from 10.0.2.254:50347 to ***-functions-java11.scm.azurewebsites.net:443. Action: Deny. No rule matched. Proceeding with default action
```
![firewall1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/355cdfa4-0c31-1f16-0ac1-098287989829.jpeg)



詳細は後ほど解説しますが、多くの場合 Kudu から Kudu 自身への要求を発行したが、到達できなかった事が原因です。


## Firewall で App Service, Functions ビルドの動作が妨げられている
以下は App Service において、Python のコードをローカル Git からのデプロイを行った場合の例です。
コード自体はあらかじめローカル Git にコミット済であり、App Service ビルドでビルド可能であることは確認済です。

一方で、1度ビルドが成功した後に、Azure Firewall を通るように構成したところ以下のようにビルドに失敗するようになりました。

```
2023-04-21T18:49:48.7338512Z,Updating branch 'cdb25972f4e942b03b62a1a653eaad680b2d6ba2'.,2df9cefc-3893-4e78-a5d5-e091d41598c8,0
2023-04-21T18:49:50.1310972Z,Updating submodules.,07e63ebe-c26e-4f10-8225-0e74614cd593,0
2023-04-21T18:49:50.2640811Z,Preparing deployment for commit id 'cdb25972f4'.,862d0d4b-9498-443d-98e9-14a03cacd866,0
2023-04-21T18:49:50.6878923Z,PreDeployment: context.CleanOutputPath False,11c5863c-ba3d-4532-84a2-348b23acf987,0
2023-04-21T18:49:50.8151809Z,PreDeployment: context.OutputPath /home/site/wwwroot,eaeb3406-a31f-4bc9-85bd-00d381a24534,0
2023-04-21T18:49:50.9560229Z,Repository path is /home/site/repository,f9757498-e73a-468a-971c-682d12b57906,0
2023-04-21T18:49:51.1385765Z,Running oryx build...,0aaf7b34-1b73-442c-a39f-4cb9a58cff99,0
	2023-04-21T18:49:51.1728121Z,Command: oryx build /home/site/repository -o /home/site/wwwroot --platform python --platform-version 3.10 -p virtualenv_name=antenv --log-file /tmp/build-debug.log  -i /tmp/8db42992fe59d6a --compress-destination-dir | tee /tmp/oryx-build.log,,0
	2023-04-21T18:49:55.2101020Z,Operation performed by Microsoft Oryx&comma; https://github.com/Microsoft/Oryx,,0
	2023-04-21T18:49:55.2885060Z,You can report issues at https://github.com/Microsoft/Oryx/issues,,0
	2023-04-21T18:49:55.3450876Z,,,0
	2023-04-21T18:49:55.4139800Z,Oryx Version: 0.2.20220825.1&comma; Commit: 24032445dbf7bf6ef068688f1b123a7144453b7f&comma; ReleaseTagName: 20220825.1,,0
	2023-04-21T18:49:55.4484751Z,,,0
	2023-04-21T18:49:56.0192022Z,Build Operation ID: |q36SkNblbPg=.3f06f9a8_,,0
	2023-04-21T18:49:56.0449037Z,Repository Commit : cdb25972f4e942b03b62a1a653eaad680b2d6ba2,,0
	2023-04-21T18:49:56.0648263Z,,,0
	2023-04-21T18:49:56.0852178Z,Detecting platforms...,,0
	2023-04-21T18:51:35.8182167Z,Error: Oops... An unexpected error has occurred.,,1
	2023-04-21T18:51:35.9134595Z,Error: System.AggregateException: One or more errors occurred. (A task was canceled.),,1
	2023-04-21T18:51:35.9691714Z, ---> System.Threading.Tasks.TaskCanceledException: A task was canceled.,,1
	2023-04-21T18:51:35.9899149Z,   --- End of inner exception stack trace ---,,1
	2023-04-21T18:51:36.0125720Z,   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification),,1
	2023-04-21T18:51:36.0597069Z,   at System.Threading.Tasks.Task`1.get_Result(),,1
	2023-04-21T18:51:36.0808670Z,   at Microsoft.Oryx.BuildScriptGenerator.SdkStorageVersionProviderBase.GetAvailableVersionsFromStorage(String platformName) in /usr/oryx/src/BuildScriptGenerator/SdkStorageVersionProviderBase.cs:line 51,,1
	2023-04-21T18:51:36.1031743Z,   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonSdkStorageVersionProvider.GetVersionInfo() in /usr/oryx/src/BuildScriptGenerator/Python/VersionProviders/PythonSdkStorageVersionProvider.cs:line 25,,1
	2023-04-21T18:51:36.1461841Z,   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonVersionProvider.GetVersionInfo() in /usr/oryx/src/BuildScriptGenerator/Python/VersionProviders/PythonVersionProvider.cs:line 44,,1
	2023-04-21T18:51:36.2102352Z,   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonPlatform.get_SupportedVersions() in /usr/oryx/src/BuildScriptGenerator/Python/PythonPlatform.cs:line 117,,1
	2023-04-21T18:51:36.2380937Z,   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonPlatform.GetMaxSatisfyingVersionAndVerify(String version) in /usr/oryx/src/BuildScriptGenerator/Python/PythonPlatform.cs:line 624,,1
	2023-04-21T18:51:36.2571823Z,   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonPlatform.ResolveVersions(RepositoryContext context&comma; PlatformDetectorResult detectorResult) in /usr/oryx/src/BuildScriptGenerator/Python/PythonPlatform.cs:line 387,,1
	2023-04-21T18:51:36.2773122Z,   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonPlatform.Detect(RepositoryContext context) in /usr/oryx/src/BuildScriptGenerator/Python/PythonPlatform.cs:line 136,,1
	2023-04-21T18:51:36.3283172Z,   at Microsoft.Oryx.BuildScriptGenerator.DefaultPlatformsInformationProvider.GetPlatformsInfo(RepositoryContext context) in /usr/oryx/src/BuildScriptGenerator/DefaultPlatformsInformationProvider.cs:line 53,,1
	2023-04-21T18:51:36.3845782Z,   at Microsoft.Oryx.BuildScriptGenerator.DefaultBuildScriptGenerator.GenerateBashScript(BuildScriptGeneratorContext context&comma; String& script&comma; List`1 checkerMessageSink) in /usr/oryx/src/BuildScriptGenerator/DefaultBuildScriptGenerator.cs:line 70,,1
	2023-04-21T18:51:36.4329592Z,   at Microsoft.Oryx.BuildScriptGeneratorCli.BuildScriptGenerator.TryGenerateScript(String& generatedScript&comma; Exception& exception) in /usr/oryx/src/BuildScriptGeneratorCli/BuildScriptGenerator.cs:line 65,,1
	2023-04-21T18:51:36.5027119Z,   at Microsoft.Oryx.BuildScriptGeneratorCli.BuildCommand.Execute(IServiceProvider serviceProvider&comma; IConsole console) in /usr/oryx/src/BuildScriptGeneratorCli/Commands/BuildCommand.cs:line 168,,1
	2023-04-21T18:51:36.5282633Z,   at Microsoft.Oryx.BuildScriptGeneratorCli.CommandBase.OnExecute(CommandLineApplication app&comma; IConsole console) in /usr/oryx/src/BuildScriptGeneratorCli/Commands/CommandBase.cs:line 84,,1
	2023-04-21T18:51:37.2252603Z,Error: Oops... An unexpected error has occurred.\nError: System.AggregateException: One or more errors occurred. (A task was canceled.)\n ---> System.Threading.Tasks.TaskCanceledException: A task was canceled.\n   --- End of inner exception stack trace ---\n   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)\n   at System.Threading.Tasks.Task`1.get_Result()\n   at Microsoft.Oryx.BuildScriptGenerator.SdkStorageVersionProviderBase.GetAvailableVersionsFromStorage(String platformName) in /usr/oryx/src/BuildScriptGenerator/SdkStorageVersionProviderBase.cs:line 51\n   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonSdkStorageVersionProvider.GetVersionInfo() in /usr/oryx/src/BuildScriptGenerator/Python/VersionProviders/PythonSdkStorageVersionProvider.cs:line 25\n   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonVersionProvider.GetVersionInfo() in /usr/oryx/src/BuildScriptGenerator/Python/VersionProviders/PythonVersionProvider.cs:line 44\n   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonPlatform.get_SupportedVersions() in /usr/oryx/src/BuildScriptGenerator/Python/PythonPlatform.cs:line 117\n   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonPlatform.GetMaxSatisfyingVersionAndVerify(String version) in /usr/oryx/src/BuildScriptGenerator/Python/PythonPlatform.cs:line 624\n   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonPlatform.ResolveVersions(RepositoryContext context&comma; PlatformDetectorResult detectorResult) in /usr/oryx/src/BuildScriptGenerator/Python/PythonPlatform.cs:line 387\n   at Microsoft.Oryx.BuildScriptGenerator.Python.PythonPlatform.Detect(RepositoryContext context) in /usr/oryx/src/BuildScriptGenerator/Python/PythonPlatform.cs:line 136\n   at Microsoft.Oryx.BuildScriptGenerator.DefaultPlatformsInformationProvider.GetPlatformsInfo(RepositoryContext context) in /usr/oryx/src/BuildScriptGenerator/DefaultPlatformsInformationProvider.cs:line 53\n   at Microsoft.Oryx.BuildScriptGenerator.DefaultBuildScriptGenerator.GenerateBashScript(BuildScriptGeneratorContext context&comma; String& script&comma; List`1 checkerMessageSink) in /usr/oryx/src/BuildScriptGenerator/DefaultBuildScriptGenerator.cs:line 70\n   at Microsoft.Oryx.BuildScriptGeneratorCli.BuildScriptGenerator.TryGenerateScript(String& generatedScript&comma; Exception& exception) in /usr/oryx/src/BuildScriptGeneratorCli/BuildScriptGenerator.cs:line 65\n   at Microsoft.Oryx.BuildScriptGeneratorCli.BuildCommand.Execute(IServiceProvider serviceProvider&comma; IConsole console) in /usr/oryx/src/BuildScriptGeneratorCli/Commands/BuildCommand.cs:line 168\n   at Microsoft.Oryx.BuildScriptGeneratorCli.CommandBase.OnExecute(CommandLineApplication app&comma; IConsole console) in /usr/oryx/src/BuildScriptGeneratorCli/Commands/CommandBase.cs:line 84\n/bin/bash -c "oryx build /home/site/repository -o /home/site/wwwroot --platform python --platform-version 3.10 -p virtualenv_name=antenv --log-file /tmp/build-debug.log  -i /tmp/8db42992fe59d6a --compress-destination-dir | tee /tmp/oryx-build.log ; exit $PIPESTATUS ",,2
2023-04-21T18:51:37.5287645Z,,db6d5c29-8c27-489b-9f4c-a47c77663aac,0
2023-04-21T18:51:37.6839124Z,Generating summary of Oryx build,97da3a1d-2725-4091-b29e-a6aa34509d61,0
2023-04-21T18:51:38.1043561Z,Parsing the build logs,350e98c3-fecf-4fab-83ce-138969920ac3,0
2023-04-21T18:51:38.2069827Z,Found 0 issue(s),f684e1c2-8130-43f3-b53b-e50406f99337,0
2023-04-21T18:51:38.3445471Z,,b478f878-871f-4fa2-b96b-9b69cb9a9fa4,0
2023-04-21T18:51:38.4493777Z,Build Summary :,968ef409-b459-4dea-93d3-021f75ecf4a4,0
2023-04-21T18:51:38.5473839Z,===============,5e8cb5aa-7324-48bc-9a51-a68b5760e8fe,0
2023-04-21T18:51:38.6533858Z,Errors (0),88e389c8-5d92-4ba8-b622-b1cf42182331,0
2023-04-21T18:51:38.7566946Z,Warnings (0),c0c69201-5601-452a-b2b9-9390ffb23cda,0
2023-04-21T18:51:38.8534474Z,,c9abe98c-dbe3-4d8b-9b17-664a2eab339e,0
2023-04-21T18:51:39.0972539Z,Deployment Failed. deployer =  deploymentPath = ,7891d62d-2a95-4823-aba6-7bbc379b2df7,0

```

Azure Firewall のログを精査してみると、App Service が使用しているサブネットから以下のようなアクセスが遮断されていることが分かりました。
```
HTTPS request from 10.0.3.254:34500 to oryx-cdn.microsoft.io:443. Action: Deny. No rule matched. Proceeding with default action
```
![firewall2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/0f4e2271-033e-6a79-efd2-87783011cb3a.jpeg)


[oryx は App Service on Linux のビルドエンジン](https://github.com/microsoft/Oryx/blob/main/doc/hosts/appservice.md) ですが、ビルドエンジンの動作が妨げられたことが原因です。


# App Service, Kudu はどこに通信するのか？
先に挙げた結果の通り、App Service や Azure Functions は、アプリケーションの動作以外にも外部に対する通信を行っており、コードのデプロイ時にも外部に通信が行われます。
具体的には以下が挙げられます。
- *.scm.azurewebsites.net (Azure Functions に対してデプロイを行っている場合)
- oryx-cdn.microsoft.io (Linux で App Service ビルドを使用している、SCM_DO_BUILD_DURING_DEPLOYMENT が有効化されている)

Azure Firewall によって送信が制御された Azure Functions に対してコードのデプロイのデプロイが行われる場合、以下の流れで通信が行われます。
 1. クライアントからコードをアップロード (デプロイ)。
 2. 再起動するために Kudu から自分自身 (Kudu) の REST API を呼出。
 3. Azure Firewall を経由して Kudu に対して要求が行われる。
 4. 最終的な処理結果がクライアントに応答される。
![functions-with-firewall.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/70078ccb-7854-a0ef-6d82-09961986b519.jpeg)


上記の通り、Azure Functions に対してコードがデプロイされた後、自分自身 (*.scm.azurewebsites.net) に対して REST で要求を行いますが。
Firewall 等で通信がブロックされている場合、(3) の Firewall から Kudu に対する要求が拒否されるため、Kudu で要求を発行した REST API クライアントは例外を throw し、コードのデプロイを行ったクライアントに対してもエラーが応答され、デプロイは失敗と判断されます。
一方で、デプロイ操作での失敗は再起動のみとなるため、エラーを無視し、手動で再起動するのも選択肢の一つとしては挙げられます。


Linux において App Service ビルドを使用する場合、以下の流れで通信が行われます。
 1. クライアントからコードをアップロード (デプロイ)。
 2. Kudu にてビルドを開始。oryx やパッケージマネージャ (nuget, npm など) の通信のためにインターネットに要求を発行。
 3. Firewall を経由してインターネットと通信。
 4. 最終的な処理結果がクライアントに応答される。
![appservice-with-firewall.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/cb029b37-013e-4157-17ed-ce50dfb6579a.jpeg)


ビルドエンジンである Oryx は必要なパッケージ情報を取得するために oryx-cdn.microsoft.io に対してアクセスを試みます。これは [Oryx のドキュメントにも許可が必要である旨の記述があります](https://github.com/microsoft/Oryx/blob/main/doc/hosts/appservice.md#network-dependencies)。
また、ビルド時に nuget や npm などのパッケージマネージャを使用したライブラリのインストールを行う場合、それぞれのパッケージ取得に必要となる通信も発生します。
これらの通信が阻害される場合、Kudu 上で行われる App Service ビルドは成功しませんので、クライアントにはデプロイの失敗 (ビルドの失敗) として応答が行われます。


マルチテナント環境で動作する App Service, Azure Functions は、VNet 統合はサポートされていますが、基本的にはインターネットへの接続が自由に行えることが前提です。
外部への通信制御 (特に日本で一般的に使用されるような全ての通信をブロックし、許可された通信だけ行える) 自体は想定されていないため、ドキュメントにも詳細な記述はありません。
一方で、通信が VNet 内に閉じた構成とすることがある程度前提となる ASE の場合、ASE の構成要件の一部として[許可すべき通信先としてドキュメントに記載があります](https://learn.microsoft.com/ja-jp/azure/app-service/environment/networking)。

一般的に、こうした閉域網を作るシナリオでは、アプリケーションが動作しない場合、Firewall の動作などブロッキングしている箇所をユーザー自身で追跡する必要があります。
筆者の経験上でも、オンプレミスで動作させて上手く動作しない場合、Firewall や proxy などの確認と調整を行っています。
そのため、上手く動作しない場合は Azure Firewall で提供されている [ルールによる通信許可・拒否をログに記録する](https://learn.microsoft.com/ja-jp/azure/firewall/firewall-diagnostics#enable-diagnostic-logging-through-the-azure-portal) 機能を使用して、どのような通信が行われているか、ブロックされているかと言った観点で分析が必要です。


## App Service ビルドを行う場合に Firewall で通信を許可する必要がある通信先
実際にサンプルコードを作成し、ローカル Git に格納したコードを App Service 上でビルドした場合に許可が必要になった (Firewall がブロックした) 通信先を列挙します。
これらは App Service の仕様ではなく、各プログラミング言語におけるパッケージ管理システムが使用する代表的なアドレスの一つです。
以下は一例であり、使用するライブラリや独自のレポジトリを使用している場合は、個別に確認が必要です。

### C# (.NET 6) の場合
Windows 環境で実施。
- api.nuget.org

### PowerShell Core の場合
Windows 環境で実施。
- www.powershellgallery.com
- psg-prod-eastus.azureedge.net

### Node.js 16 (npm) の場合
Linux 環境で実施。
- registry.npmjs.org

### Java 11 (maven) の場合
Java は App Service ビルドは行われません。

### Python 3.10 (pip) の場合
Linux 環境で実施。
- pypi.org
- files.pythonhosted.org


# まとめ
Azure Firewall が統合された環境においての App Service と Azure Functions に対するコードのデプロイに失敗する理由を示しました。

クラウドに限らず、Firewall や proxy など、通信を制限する機能がネットワーク上で有効になっている場合、それらの機能により動作しないソフトウェアというのは無数に存在します。
通信が可能となるように暗黙的に設定されている場合もありますが、ドキュメントの記載通り動作しない場合は、まずは自分たちの使っているネットワークの観点で何か制限されていないかという観点で調べてみることで、問題を早期に解決できる可能性があります。