Azure API Management でワイルドカードオペレーションを試してみた

## 背景と目的

Azure API Management のバックエンド API に機能を追加したら、当然 Azure API Management 側でも OpenAPI の定義を再インポートするとか、別バーションとして定義するとか、組織やチームの管理方針に従って対応していく事になると思います。今回はワイルドカードオペレーションを使って、バックエンド API に機能が追加されても Azure API Management 側では対応不要な設定を試してみました。

## APIM 検証環境を作る

```bash:bash
# 環境変数をセットします
region=japaneast
prefix=mnrapimwild

# リソースグループを作成します
az group create \
  --name ${prefix}-rg \
  --location $region

# Azure API Management を作成します
az apim create \
  --name ${prefix} \
  --resource-group ${prefix}-rg \
  --sku-name Consumption \
  --publisher-email $(az account show --query user.name --output tsv) \
  --publisher-name ${prefix}
```

## 比較元のバックエンド API をインポート

```bash:bash
# チュートリアルにあるマイクロソフト提供のバックエンド API をインポートします
az apim api import \
  --resource-group ${prefix}-rg \
  --service-name ${prefix} \
  --path conference \
  --specification-url https://conferenceapi.azurewebsites.net?format=json \
  --specification-format OpenApi

# API のサブスクリプションキーを取得します
apimkey=$(az rest \
  --method post \
  --uri "https://management.azure.com/subscriptions/$(az account show --query id --output tsv)/resourceGroups/${prefix}-rg/providers/Microsoft.ApiManagement/service/${prefix}/subscriptions/master/listSecrets?api-version=2019-12-01" \
  --query primaryKey \
  --output tsv)

# JSON がレスポンスされるか動作確認します
curl -s https://${prefix}.azure-api.net/conference/topics \
  -H "Ocp-Apim-Subscription-Key: $apimkey"
```

## インポートした API の一覧を表示

```bash:bash
# バックエンド API の ID を取得します
apiid=$(az apim api list \
  --resource-group ${prefix}-rg \
  --service-name ${prefix} \
  --query "[0].name" \
  --output tsv)

# API オペレーションの一覧を表示します
az apim api operation list \
  --resource-group ${prefix}-rg \
  --service-name ${prefix} \
  --api-id $apiid \
  --output table
```

API オペレーションの一覧を確認すると GET オペレーションが複数あることがわかります。

```bash:bash
Description                                                                              DisplayName         Method    Name                ResourceGroup    UrlTemplate
---------------------------------------------------------------------------------------  ------------------  --------  ------------------  ---------------  ----------------------
Retreive a representation of a single session by Id                                      GetSession          GET       GetSession          mnrapimwild-rg   /session/{id}
A list of sessions.  Optional parameters work as filters to reduce the listed sessions.  GetSessions         GET       GetSessions         mnrapimwild-rg   /sessions
A list of topics covered by a particular session                                         GetSessionTopics    GET       GetSessionTopics    mnrapimwild-rg   /session/{id}/topics
                                                                                         GetSpeaker          GET       GetSpeaker          mnrapimwild-rg   /speaker/{id}
                                                                                         GetSpeakers         GET       GetSpeakers         mnrapimwild-rg   /speakers
                                                                                         GetSpeakerSessions  GET       GetSpeakerSessions  mnrapimwild-rg   /speaker/{id}/sessions
                                                                                         GetSpeakerTopics    GET       GetSpeakerTopics    mnrapimwild-rg   /speaker/{id}/topics
                                                                                         GetTopic            GET       GetTopic            mnrapimwild-rg   /topic/{id}
                                                                                         GetTopics           GET       GetTopics           mnrapimwild-rg   /topics
                                                                                         GetTopicSessions    GET       GetTopicSessions    mnrapimwild-rg   /topic/{id}/sessions
                                                                                         GetTopicSpeakers    GET       GetTopicSpeakers    mnrapimwild-rg   /topic/{id}/speakers
Retreive a representation of a single session by Id                                      SubmitSession       POST      SubmitSession       mnrapimwild-rg   /session/{id}/feedback
```

## ワイルドカードオペレーション用の API を登録

```bash:bash
# チュートリアルと同じバックエンドで別の API を Azure API Management に作成します
az apim api create \
  --service-name ${prefix} \
  --resource-group ${prefix}-rg \
  --api-id wildcardapi \
  --path wildcardapi \
  --display-name wildcardapi \
  --service-url https://conferenceapi.azurewebsites.net

# GET 用のワイルドカードオペレーションを登録します
az apim api operation create \
  --resource-group ${prefix}-rg \
  --service-name ${prefix} \
  --api-id wildcardapi \
  --url-template "/*" \
  --method "GET" \
  --display-name WildcardGet
```

## ワイルドカードオペレーションの動作確認

複数の GET オペレーションを試してみます。JSON がレスポンスされるか動作確認します。

```bash:bash
curl -s https://${prefix}.azure-api.net/wildcardapi/topics \
  -H "Ocp-Apim-Subscription-Key: $apimkey"

curl -s https://${prefix}.azure-api.net/wildcardapi/sessions \
  -H "Ocp-Apim-Subscription-Key: $apimkey"

curl -s https://${prefix}.azure-api.net/wildcardapi/speakers \
  -H "Ocp-Apim-Subscription-Key: $apimkey"

curl -s https://${prefix}.azure-api.net/wildcardapi/topic/1/sessions \
  -H "Ocp-Apim-Subscription-Key: $apimkey"
```

OpenAPI をインポートしたサンプルでは、1 機能 1 オペレーションでしたが、ワイルドカードオペレーションでは、複数機能が 1 オペレーションで実現できることがわかりました。

## 検証環境の削除

```bash:bash
# リソースグループを削除します
az group delete \
  --name ${prefix}-rg \
  --yes

# Azure API Management の論理的に削除されたインスタンスを消去する
az rest \
  --method delete \
  --url "https://management.azure.com/subscriptions/$(az account show --query id --output tsv)/providers/Microsoft.ApiManagement/locations/$region/deletedservices/$prefix?api-version=2021-08-01"
```

## 参考

https://docs.microsoft.com/azure/api-management/add-api-manually#add-and-test-a-wildcard-operation

https://docs.microsoft.com/ja-jp/azure/api-management/import-and-publish

https://docs.microsoft.com/ja-jp/cli/azure/apim?view=azure-cli-latest

https://docs.microsoft.com/ja-jp/azure/api-management/soft-delete#purge-a-soft-deleted-instance