Azure のサーバーレスの代名詞の Azure Functions でリアルタイム監視してみよう

さて、個人的に気に入ってるというか手間も少なくそれっぽいデモとして気に入ってる Azure Functions から Cosmos DB にデータを登録して、CosmosDB Trigger で登録されたデータを確認して条件を満たしたら SignalR で接続しているクライアントにメッセージを投げて画面を更新するみたいなことをよくやってます。

こんな感じのイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/426da03e-ab85-4110-66af-f9c2009947aa.png)

- Azure Functions
    - Azure のサーバーレスの代名詞。C# や JavaScript などで開発した関数を実行できる。
- CosmosDB
    - Azure で一番強い NoSQL データベース
- SignalR Service
    - WebSocket で双方向にリアルタイムにメッセージをやり取りできる

ここでは、そのデモの作り方の手順を紹介しようと思います！あと Azure Functions と CosmosDB は割といい組み合わせでお気に入りなので是非触ってみてね！

## 下準備

### VS 2019 のインストール

Visual Studio 2019 を Azure 開発のワークロードを選んだ状態で入れます。VS Code でも出来るのですが今回使う予定の C# の場合は、あえて VS Code でやる理由もないので個人的には VS 2019 を使うのがお勧めです。色々強い。

https://visualstudio.microsoft.com/ja/

### リソースの作成

では、Azure 側の準備を行います。Cosmos DB と SignalR Service の作成を行います。

まず、Cosmos DB はこんな感じで。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/25132d9c-343d-7958-d7b2-e198ceaf7f16.png)

試しにサーバーレス選んでみました。

SignalR のほうも ServiceMode を Serverless にして作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/7bfb16b9-1e89-9731-f992-7fa0baee8e01.png)

## やってみよう

### データ登録処理

CosmosDB にデータを登録する処理を追加します。

Visual Studio から Azure Functions プロジェクトを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/466505ce-373c-10d7-3254-1510e570d2ce.png)

プロジェクト作成ウィザードで Http Trigger の関数も作成しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/723067ad-601b-3b59-939b-4af38fe93501.png)

プロジェクトが作成されたら `Microsoft.Azure.WebJobs.Extensions.CosmosDB` のパッケージを追加します。

では、Http Trigger に JSON が POST で送られてきたら CosmosDB に登録する処理を書きます。
登録するデータを表すクラスを定義して…

```csharp
// 以下の using が必要
// using Newtonsoft.Json;

public class SensorData
{
    [JsonProperty("value")]
    public int Value { get; set; }
    [JsonProperty("dateTime")]
    public DateTimeOffset DateTime { get; set; }
    [JsonProperty("sensor")]
    public Sensor Sensor { get; set; }
}

public class Sensor
{
    [JsonProperty("id")]
    public string Id { get; set; }
}
```

そして、プロジェクト作成時に生成されていた関数を以下のように書き換えます。

```csharp
public static class Function1
{
    [FunctionName("AddData")]
    public static async Task<IActionResult> AddData(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
        [CosmosDB(
            "azfundemoDb", 
            "sensors", 
            ConnectionStringSetting = "CosmosDb", 
            CreateIfNotExists = true, 
            Id = "id", 
            PartitionKey = "/sensor/id")] IAsyncCollector<SensorData> sensorData,
        ILogger log)
    {
        var inputs = JsonConvert.DeserializeObject<SensorData[]>(await req.ReadAsStringAsync());
        log.LogInformation("Received {0} items.", inputs.Length);
        foreach (var input in inputs)
        {
            await sensorData.AddAsync(input);
        }

        return new AcceptedResult();
    }
}
```

Azure ポータルから CosmosDB の接続文字列（ポータルで CosmosDB を選択してキーの部分を開くと書いてあります）をコピーします。
Azure Functions のプロジェクトの local.settings.json に CosmosDb という名前をキーにして接続文字列を追加します。

```json:local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "CosmosDb": "ここに接続文字列"
  }
}
```

デバッグ実行をすると、以下のように AddData 関数を叩くための URL が取得できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/de1b7536-a446-de56-0d97-8028691362d4.png)

Postman や VS Code の REST Client あたりを使って叩いてみましょう。私はちょっと REST API 叩くときは REST Client を使ってます。こんな感じのものを書くとリクエスト飛ばしてくれるので重宝してます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/602c532f-72be-68ca-ae23-6fb4b8cb976d.png)

CosmosDB のデータエクスプローラーを開くと、ちゃんとデータが入っていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/91536a29-91cc-6ee2-e5fe-183c1cff09ef.png)

### 通知処理を作ろう

では、登録されたデータのチェックを行う関数を追加しましょう。
とりあえず通知は置いておいて、異常なデータの検出だけを行います。

