Azure Functions × Cosmos DBで作るAPIサーバ


# はじめに

Azureのサーバーレス[Azure Functions](https://azure.microsoft.com/ja-jp/services/functions/)とNoSQLストレージ[Cosmos DB](https://azure.microsoft.com/ja-jp/services/cosmos-db/)でAPIサーバを構築しました。

作ったもの → https://github.com/momotaro98/yarana-api

本記事ではその実装内容を記載します。

# 対象者

パブリッククラウドとしてAzureを採用している(しようとしている)人
APIサーバをサーバレスで構築しようとしている人

# 実装

APIを作っていきます。

Azure Functionsの実装では今回はC#のcsxスクリプトを採用しています。

本記事ではAzure Portal上でのAzure Functions構築方法は記載しません。以前に書いた[Azure FunctionsでTable StorageをCRUDするまで手順](https://qiita.com/momotaro98/items/84fb1ba832f0d4f9f80b)という記事に記述しているので是非見てみてください。

## 本記事で扱うデータモデル&エンドポイント

以下のデータモデルを本記事では例として扱います。

```koto.json
{
    "id":"mhlv43098tbsu7bgsc4voku398qx1410",
    "userId":"Ufea0979d178607eb0483c4f9cmhd3d25",
    "title":"筋トレ"
}
```

ID、ユーザーID、タイトル名の3項目あります。

また、以下の2つのエンドポイントを作ります。

```
POST
/koto
GET
/kotos?userId=string
```

## データを登録する `POST /koto`

上記のデータモデルのJSONがPOSTでやってきた場合の処理です。Cosmos DBへデータを格納します。

### Azure Functions上のソースコード

```run.csx
#r "Newtonsoft.Json"

using System.Net;
using Microsoft.Azure.WebJobs.Host;
using Newtonsoft.Json.Linq;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, IAsyncCollector<dynamic> kotoDocument, TraceWriter log)
{
    dynamic data = await req.Content.ReadAsAsync<object>();
    dynamic koto = JObject.Parse(data.ToString());
    kotoDocument.AddAsync(koto);
    return req.CreateResponse(HttpStatusCode.OK);
}
```

```function.json
{
    "bindings": [
      {
        "authLevel": "function",
        "name": "req",
        "type": "httpTrigger",
        "direction": "in",
        "methods": [
          "post"
        ],
        "route": "koto"
      },
      {
        "name": "$return",
        "type": "http",
        "direction": "out"
      },
      {
        "type": "documentDB",
        "name": "kotoDocument",
        "databaseName": "Yarana",
        "collectionName": "Kotos",
        "createIfNotExists": false,
        "connection": "yarana_DOCUMENTDB",
        "direction": "out"
      }
    ],
    "disabled": false
  }
```

コードはたった4行で済みます。
Cosmos DBのコレクションと紐付いた`IAsyncCollector<dynamic>`型を利用することでPOSTでやって来るJSONをそのまま`.AddAsync`メソッドでCosmos DBへ格納することができます。

### Cosmos DBに登録されたデータを確認

![スクリーンショット_2018-04-01_16_52_35.png](https://qiita-image-store.s3.amazonaws.com/0/91562/c6ca87c5-572f-5515-7c1b-af44ed945439.png)

ドキュメントが追加されています。

## データを取得する `GET /kotos?userId=string`

上記で作成したモデルを含むデータを取得します。ここではユーザーIDをURLの`userId`クエリで指定し、そのユーザーIDを持つドキュメントを返す機能を実装します。

### Azure Functions上のソースコード

```run.csx
#r "Newtonsoft.Json"

using System.Net;
using Newtonsoft.Json;
using System.Net.Http.Headers;

public static HttpResponseMessage Run(HttpRequestMessage req, IEnumerable<dynamic> documents, TraceWriter log)
{
    log.Info("C# HTTP trigger function processed a request.");

    // Parse url query parameter
    // ex. https://yarana-api.azurewebsites.net/api/kotos?userId=d59964bb713fd6f4f5ef6a7c7e029388
    string userId = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "userId", true) == 0)
        .Value;
    if (userId == null)
        return req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a userId on the query string or in the request body");
    log.Info($"userId: {userId.ToString()}");

    // Query from documents in CosmosDB
    IEnumerable<dynamic> docs = documents.Where(doc => doc.userId == userId);

    // Create JSON to return
    string responseJSON = JsonConvert.SerializeObject(docs);
    log.Info($"JSON to response: {responseJSON}");

    // Create response
    var response = req.CreateResponse(HttpStatusCode.OK);
    response.Content = new StringContent(responseJSON);
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
    response.Content.Headers.ContentType.CharSet = "utf-8";
    return response;
}
```

```function.json
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "name": "req",
      "type": "httpTrigger",
      "direction": "in",
      "methods": [
        "get"
      ],
      "route": "kotos"
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    },
    {
      "type": "documentDB",
      "name": "documents",
      "databaseName": "Yarana",
      "collectionName": "Kotos",
      "connection": "yarana_DOCUMENTDB",
      "direction": "in"
    }
  ],
  "disabled": false
}
```

Cosmos DBからデータ取得する場合はコレクションと紐付いた`IEnumerable<dynamic>`型を利用することで`documents.Where(doc => doc.userId == userId)`のようにC#の文法でクエリできます。

### GETで得られるJSONを確認

上記FunctionのURLへGETすると以下のようにデータが取得されます。

```result.json
[
    {
        "id":"mhlv43098tbsu7bgsc4voku398qx1410",
        "userId":"Ufea0979d178607eb0483c4f9cmhd3d25",
        "title":"筋トレ","_rid":"a4EkALKyCQAUAAAAAAAAAA==",
        "_self":"dbs/a4EkAA==/colls/a4EkALKyCQA=/docs/a4EkALKyCQAUAAAAAAAAAA==/",
        "_etag":"\"00006b00-0000-0000-0000-5aad29c30000\"",
        "_attachments":"attachments/",
        "_ts":1521297859
    },
    {
        "id":"l0wklkhyozgzg5m3095lmk6b8mf3tua6",
        "userId":"Ufea0979d178607eb0483c4f9cmhd3d25",
        "title":"英語","_rid":"a4EkALKyCQAVAAAAAAAAAA==",
        "_self":"dbs/a4EkAA==/colls/a4EkALKyCQA=/docs/a4EkALKyCQAVAAAAAAAAAA==/",
        "_etag":"\"00006c00-0000-0000-0000-5aad33a50000\"",
        "_attachments":"attachments/",
        "_ts":1521300389
    }
]
```

`_self`, `_etag`, `_attachments`, `_ts`は、Cosmos DBが管理するメタデータです。上記のコードではこれらが含まれてしまいます。
これらを含めないようにするには、返したい要素のみを持つエンティティ(クラス)を定義し、そのエンティティのJSONを返すような実装にする必要があります。

# 【おまけ】 Swaggerが使えるAzure FunctionsのAPI Definition

Azure FunctionsではAPI Definitionという機能が提供されており、Swagger規格でAPIドキュメントを管理できます。

以下スクショ

![スクリーンショット 2018-04-01 16.07.29.png](https://qiita-image-store.s3.amazonaws.com/0/91562/d8160d23-b750-1d36-2c9a-d203d8254c68.png)

Swagger UIが表示されます。

# おわりに

サーバーレス技術はシンプルな機能を持つAPIを構築する場合、開発&運用のコストを抑える意味で力を発揮します。
また、NoSQLストレージを利用すればJSONをそのまま入れ込み、取得できるコードを書ける環境が整っているので実装が楽ちんです。

# 参考

* https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-cosmosdb