Azure API ManagementポリシーでREST APIを呼び出す

## はじめに
Azure API Managementのポリシー内では、独自のAPIを呼び出すことが可能です。

そのため、API Managementのポリシーだけではまかなえない、様々なカスタム処理をアプリへのアクセス前にさせることができます。

今回は、[前回の記事](https://qiita.com/AsukaSeki/items/8f861472263df2b2e27b)で作成したポリシーを拡張し、「携帯端末でPC向けアプリにアクセスした際にモバイル向けアプリにリダイレクトさせる」といった例で、
API Managementで独自APIを呼び出し、その結果に応じてリダイレクトさせる方法ご紹介します。

今回ご紹介する例を図にするとこのようになります。

![スライド2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398865/66cabfcc-40cf-e559-6878-eb7d64b15749.png)

## 前提
* 前回の記事同様、下記を前提としています。
    * PC向けアプリ/モバイル向けアプリは、それぞれApp Services等に既にデプロイ済みとします。
    * API Managementでは、PC向けアプリ/モバイル用2つのAPIがあるとします。
        * 本記事ではそれぞれの設定は下記の通りとします。
            * PC向けアプリ
                * Base URL: `https://<API Managementドメイン>`
                * API URL suffix：なし

                  ![settings1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398865/cc2274aa-384c-859b-35de-c7f2240af186.png)

            * モバイル向けアプリ
                * Base URL: `https://<API Managementドメイン>/v2`
                * API URL suffix：v2

                  ![settings2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398865/22811c29-87f4-aeca-7766-aca36be14029.png)


## 独自APIの内容
API Managementで独自APIを呼び出し方の前に、まずは呼び出し先となる独自APIのリクエスト/レスポンスとサンプルコードをご紹介します。


### リクエスト例
Request Bodyの内容は下記の通りです。API Managementにアクセスした端末のUserAgentを受け取ります。

```json
{
    "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
}
```

### レスポンス例
レスポンスでは、受け取ったUserAgentをもとにリダイレクトをするかどうかの結果を返します。

```json
{
    "isRedirect": true
}
```

### サンプルコード
前述のリクエスト内容で受け取ったUserAgentから、モバイル端末（iPhone, Android）かどうかを判定し、モバイルの場合はリダイレクトするよう結果を返します。

下記はAzure FunctionのHTTP Triggerを利用した場合のサンプルコードです。

Azure Functionのランタイムバージョンはv4、フレームワークは.NET6としています。

```csharp
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.IO;
using System.Threading.Tasks;

namespace ApiManagementFunction
{
    public static class ApiManagementFunction
    {
        /// <summary>
        /// UserAgentからリダイレクトさせるかを判定する
        /// </summary>
        /// <param name="req"></param>
        /// <param name="log"></param>
        /// <returns></returns>
        [FunctionName("IsRedirect")]
        public static async Task<ActionResult<IsRedirectResponseViewModel>> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);

            var userAgent = ((string)(data?.userAgent)).ToLower();

            var vm = new IsRedirectResponseViewModel
            {
                IsRedirect = false
            };

            // UserAgentからモバイル端末かどうかを判定する
            if (userAgent.IndexOf("iphone") > -1 || (userAgent.IndexOf("android") > -1 && userAgent.IndexOf("mobile") > -1))
            {
                // モバイル端末であれば、リダイレクトさせる
                vm.IsRedirect = true;
            }

            log.LogInformation($"User Agent = {userAgent}, IsRedirect = {vm.IsRedirect}");

            return new OkObjectResult(vm);
        }
    }

    /// <summary>
    /// APIレスポンス用ViewModel
    /// </summary>
    public class IsRedirectResponseViewModel
    {
        /// <summary>
        /// リダイレクトするかどうか
        /// </summary>
        public bool IsRedirect { get; set; }
    }
}
```

## API Managementポリシー設定
PCアプリ側のGETオペレーションのInbound processingポリシーに下記の要素を追加します。

Azure PortalのUIではこちらを選択するとポリシーの編集画面になります。

![1.PNG](/.attachments/1-7c83bf48-24fb-4fbf-893b-02b974534af2.PNG)

```xml
<policies>
    <inbound>
        <base />
        <!-- 追加 -->
        <!-- 1. 独自APIの呼び出し  -->
        <send-request mode="new" response-variable-name="response">
            <!-- API URL -->
            <set-url>https://{your app}.azurewebsites.net/api/IsRedirect</set-url>
            <set-method>POST</set-method>
            <!-- API Request Header -->
            <set-header name="Content-Type" exists-action="override">
                <value>application/json</value>
            </set-header>
            <set-header name="x-functions-key" exists-action="override">
                <value>{your function key}</value>
            </set-header>
            <!-- API Request Body -->
            <set-body>@{
                string userAgent = context.Request.Headers.GetValueOrDefault("User-Agent","");
                return new JObject(
                    new JProperty("userAgent", userAgent)
                ).ToString();
            }</set-body>
        </send-request>
        <!-- 2. APIのレスポンスをオブジェクト型の変数に定義 -->
        <set-variable name="apiResult" value="@(((IResponse)context.Variables["response"]).Body.As<JObject>())" />
        <!-- 3. APIの結果から判定 -->
        <choose>
            <when condition="@((bool)((JObject)context.Variables["apiResult"]).Property("isRedirect"))">
                <!-- 4. リダイレクト -->
                <return-response>
                    <set-status code="303" reason="Redirecting" />
                    <set-header name="Location" exists-action="override">
                        <value>@{
                            return "/v2";
                        }</value>
                    </set-header>
                </return-response>
            </when>
            <otherwise />
        </choose>
        <!-- /追加 -->
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
```

## 解説
ポリシーの中でも、今回追加した箇所をピックアップします。

少しコードが複雑になっているので、それぞれ何をしているかの概要のみ説明します。詳細については各項目の参考ドキュメントをご参照ください。

```xml
<!-- 1. 独自APIの呼び出し  -->
<send-request mode="new" response-variable-name="response">
    <!-- API URL -->
    <set-url>https://{your app}.azurewebsites.net/api/IsRedirect</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
    <set-header name="x-functions-key" exists-action="override">
        <value>{your function key}</value>
    </set-header>
    <!-- API Request Body -->
    <set-body>@{
        string userAgent = context.Request.Headers.GetValueOrDefault("User-Agent","");
        return new JObject(
            new JProperty("userAgent", userAgent)
        ).ToString();
    }</set-body>
</send-request>
<!-- 2. APIのレスポンスをオブジェクト型の変数に定義 -->
<set-variable name="apiResult" value="@(((IResponse)context.Variables["response"]).Body.As<JObject>())" />
<!-- 3. APIの結果から判定 -->
<choose>
    <when condition="@((bool)((JObject)context.Variables["apiResult"]).Property("isRedirect"))">
        <!-- 4. リダイレクト -->
        <return-response>
            <set-status code="303" reason="Redirecting" />
            <set-header name="Location" exists-action="override">
                <value>@{
                    return "/v2";
                }</value>
            </set-header>
        </return-response>
    </when>
    <otherwise />
</choose>
```

### 1. 独自APIの呼び出し
ポリシーの`send-request`の部分では、独自APIの定義を行います。

```xml
<!-- 1. 独自APIの呼び出し  -->
<send-request mode="new" response-variable-name="response">
    <!-- API URL -->
    <set-url>https://{your app}.azurewebsites.net/api/IsRedirect</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
    <!-- API Key -->
    <set-header name="x-functions-key" exists-action="override">
        <value>{your function key}</value>
    </set-header>
    <!-- API Request Body -->
    <set-body>@{
        string userAgent = context.Request.Headers.GetValueOrDefault("User-Agent","");
        return new JObject(
            new JProperty("userAgent", userAgent)
        ).ToString();
    }</set-body>
</send-request>
```
主な定義内容は下記の通りです。

* `set-url`：独自APIのURL
* `set-method`：独自APIを呼び出すHTTPメソッド（GET, POSTなど）
* `set-header`：独自APIのリクエストヘッダー
    * 上記の例では、`Content-Type`および`x-functions-key`（Azure Functionsのキー）を設定しています
* `set-body`：Request Body
    * 上記の例では、リクエストに含まれるUserAgentの情報をBodyに設定しています

参考：https://learn.microsoft.com/ja-jp/azure/api-management/send-request-policy

### 2. APIのレスポンスをオブジェクト型の変数に定義
ポリシーの`set-variable`の部分では、変数を定義します。

```xml
<!-- 2. APIのレスポンスをオブジェクト型の変数に定義 -->
<set-variable name="apiResult" value="@(((IResponse)context.Variables["response"]).Body.As<JObject>())" />
```

カッコが多く少しわかりづらいですが、上記の例では受け取ったレスポンスをオブジェクト型の`apiResult`という名前の変数に代入しています。

参考：https://learn.microsoft.com/ja-jp/azure/api-management/set-variable-policy

### 3. APIの結果から判定
ポリシーの`choose`の部分では、ある条件に一致した場合のみ処理を行うよう制御します。C#やJavaのif-else文のようなものです。

```xml
<!-- 3. APIの結果から判定 -->
<choose>
    <when condition="@((bool)((JObject)context.Variables["apiResult"]).Property("isRedirect"))">
        <!-- 条件に一致した場合の処理 -->
    </when>
    <otherwise>
        <!-- 条件に一致しなかった場合の処理 -->
    </otherwise>
</choose>
```

主な定義内容は下記の通りです。

* `when`：`condition`に条件式を記載し、`when`タグ内部ではその条件に一致した場合の処理を実行します。
    * 上記の例では、APIから受け取ったレスポンスの`isRedirect`がtrueの場合はwhen内部を実行しています。
* `otherwise`：`when`で定義した条件に一致しなかった場合の処理を実行します。

参考：https://learn.microsoft.com/ja-jp/azure/api-management/choose-policy

### 4. リダイレクト
`return-response`では、API Managementからのレスポンスを定義します。

```xml
<!-- 4. リダイレクト -->
<return-response>
    <set-status code="303" reason="Redirecting" />
    <set-header name="Location" exists-action="override">
        <value>@{
            return "/v2";
        }</value>
    </set-header>
</return-response>
```

今回は、モバイル向けアプリのパスへのリダイレクト処理を行っています。

こちらについては、前回の記事をご参照ください。

## 動作確認
API Managementで変更を適用後、PC向けアプリへのURLにアクセスしてみましょう。

* URL例：`https://<API Managementサービス名>.azure-api.net/`

まずPCでアクセスしてみると、URLはそのまま変更されずにPC向けアプリにアクセスしています。

![1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398865/50d43589-32da-3d0c-9b8d-208ea320d7b6.png)

次にモバイル端末でアクセスしてみると、モバイル向けURLにリダイレクトできていることが確認できます。（キャプチャはChrome開発者ツールのモバイルエミュレーター機能を利用したものになります。）

ブラウザの開発者ツールでネットワークログを確認すると、下記のログが確認できます。

前回の記事同様に、リダイレクトされていますね。

![2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398865/57b2d66b-6942-c01f-d6c4-2475fbd8f4fe.png)


## まとめ
今回はPCアプリ⇒モバイルアプリのAPIでのみリダイレクト対応しましたが、独自APIを改修することで、逆のパターンものリダイレクト処理もできます。

また今回は割愛しましたが、独自APIでDB等の他リソースと連携させることにより様々なカスタマイズもできます。

## 関連記事
* [Azure API Managementポリシーを使ったリダイレクト](https://qiita.com/AsukaSeki/items/8f861472263df2b2e27b)

## 参考
* [API Management ポリシー リファレンス](https://learn.microsoft.com/ja-jp/azure/api-management/api-management-policies)
    * [要求の送信](https://learn.microsoft.com/ja-jp/azure/api-management/send-request-policy)
    * [変数を設定する](https://learn.microsoft.com/ja-jp/azure/api-management/set-variable-policy)
    * [制御フロー](https://learn.microsoft.com/ja-jp/azure/api-management/choose-policy)
    * [応答を返す](https://learn.microsoft.com/ja-jp/azure/api-management/return-response-policy)