ApplicationGatewayとAppServiceを統合する際に調べたこと・ハマったこと

Application Gateway と App Service を組み合わせて動かしたことはありますが、接続元の IP が取れるかどうか、App Service の組み込み認証を使った場合の挙動を試したことがなかったため、改めて調べました。

---

## 要約

https://learn.microsoft.com/ja-jp/azure/app-service/overview-app-gateway-integration#considerations-for-using-service-endpoints

単に接続するだけであれば、以下を行えばよいです。

- **Application Gateway**
  - Subnet のサービスエンドポイント (`Microsoft.Web`) を有効化します
- **App Service**
  - VNET Integration (VNET 統合) を有効化します
  - アクセス制限で Application Gateway のサブネットを許可します

また、App Service の組み込み認証を使う場合、App Service の既定のドメインを使うかカスタムドメイン（独自ドメイン）を使うかでやることが大きく変わるので注意が必要です。

なお、App Service にプライベートエンドポイント接続を使う構成を見かけることがありますが、単純に Application Gateway の裏に App Service を配置したいだけで他に特殊な要件がない限りサービスエンドポイント接続で充分です。

---

## App Service を既定のドメインで運用するか、カスタムドメインで運用するか

https://learn.microsoft.com/ja-jp/azure/application-gateway/configure-web-app?tabs=customdomain%2Cazure-portal

App Service 自体は決められたドメインでないとアクセスを受け付けない仕様です。そのため、カスタムドメイン（独自ドメイン）を追加して運用するか、既定のドメイン (`***.azurewebsites.net`) で運用するかを決める必要があります。

この選択は、Application Gateway のバックエンドプール設定やプローブの設定にも影響を与えます。Host ヘッダのオーバーライドを行うか否かが変わるためです。既定のドメインを使う場合は Host ヘッダ のオーバーライドが必要になり、カスタムドメインの場合は不要です。

App Service は外部に直接公開されない形になるため、「わざわざカスタムドメインを使わなくてもいいのでは」と思うかもしれません。しかし、App Service の組み込み認証を使う場合、ドメインの違いが表面化するので注意が必要です。App Service 側でドメイン認証した後に Application Gateway に転送するようにDNSの作業を行うのは少し違和感があるかもしれませんが、仕方がありません。

---

## Application GatewayからAppServiceへの通信にサービスエンドポイント接続を使うかプライベートエンドポイント接続を使うか

https://learn.microsoft.com/ja-jp/azure/app-service/overview-app-gateway-integration#integration-with-app-service

ドキュメントでいずれかを選択する形に見えますが、通常Application GatewayからAppServiceの通信はサービスエンドポイント接続で充分です。特に今回のケースはApplication Gatewayからの通信だけでよいはずなので、わざわざプライベートエンドポイント接続を使って、時間による課金と帯域による課金を発生させる必要はありません。

ただ、以下のように特殊な事情もあるかもしれません。プライベートエンドポイント接続を使えることも頭に入れておくと良いでしょう。

- オンプレミスからApplication GatewayをバイパスしてAppServiceにアクセスさせたい要件がある
- VNET内の通信はプライベートIPでなければならないポリシーがある
- 上記の要件が後から出てきて追加のコストが発生すると色々厳しいのでとりあえず多めに盛っておきたい

---

## Application Gateway の裏に App Service を配置し、かつ、既定のドメイン、かつ、App Service の組み込み認証を使う場合にうまくいかない

少し古いですが、次の記事が詳しいです。  

https://azure.github.io/AppService/2021/03/26/Secure-resilient-site-with-custom-domain.html#alter-authentication-settings-1

事象としては、以下のようなものがあります。

- **（事象1）認証のリダイレクトで既定のドメインにリダイレクトしてしまい、Application Gatewayを迂回してしまう。**  
  - App Service の前にプロキシがあることを教える設定を行う
