IoT Edge の Azure Functions を試して見た （２）

前回 [IoT Edge の Azure Functions を試して見た （１）](https://qiita.com/TsuyoshiUshio@github/items/cf5ffc1e0360b7115815) でモジュールのインストールまでを実施しました。次はエッジの上で動作する Azure Functions を開発して、その上で、アラートを Azure Functions と、Service Bus を使って検出するというところまでやって見ました。

# Edge で動作する Azure Functions を作成する

* [Deploy Azure Function as an IoT Edge module - preview](https://docs.microsoft.com/en-us/azure/iot-edge/tutorial-deploy-function) を参考に、Edge で動作する Azure Functions を作ります。前提条件として下記のことが揃っていること。

* Visual Studio Code
* C# for Visual Studio Code extension
* Azure IoT Edge extension for Visual Studio Code
* Docker
* .NET Core 2.0 SDK

ちなみに、Azure IoT Edge extension for Visual Studio Code はいい感じでログをみれたり、 Docker をデプロイできたりします。

さて始めましょう。

## Azure Container Registry のセットアップ

DokcerHub でもいいのですが、折角なのでプライベートリポジトリを試して見ます。Azure Container Registry のセットアップ自体はどこにでもあるので書きませんが、作成して、ホスト名、ユーザ名、パスワードを取得します。Edge の方で、プライベートリポジトリにあるバイナリを使いたい時は必要になります。

## Azure Function 作成

### テンプレートを作成する。

次のコマンドで、IoTEdge 用の Function をインストールします。

```
$ dotnet new -i Microsoft.Azure.IoT.Edge.Function
```

名前はなんでもいいのですが、`FilterFunction` という名前で作って見ましょう。

```
$ dotnet new aziotedgefunction -n FilterFunction
```

`FilterFunction` のディレクトリができて、その中に Edge 用の Azure Functions のフルセットが出来上がります。

<img width="425" alt="Screen Shot 2017-11-29 at 10.46.13 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/f71eaf12-c767-5b3a-21f2-ae5d3f27b991.png">

## Azure Function のコードを書く

残念ながらインテリセンスはありませんが、コードを書いて見ましょう。

_run.csx_

```csharp
#r "Microsoft.Azure.Devices.Client"
#r "Newtonsoft.Json"

using System.IO;
using Microsoft.Azure.Devices.Client;
using Newtonsoft.Json;

class MessageBody
{
    public Machine machine {get;set;}
    public Ambient ambient {get; set;}
    public string timeCreated {get; set;}
}
class Machine
{
   public double temperature {get; set;}
   public double pressure {get; set;}         
}
class Ambient
{
   public double temperature {get; set;}
   public int humidity {get; set;}         
}

public static async Task Run(Message messageReceived, IAsyncCollector<Message> output, TraceWriter log)
{
const int temperatureThreshold = 25;

byte[] messageBytes = messageReceived.GetBytes();
var messageString = System.Text.Encoding.UTF8.GetString(messageBytes);

if (!string.IsNullOrEmpty(messageString))
{
    // Get the body of the message and deserialize it
    var messageBody = JsonConvert.DeserializeObject<MessageBody>(messageString);

    if (messageBody != null && messageBody.machine.temperature > temperatureThreshold)
    {
        // We will send the message to the output as the temperature value is greater than the threashold
        var filteredMessage = new Message(messageBytes);
        // We need to copy the properties of the original message into the new Message object
        foreach (KeyValuePair<string, string> prop in messageReceived.Properties)
        {
            filteredMessage.Properties.Add(prop.Key, prop.Value);
        }
        // We are adding a new property to the message to indicate it is an alert
        filteredMessage.Properties.Add("MessageType", "Alert");
        // Send the message        
        await output.AddAsync(filteredMessage);
        log.Info("Received and transferred a message with temperature above the threshold");
    }
}

}
```

_function.json_

```
{
    "bindings": [
        {
            "type": "edgeHubTrigger",
            "name": "messageReceived",
            "InputName": "input1",
            "direction": "in"
        },
        {
            "type": "edgeHub",
            "name": "output",
            "outputName": "alertOutput",
            "batchSize": 10,
            "direction": "inout"
        }
    ]
}
```

ポイントは多くありませんがここです。Edge の Function は、Edgeからメッセージを受け取りって、最終的に、エッッジに吐きます。下記のロジックで、`Properties` に対して `MessageType` を足していますが、これは、メッセージのボディではなく、ヘッダとかメタ情報に近いものです。残りのファンクションの設定は、IoT Hub 側で行います。ここでは、閾値以上の気温の場合、アラートを出しています。この情報は、IoTHub に送信されるメッセージボディには現れません。このアラートは、IoTEdgeが管理します。

```csharp
        filteredMessage.Properties.Add("MessageType", "Alert");
        // Send the message        
        await output.AddAsync(filteredMessage);
```

# IoT Edge のビルドとプッシュ

IoT Edge Azure Functions は、Docker にパックして配布されますが、このビルドと、プッシュが Visual Stdio Code のエクステンションでサポートされいますので、注意点含めて流して見ます。

## docker image の作成

Dockerfile が複数ありますが自分のEdge端末の種類に合わせてDockerfileを選択して右クリックします。そして、`Build IoT Edge module Docker image` を選びます。



<img width="545" alt="Screen Shot 2017-11-29 at 11.05.15 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/76c847af-59d6-a80e-e89a-4adbf3d49ba1.png">

次がポイントですが、次の画面で、`Select folder as EXE_DIR` ボタンを押します。ポイントはこの指定しているディレクトリが、`host.json` のあるディレクトリになっていることです。このディレクトリにあるファイルの内容が、docker image に取り込まれます。

<img width="714" alt="Screen Shot 2017-11-29 at 11.05.32 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/b55084db-0f41-a8b4-48eb-c6b37b584a23.png">

次に、この画面になりますので、ここに、Azure Container Registry に格納するイメージ名を書きます。基本的に、`{Azure Container Registry host name}/{function name}:{version}` になっています。サンプルだと、`latest` を指定するように書いていますが、バージョン番号にすることをお勧めします。（あとでアップデートやロールバックが楽なので）

<img width="736" alt="Screen Shot 2017-11-29 at 11.06.17 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/201fd001-10e7-49fc-47e2-49ea5f7de601.png">

エンターを押すと、あとは、下記のコマンドが、VSCode の 内臓ターミナルで実行されます。ディレクトリが同じであれば、このコマンドを直接叩いても構いません。


```
$ docker build -f "/Users/ushio/Codes/IoTEdge/functions/FilterFunction/Docker/linux-x64/Dockerfile" --build-arg EXE_DIR="." -t "iotregistry.azurecr.io/filterfunction:1.0.6" "/Users/ushio/Codes/IoTEdge/functions/FilterFunction"
Sending build context to Docker daemon  17.41kB
Step 1/4 : FROM microsoft/azureiotedge-functions-binding:1.0-preview
 ---> d75b891c5691
Step 2/4 : ENV AzureWebJobsScriptRoot /home/site/wwwroot
 ---> Using cache
 ---> 04456026fc09
Step 3/4 : ARG EXE_DIR=.
 ---> Using cache
 ---> 3492a0c63b1f
Step 4/4 : COPY $EXE_DIR/ /home/site/wwwroot
 ---> aad55f1f1e93
Removing intermediate container c6f361bad218
```
## Docker images のプッシュ

`Command + Shift + P` を選択して次の画面にします。`Edge: Push IoT Edge module Docker image` を選びます。

<img width="702" alt="Screen Shot 2017-11-29 at 11.16.01 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/501165d5-0a61-93c8-6047-cd29c1547b32.png">

イメージの作成と同じイメージ名を設定してエンターすると、リポジトリにイメージがプッシュされます。

<img width="727" alt="Screen Shot 2017-11-29 at 11.17.37 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/63a542f6-6801-18f6-2ead-79b7f4165674.png">

## Azure Functions を実行したらエラーになる場合は？

下記のエラーが出たら、先ほどの、`EXE_DIR` が間違っている、つまり`host.json` のあるディレクトリじゃないところでDocker のビルドをしているため、Azure Functions のファイルが十分コンテナの中に格納されていない感じですので、そこを疑ってください。

* [[V2][Preview] Unable to send data to the IoT Hub using Azure Function Module #425](https://github.com/Azure/iot-edge/issues/425)

```
No job functions found. Try making your job classes and methods public. If you're using binding extensions (e.g. ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. config.UseServiceBus(), config.UseTimers(), etc.).
```

# IoT Hub　から Functions をデプロイする

さて、作成したFunctions は IoT Hub からデプロイします。
デバイスを選択します。

<img width="1159" alt="Screen Shot 2017-11-29 at 11.41.42 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/e107c3b9-0f3e-43e6-1fcd-452453fbf513.png">

Set Modules でモジュールをデプロイしたり、変更したりします。

<img width="494" alt="Screen Shot 2017-11-29 at 11.42.00 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/b96d2b26-9433-ef96-ff0b-27ee08b09953.png">

私の環境では、すでに、filterfunction をデプロイしていますが、皆様の場合ないはず。`Add IoT Edge Module` を選択します。

<img width="872" alt="Screen Shot 2017-11-29 at 11.42.17 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/99b8e767-70bd-6e0a-4513-e956e0c47e39.png">

名前をつけます。`fileterfunction` のような感じでつけます。それと先ほど作成した docker image を指定してセーブ

<img width="674" alt="Screen Shot 2017-11-29 at 11.43.04 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/19ace320-107f-da11-f5fa-16de1daf9455.png">

次の画面で、モジュールのルーティングを設定します。`tempSensor` という前回デプロイしたダミーデータを生成するモジュールのアウトプットを、filterfunction に入れて、filterfunctionのアウトプットを、Upstream つまり、IoTHub に送るという意味合いになっています。

<img width="926" alt="Screen Shot 2017-11-29 at 11.43.23 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/756bda8e-a5e5-e6b7-1d29-edee056a3724.png">

これでデプロイしてデバイスの画面に、戻ってインストールが完了してたらOKです。

<img width="1167" alt="Screen Shot 2017-11-29 at 11.50.42 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/c0223a69-7b20-d406-7fe3-0c1ecc30863c.png">

これで、データがやってきているはずですが`docker logs ` で直接コンテナのログを見たり、Azure IoT Hub 側に来たデータを見て見てもどこにも、`MessageType: Alert` を指定したものが見当たりません。なぜかというと、それは、メッセージの Body に含まれる情報ではなく、IoTEdge に送られたメタ情報だからです。じゃあ、それをどうやって見ましょうか？ CSharp の DLL のモジュールを作ったり、VSCode のIoTEdge extensionで見ることはできますが、どうしたらいいでしょうか？

# Alert を取得するために、IoT Edge のルートと Service Bus + Functions を設定する。

最初に単独で、Service Bus を作っておきましょう。Queue も作っておきます。Storage Queue との違いは、持てるデータが256K に対して、512k まで持てるとかの違い（だったと思います。）

<img width="1135" alt="Screen Shot 2017-11-29 at 11.54.44 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/3b8d0ffd-52ff-d2ab-075e-2fa94d7d460c.png">

次に、IoT Hub に戻って、Endpoint を指定します。

<img width="902" alt="Screen Shot 2017-11-29 at 11.57.26 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/d0cb75b7-722e-be6c-0170-02284d08e2f1.png">

Route を指定します。

<img width="1133" alt="Screen Shot 2017-11-29 at 11.57.47 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/afcf7fe1-9f25-24f1-e0aa-71d51cb72cae.png">

こうなるためには、 Add で次の項目を入力します。

ポイントは、Query string　のところで、Azure Functions ないで指定した `MessageType`を指定してフィルタをかけます。こうすると、メッセージの中で、`MessageType`が`Alert` のもののみ指定したEndpoint(つまり先ほどの Service Bus)にメッセージが Queue に格納されて投げられます。

<img width="468" alt="Screen Shot 2017-11-29 at 11.57.56 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/060ac4be-a78b-ba25-3687-422f4c635c55.png">

# Azure Functions で　アラートの上がったメッセージを取得する

最終確認で、先ほどの、Service Bus に投げられたQueue を Service Bus Queue トリガーの Azure Functions を作成して、検出します。

_run.csx_

```csharp
using System;
using System.Threading.Tasks;

public static void Run(string myQueueItem, TraceWriter log)
{
    log.Info($"C# ServiceBus queue trigger function processed message: {myQueueItem}");
}

```

_function.json_

```
{
  "bindings": [
    {
      "name": "myQueueItem",
      "type": "serviceBusTrigger",
      "direction": "in",
      "queueName": "temperature",
      "connection": "iotedge_RootManageSharedAccessKey_SERVICEBUS",
      "accessRights": "Manage"
    }
  ],
  "disabled": false
}
```
バッチリアラートが上がった情報のみをゲットできました。

<img width="1098" alt="Screen Shot 2017-11-29 at 2.19.25 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/d3e33a4a-6049-eb4c-65ef-ade6a01fc13a.png">

# 次回

本当はこの後、トリガーのデータが多すぎるので、Stream Analytics でメッセージの数を減らしたのちに、メール通知することにしました。Stream Analytics -> Azure Functions -> SendGrid という構成ですが、今日はもう力尽きたのでこの辺で。

# Resource

* [Deploy Azure Function as an IoT Edge module - preview](https://docs.microsoft.com/en-us/azure/iot-edge/tutorial-deploy-function)
* [Process IoT Hub device-to-cloud messages using routes (.NET)](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-csharp-csharp-process-d2c)
* [Understand and use device twins in IoT Hub](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-device-twins)
* [Understand the requirements and tools for developing IoT Edge modules - preview](https://docs.microsoft.com/en-us/azure/iot-edge/module-development)