Azure API Management から直接 Azure Cosmos DB の REST API をリクエストしてみた

## 背景と目的

Cosmos DB にアクセスする API アプリを App Service や Functions で用意しおき、その API を API Management から使用するのが一般的だと思います。でもよくよく考えると Cosmos DB にはデータプレーン用の REST API が備わっており JSON がレスポンスされます。だったら、API Management から直接 Cosmos DB と連携した方が汎用的に使い回せて良いのではないかと思い、実際に試してみました。

## 前提条件

検証コマンドの実施環境は、Mac + Azure CLI です。

```zsh:zsh
% sw_vers
ProductName:    macOS
ProductVersion: 11.4
BuildVersion:   20F71

% az version
{
  "azure-cli": "2.25.0",
  "azure-cli-core": "2.25.0",
  "azure-cli-telemetry": "1.0.6",
  "extensions": {}
}
```

## 実施内容

### リソースグループと Cosmos DB を作成

```zsh:zsh
# 環境変数とセットします
region=japaneast
prefix=mnrapimcdb

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

# Cosmos DB を作成します
az cosmosdb create \
  --name ${prefix}-cdb \
  --resource-group ${prefix}-rg
```

### Cosmos DB のサンプル Python アプリで動作確認

```zsh:zsh
git clone https://github.com/Azure-Samples/azure-cosmos-db-python-getting-started.git

cd azure-cosmos-db-python-getting-started/

cdbendpoint=$(az cosmosdb show \
  --name ${prefix}-cdb \
  --resource-group ${prefix}-rg \
  --query documentEndpoint \
  --output tsv)

cdbprimarykey=$(az cosmosdb keys list \
  --name ${prefix}-cdb \
  --resource-group ${prefix}-rg \
  --query primaryMasterKey \
  --output tsv)

sed -ie "s#\"endpoint\"#\"$cdbendpoint\"#" cosmos_get_started.py

sed -ie "s#\'primary_key'#'$cdbprimarykey'#" cosmos_get_started.py

python3 -m venv .venv

. .venv/bin/activate

pip install azure-cosmos

python cosmos_get_started.py

deactivate

cd ..
```

### API Management を作成

```zsh:zsh
#  API Management を作成します
az apim create \
  --name ${prefix}-apim \
  --resource-group ${prefix}-rg \
  --sku-name Consumption \
  --publisher-email email@mydomain.com \
  --publisher-name Microsoft

# Blank API を作成します
az apim api create \
  --service-name ${prefix}-apim \
  --resource-group ${prefix}-rg \
  --api-id MyApi \
  --path '/myapi' \
  --display-name 'My API'

# API 操作を作成します
az apim api operation create \
  --service-name ${prefix}-apim \
  --resource-group ${prefix}-rg \
  --api-id MyApi \
  --url-template "/getitem/{container}/{lastname}" \
  --method "GET" \
  --display-name GetContainerItem \
  --template-parameters name=container required="true" type="string" \
  --template-parameters name=lastname required="true" type="string"

# 名前付きの値を作成します：cdbendpoint
az apim nv create \
  --service-name ${prefix}-apim \
  --resource-group ${prefix}-rg \
  --named-value-id cdbendpoint \
  --display-name 'CosmosDB-Endpoint' \
  --value $cdbendpoint

# 名前付きの値を作成します：cosmosdbkey
az apim nv create \
  --service-name ${prefix}-apim \
  --resource-group ${prefix}-rg \
  --named-value-id cosmosdbkey \
  --display-name 'CosmosDB-Key' \
  --value $cdbprimarykey
```

### 作成した MyApi の Policy を登録

下記の XML を管理ポータル上で MyApi の Policy に登録する。