- **（事象2）リダイレクト時に以下のようなエラーが発生する**  
  ```
  AADSTS50011: The redirect URI 'https://{独自ドメイン}/.auth/login/aad/callback' specified in the request does not match the redirect URIs configured for the application 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX'. 
  Make sure the redirect URI sent in the request matches one added to your application in the Azure portal. 
  Navigate to https://aka.ms/redirectUriMismatchError to learn more about how to fix this.
  ```
  - [上記のエラー文に含まれるリンク先](https://learn.microsoft.com/ja-jp/troubleshoot/entra/entra-id/app-integration/error-code-AADSTS50011-redirect-uri-mismatch)を参考に、Authentication Provider を追加した際の Microsoft Entra アプリのリダイレクト URL に独自ドメインのコールバックが設定されているかを見直してください。

---

### （事象1）の対処: App Service の前にプロキシがあることを教える設定を行う

App Service の既定ドメインを使って Application Gateway から接続する場合、Host ヘッダを既定のドメインに書き換えてしまうため、リダイレクトが正しく行われないという事象が発生します。  
そのため、App Service に「リバースプロキシの裏で動作している」ことを認識させ、どの Host ヘッダを見るべきかを指定する必要があります。  
[Application Gateway では `X-Original-Host` ヘッダ](https://learn.microsoft.com/ja-jp/azure/application-gateway/how-application-gateway-works#modifications-to-the-request) にアクセス時の Host 情報が記録されるので、これを利用します。

図で理解したい場合は、ドキュメントの「[正しくないリダイレクト URL](https://learn.microsoft.com/ja-jp/azure/architecture/best-practices/host-name-preservation#incorrect-redirect-urls)」を参照すると分かりやすいです。


#### 対応手順（Azure Resource Explorer）

Azure CLIで行うこともできますが、少し複雑なのでAzure Resource Explorerから行うのも手でしょう。

https://azure.github.io/jpazpaas/2022/03/09/Application-Gateway-front-of-App-Service-auth.html


#### 対応手順（Azure CLI の例）

[Azure 公式ドキュメント](https://learn.microsoft.com/ja-jp/azure/app-service/overview-app-gateway-integration#authentication)にも手順が書かれていますが、少し抽象的なので、以下に CLI 例を示します。

```bash
# Azure CLIの拡張機能 authV2 を追加
az extension add --name authV2

# az webapp auth が v2(authsettingsv2)に置き換わり、v1はauth-classicに置き換わります
RESOURCE_GROUP=リソースグループ名
WEBAPP_NAME=WebApp名

# 現在の設定を取得 (properties 部分を抜粋)
az webapp auth show --resource-group "$RESOURCE_GROUP" --name "$WEBAPP_NAME" --query properties > auth.json

# auth.json を編集して、httpSettings.forwardProxy を置き換えます
code auth.json
```

`auth.json` 内の `httpSettings.forwardProxy` を以下のように編集します。

```json
  "httpSettings": {
    "forwardProxy":{
      "convention": "Custom",
      "customHostHeaderName": "X-Original-Host"
    }
  }
```

編集が終わったら、設定を流し直します。

```bash
az webapp auth set --resource-group "$RESOURCE_GROUP" --name "$WEBAPP_NAME" --body @auth.json

# 応答の httpSettings が書き換わっていれば成功です
# 失敗すると IdentityProvider の設定が消えることがあるので注意してください
# 動作確認ができたら、auth.json は削除して問題ありません
# rm auth.json
```


#### 対応手順（terraformの例）

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_web_app#forward_proxy_convention-2

`azurerm_linux_web_app`の`auth_settings_v2`で可能です。

```tf
resource "azurerm_linux_web_app" "example" {
  // ...
  auth_settings_v2 {
    forward_proxy_convention              = "Custom"
    forward_proxy_custom_host_header_name = "X-Original-Host"
  }
  // ...
}
```

### （事象2）の対処: アプリのリダイレクト URL の見直しと設定

[エラー文に含まれるリンク先](https://learn.microsoft.com/ja-jp/troubleshoot/entra/entra-id/app-integration/error-code-AADSTS50011-redirect-uri-mismatch)に書かれているとおりに対応すれば、基本的には解決します。

App Service で Microsoft をプロバイダとした認証を設定すると、デフォルトでは既定ドメインのコールバック URL のみがリダイレクト URL として登録されます。そのため、独自ドメイン経由で遷移した際に不一致が起こります。

#### 対応手順（Azure CLI の例）

```bash
APP_ID="組み込みの認証時に払い出されるApp(Client)ID"
URLS="https://{your-webapp-name}.azurewebsites.net/.auth/login/aad/callback https://{custom-domain-name}/.auth/login/aad/callback"

# --web-redirect-uris オプションで登録
az ad app update --id "$APP_ID" --web-redirect-uris $URLS

# 確認
az ad app show --id "$APP_ID"
```

:::note info
過去には `--reply-urls` が使われていましたが、現在は `--public-client-redirect-uris` と `--web-redirect-uris` に分かれています  （[参考リンク](https://github.com/Azure/azure-cli/issues/23193#issuecomment-1184086738)）
また、両方指定すると、私の環境によっては片方が抜け落ちてしまったので、`--web-redirect-uris` のみにするとよいです。
:::


上記例では、App Service の既定ドメインと独自ドメインの2つを登録していますが、実際に使うドメインだけでも問題ありません。

---

## Application Gateway と App Service の VNET 統合、および Web アプリケーションから見た Remote Host の見え方の確認

Web アプリケーション側で、Remote Host（UA、つまりアプリ利用者の IP）を取得したい場合の挙動を事前に調査しておくことをおすすめします。

具体例として、Spring Boot (tomcat) を使用した場合の動きを挙げます。

- Spring Boot では `server.forward-headers-strategy=NATIVE` かつ `X-Forwarded-For` は特定の IP 範囲である必要があります  
  - なお、Spring Boot には [App Service 専用の検出ロジック](https://github.com/spring-projects/spring-boot/blob/3.4.x/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/cloud/CloudPlatform.java#L155-L163) があり、条件を満たすと `server.forward-headers-strategy=NATIVE` がデフォルトとなります  
  - （App Service 独自の環境変数が存在するかどうかを見ているだけなので、環境変数を追加すれば再現できます）

- `server.forward-headers-strategy=NATIVE` の挙動については [公式ドキュメント](https://docs.spring.io/spring-boot/how-to/webserver.html#howto.webserver.use-behind-a-proxy-server) に記載がありますが、プロキシの IP 範囲については明記されていません  
- プロキシの IP 範囲はサーブレットの実装に依存するようです。たとえば [Tomcat 10.0 の RemoteIpValve](https://tomcat.apache.org/tomcat-10.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html) では、一般的な IPv4 のプライベートアドレスと IPv6 のローカルホスト (::1/128) のみが許可されています


### ふるまい検証結果

Tomcat の IP 範囲設定や App Service 側の IP 制限をどうするかによって、見える IP が異なるケースがありました。  
IP 制限をしない場合、なぜか IPv6 のアドレスが見えることがあり（IPv6 ULA なのと、サブネットに IPv6 を有効化していないので、おそらく App Service の内部 LB か何かからのアドレスと推測しています）。

VNET 統合、Service Endpoint、IP 制限の設定それぞれを変えて挙動を比較した結果は以下のとおりです（意味のない組み合わせは除外しています）。

| App Service              | AppGwのServiceEndpoint | App Service アクセスログの Cip             | WebApp の x-client-ip                                | Webアプリから見た RemoteHost               |
| ------------------------ | ---------------------- | ------------------------------------------ | ----------------------------------------------------- | ------------------------------------------ |
| 非 VNET 統合             | 無効                   | (Application Gateway のパブリックIP)       | `[fde4:8dba:1800:f61d:6e18:400:xxxx:xxxx]`            | `(Application Gateway のパブリックIP):1025` |
| 非 VNET 統合             | 有効                   | `[fde4:8dba:1800:f61d:6e18:400:xxxx:xxxx]` | `[fde4:8dba:1800:f61d:6e18:400:xxxx:xxxx]`            | `[fde4:8dba:1800:f61d:6e18:400:xxxx:xxxx]:34178` |
| VNET 統合                | 有効                   | `[fde4:8dba:1800:f61d:6e18:400:xxxx:xxxx]` | `[fde4:8dba:1800:f61d:6e18:400:xxxx:xxxx]:58714`      | `[fde4:8dba:1800:f61d:6e18:400:xxxx:xxxx]:58714` |
| VNET 統合 + IP 制限(Subnet) | 有効                   | `[fde4:8dba:1800:f61d:6e18:400:xxxx:xxxx]` | `10.x.x.4` (Application Gateway のサブネット内プライベートIP) | X-Forwarded-For の IP:port (UA のグローバルIP(IPv4)) |

この検証では、VNET 統合 + IP 制限 (subnet) + Service Endpoint 有効時だけが、期待どおりの Remote Host を取得できました。これは、アプリケーションが認識しているプロキシの IP 範囲が、IP 制限をすることで所望の送信元になったためと考えられます。

なお、Remote Host なのにポート番号が付与されているのは、Application Gateway のふるまいです。[ポートを除去するためのドキュメント](https://learn.microsoft.com/ja-jp/azure/application-gateway/rewrite-http-headers-url#remove-port-information-from-the-x-forwarded-for-header)があるので、必要に応じて参考にしてください。

また、`x-client-ip` ヘッダにポート番号が付いたり付かなかったりする場合があります。元々独自ヘッダであり、あまり使い道がないかもしれませんが、挙動が変わる点に注意しましょう。

`x-forwarded-for` ヘッダについては、通常どおり Application Gateway が付与してくれるため、送信元を信頼できると判断するなら独自に解析してもかまいません。

通常、Application Gateway を前段に置く場合、App Service への直接アクセスを禁止するために IP 制限をしないという選択肢はありません。しかし、IP 制限するかしないかで挙動が変わるという点は覚えておく必要があります。（この挙動についてのドキュメントは見当たりませんでした）

---

## メモ: Application Gateway 経由時、組み込み認証を追加した場合に増えるヘッダ(2025/01確認時)

- **Application Gateway 経由時に付与されるヘッダ**
  - `x-client-ip`, `client-ip`
  - `x-forwarded-port`
  - `x-forwarded-for` （送信元のIP:Port）
  - `x-original-host` （Application Gateway が受けたときの Host ）
  - `x-appgw-trace-id`
- **AppService 組み込み認証有効時に付与されるヘッダ**
  - `x-ms-token-aad-expires-on`
  - `x-ms-token-aad-access-token`
  - `x-ms-token-aad-id-token`
  - `x-ms-client-principal`
  - `x-ms-client-principal-id`
  - `x-ms-client-principal-idp`
  - `x-ms-client-principal-name`
  - `traceparent`