```csharp
[FunctionName("CheckValue")]
public static async Task CheckValue(
    [CosmosDBTrigger(
        "azfundemoDb", 
        "sensors", 
        ConnectionStringSetting = "CosmosDb", 
        CreateLeaseCollectionIfNotExists = true)] IReadOnlyList<Document> inputs,
    ILogger log)
{
    log.LogInformation("{0} items were added.", inputs.Count);
    var alertTargets = inputs.Select(x => (SensorData)(dynamic)x)
        .Where(x => x.Value >= 50);

    foreach (var alertTarget in alertTargets)
    {
        log.LogInformation("Detected an alert target value {0} at {1}.", alertTarget.Value, alertTarget.Sensor.Id);
    }
}
```

この状態でデバッグ実行を行うと、いくつかデータを追加していくとこんな感じのログが出ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/422cf702-9466-a80a-720d-be6a02980fb4.png)

とりあえず、今回は 50 以上が来たらダメって感じにしました。
異常なデータが取れたので SignalR Service 経由で通知する処理を書きましょう。

`Microsoft.Azure.WebJobs.Extensions.SignalRService` パッケージを追加して、クライアントが接続してきたときに接続の情報を返す negotiate 関数を作ります。

```csharp
[FunctionName("negotiate")]
public static SignalRConnectionInfo Negotiate(
    [HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req,
    [SignalRConnectionInfo(HubName = "alert")] SignalRConnectionInfo connectionInfo)
    => connectionInfo;
```

では、CheckValue 関数を書き換えます。ログを出していた部分を SignalR Service のメッセージを投げる形にします。

```csharp
[FunctionName("CheckValue")]
public static async Task CheckValue(
    [CosmosDBTrigger(
        "azfundemoDb", 
        "sensors", 
        ConnectionStringSetting = "CosmosDb", 
        CreateLeaseCollectionIfNotExists = true)] IReadOnlyList<Document> inputs,
    [SignalR(HubName = "alert")] IAsyncCollector<SignalRMessage> messages,
    ILogger log)
{
    log.LogInformation("{0} items were added.", inputs.Count);
    var alertTargets = inputs.Select(x => (SensorData)(dynamic)x)
        .Where(x => x.Value >= 50);

    await messages.AddAsync(new SignalRMessage
    {
        Target = "alert",
        Arguments = new[] { alertTargets }
    });
}
```

そして、Azure ポータルから SignalR Service の接続文字列を取得してきて local.settings.json に AzureSignalRConnectionString という名前で追加します。

```json:local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "CosmosDb": "CosmosDB の接続文字列",
    "AzureSignalRConnectionString": "Azure SignalR Service の接続文字列"
  }
}
```

これでサーバー側は一通り出来上がりました、あとはこの SignalR Service につなぐクライアントを適当に作ってしまえば完成です。
クライアントのコードについてはここには載せませんが、単純に繋いで alert メッセージを受け取ったらデータを表示するようなコードを書けば OK です。私は WPF アプリとして作りました。

以下のような感じで CosmosDB Trigger が動いて SignalR Service に出力されると WPF アプリのほうにデータが表示されます。

![test2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/11257c69-bec2-867a-fe9c-f57657824d9d.gif)

## ソースコード

特に整形とかはしてませんが、そのまま GitHub のほうにアップしておきました。

https://github.com/runceel/AzFunDemo

本来は `.gitignore` に入っている `local.settings.json` もリポジトリに入れているので CosmosDB と SignalR Service を作って接続文字列を置き換えたら動くと思います。注意点としては、CheckValue 関数を一旦コメントアウトして AddData 関数を実行して DB とコンテナを作ってもらうか手動で作らないと、多分エラーになります。

## まとめ

ということで、本番で使うにはエラー処理も異常値判定もお粗末な感じですが、Azure Functions 側のトータル行数 100 行以下で DB へのデータ登録、その中から異常値を判定して接続しているクライアントに通知という処理が書けるので、やろうと思えば通しでライブコーディングも出来るレベルです。

Function1.cs に全コードを追記していったのですが今見たら 86 行でした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/1fa2b3bc-f6e8-0427-c873-0c258c29be41.png)

今回はクライアント側が手抜きですが、それなりの見た目のクライアント側のアプリを作っておくと見た目的にもいい感じになります。
ということで、Azure Functions でトリガーやバインドがうまくハマると少ないコード量で色々出来ます。もちろん凝ったことしようとするとトリガーやバインドで賄えないケースも出てくるので、その場合は自分で書かないといけないので、ここまで少ないコード量になることはないですが、スタート地点として少ないコード量から始めることが出来る点と、簡単なことは簡単に書けるという点が Azure Functions で好きなポイントです。

あと、今回はローカル実行しかしていませんが、当然これを Azure の Function App にデプロイするとクラウドで動きます。

ということで、少しでもいいなって思ったらぜひ Azure Functions を試してみてください。毎月 100 万回のリクエストまで無料枠があります。

https://azure.microsoft.com/ja-jp/free/search/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/11a62edc-2f7e-86bb-d2bb-044a93c09947.png)