```xml:xml
<policies>
    <inbound>
        <base />
        <set-variable name="requestDateString" value="@(DateTime.UtcNow.ToString("r"))" />
        <set-variable name="container" value="@(context.Request.MatchedParameters["container"])" />
        <set-variable name="lastname" value="@(context.Request.MatchedParameters["lastname"])" />
        <send-request mode="new" response-variable-name="response" timeout="10" ignore-error="false">
            <set-url>@("{{CosmosDB-Endpoint}}/dbs/AzureSampleFamilyDatabase/colls/" + (string)context.Variables["container"] + "/docs")</set-url>
            <set-method>POST</set-method>
            <set-header name="Authorization" exists-action="override">
                <value>@{
                    var verb = "post";
                    var resourceType = "docs";
                    var resourceLink = "dbs/AzureSampleFamilyDatabase/colls/" + (string)context.Variables["container"];
                    var key = "{{CosmosDB-Key}}";
                    var keyType = "master";
                    var tokenVersion = "1.0";
                    var date = context.Variables.GetValueOrDefault<string>("requestDateString");
                    var hmacSha256 = new System.Security.Cryptography.HMACSHA256 { Key = Convert.FromBase64String(key) };  

                    verb = verb ?? "";  
                    resourceType = resourceType ?? "";
                    resourceLink = resourceLink ?? "";
                    string payLoad = string.Format("{0}\n{1}\n{2}\n{3}\n{4}\n",  
                            verb.ToLowerInvariant(),  
                            resourceType.ToLowerInvariant(),  
                            resourceLink,  
                            date.ToLowerInvariant(),  
                            ""  
                    );  
                    byte[] hashPayLoad = hmacSha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payLoad));  
                    string signature = Convert.ToBase64String(hashPayLoad);  

                    return System.Uri.EscapeDataString(String.Format("type={0}&ver={1}&sig={2}",  
                        keyType,  
                        tokenVersion,  
                        signature));
                }</value>
            </set-header>
            <set-header name="Content-Type" exists-action="override">
                <value>application/query+json</value>
            </set-header>
            <set-header name="x-ms-documentdb-isquery" exists-action="override">
                <value>True</value>
            </set-header>
            <set-header name="x-ms-documentdb-query-enablecrosspartition" exists-action="override">
                <value>True</value>
            </set-header>
            <set-header name="x-ms-date" exists-action="override">
                <value>@(context.Variables.GetValueOrDefault<string>("requestDateString"))</value>
            </set-header>
            <set-header name="x-ms-version" exists-action="override">
                <value>2018-12-31</value>
            </set-header>
            <set-header name="x-ms-query-enable-crosspartition" exists-action="override">
                <value>true</value>
            </set-header>
            <set-body>@("{\"query\": \"SELECT * FROM c WHERE c.lastName = @lastName\", " +
          "\"parameters\": [{ \"name\": \"@lastName\", \"value\": \"" + (string)context.Variables["lastname"] + "\"}]}")</set-body>
        </send-request>
        <return-response response-variable-name="existing response variable">
            <set-status code="200" reason="OK" />
            <set-header name="Content-Type" exists-action="override">
                <value>application/json</value>
            </set-header>
            <set-body>@(new JObject(new JProperty("response",((IResponse)context.Variables["response"]).Body.As<JObject>())).ToString())</set-body>
        </return-response>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
```

## 実施結果

API Management 経由で Cosmos DB の JSON を取得できるか試してみます。

```zsh:zsh
# API Management のシークレットキーを取得します
apimkey=$(az rest \
  --method post \
  --uri "https://management.azure.com/subscriptions/$subid/resourceGroups/${prefix}-rg/providers/Microsoft.ApiManagement/service/${prefix}-apim/subscriptions/master/listSecrets?api-version=2019-12-01" \
  --query primaryKey \
  --output tsv)

# FamilyContainer の lastName = Smith をリクエストします 
curl -s https://${prefix}-apim.azure-api.net/myapi/getitem/FamilyContainer/Smith \
  -H "Ocp-Apim-Subscription-Key: $apimkey"

# lastName = Smith の Item を取得できました
{
  "response": {
    "_rid": "c8Y7AKAgb44=",
    "Documents": [
      {
        "id": "Smith_0961f8ce-93a9-41ea-b610-1cf0b3dfb496",
        "lastName": "Smith",
        "parents": null,
        "children": null,
        "address": {
          "state": "WA",
          "city": "Redmond"
        },
        "registered": true,
        "_rid": "c8Y7AKAgb44CAAAAAAAAAA==",
        "_self": "dbs/c8Y7AA==/colls/c8Y7AKAgb44=/docs/c8Y7AKAgb44CAAAAAAAAAA==/",
        "_etag": "\"020033b8-0000-2300-0000-60def6660000\"",
        "_attachments": "attachments/",
        "_ts": 1625224806
      }
    ],
    "_count": 1
  }
}

# FamilyContainer の lastName = Andersen をリクエストします 
curl -s https://${prefix}-apim.azure-api.net/myapi/getitem/FamilyContainer/Andersen \
  -H "Ocp-Apim-Subscription-Key: $apimkey"

# lastName = Andersen の Item を取得できました
{
  "response": {
    "_rid": "c8Y7AKAgb44=",
    "Documents": [
      {
        "id": "Andersen_5feb3d2c-f103-43f9-925f-8a97995e2aff",
        "lastName": "Andersen",
        "district": "WA5",
        "parents": [
          {
            "familyName": null,
            "firstName": "Thomas"
          },
          {
            "familyName": null,
            "firstName": "Mary Kay"
          }
        ],
        "children": null,
        "address": {
          "state": "WA",
          "county": "King",
          "city": "Seattle"
        },
        "registered": true,
        "_rid": "c8Y7AKAgb44BAAAAAAAAAA==",
        "_self": "dbs/c8Y7AA==/colls/c8Y7AKAgb44=/docs/c8Y7AKAgb44BAAAAAAAAAA==/",
        "_etag": "\"020032b8-0000-2300-0000-60def6660000\"",
        "_attachments": "attachments/",
        "_ts": 1625224806
      }
    ],
    "_count": 1
  }
}
```

## 参考

検証が済んだら後片付け。

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

[データベースを取得するREST API例](https://docs.microsoft.com/ja-jp/rest/api/cosmos-db/get-a-database#example)

[Authorization headerの作成例](https://docs.microsoft.com/ja-jp/rest/api/cosmos-db/access-control-on-cosmosdb-resources#authorization-header)

[Api Policy - Create Or Update](https://docs.microsoft.com/ja-jp/rest/api/apimanagement/2019-12-01/api-policy/create-or-update)

[Use bash, Azure CLI and REST API to access CosmosDB - how to get token and hash right?](https://stackoverflow.com/questions/61426902/use-bash-azure-cli-and-rest-api-to-access-cosmosdb-how-to-get-token-and-hash)