Event Grid を Azure Functions と EventGrid Extension 無しで連携させる

今回は EventGrid を Azure Functions から使いたいたかったので、EventGrid の SDK 無しで Azure Functionと連携させる方法をシェアしてみたいと思います。
![EventGrid.JPG](https://qiita-image-store.s3.amazonaws.com/0/3470/b868789a-e37a-4362-f3d6-b7ded5d26e4f.jpeg)


# 全体像

EventGrid が初めてという方は是非こちらをご覧ください。

* [Azure Event Grid の概要](https://docs.microsoft.com/ja-jp/azure/event-grid/overview)
* [Azure Functions の Event Grid トリガー](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-event-grid#use-an-http-trigger-as-an-event-grid-trigger)

全体の流れは単純で、EventGrid Topic があり、そこで、カスタムイベントを受け付けます。すると、そのトピックにサブスクライブしている EventGrid Subscription に対してメッセージが通知されます。Function B は、サブスクリプションから WebHookを受けて何らかの処理をするという感じです。Publish-Subscribe パターンでメッセージ送信をしてくれる基盤なのですがミリセカンドレベルのメッセージセンディングを目指しているのと、リトライなどもしてくれるので、信頼性が普通のHttpコールより高くなっています。これが、気軽に使えるのでいろいろ面白いことができそうです。一つのトピックに対して複数のサブスクリプションが対応しています。

```
Function A -> EventGrid Topic -> EventGrid Subscritpion -> Function B
```

実はこれに相当する機能は、[EventGrid Extension](https://github.com/Azure/azure-functions-eventgrid-extension) で実装済みなのですが、いくつか課題があります。

* GA 前であり、サポートされていないリージョンでは動作しない
* サポートされていないリージョンでは単に動作しないので、なぜ動かないのかがよくわからない場合がある（サポートに連絡する必要あり）
* 日本のリージョンはまだ

しかしSDKは単純にhttps で、WebHook になっています。

# Topic の作成

Azure CLI からも、Portal からも作れます。適当に名前を付けて、リージョンを選びましょう。ただし、日本にはまだ残念ながら来ていないので、選べるリージョンから選びましょう。

![Topic.JPG](https://qiita-image-store.s3.amazonaws.com/0/3470/87d37fae-cf90-12cd-3d55-1cb4450fcda9.jpeg)

# メッセージを受け付けるファンクションの作成。

単なる HttpTrigger になっています。（私は Azure Functions V2 で作成しています) 実際やっていることで一つだけポイントがあるとすると、`eventType` が `Microsoft.EventGrid.SubscriptionValidationEvent` のイベントに対して、北メッセージの、`["data"]["validationCode"]` を返却しています。このメッセージは、サブスクライバが Topic に登録したとき、また定期的に Topic がサブスクライバが生きているかをチェックするために使うものと思われます。実際にサブスクライブするタイミングで呼ばれていました。あとは、メッセージボディの中身をJson からパースして表示しているだけです。

```csharp
       [FunctionName("HttpTriggerFunc")]
        public static IActionResult HttpTriggerFunc(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequest req, TraceWriter log)
        {
            log.Info("C# HttpTrigger for EventGrid.");
            var requestBody = new StreamReader(req.Body).ReadToEnd();
            var messages = JsonConvert.DeserializeObject<JArray>(requestBody);
            // If the request is for subscription validation, send back the validation code.
            if (messages.Count > 0 && string.Equals((string)messages[0]["eventType"],
                "Microsoft.EventGrid.SubscriptionValidationEvent",
                System.StringComparison.OrdinalIgnoreCase))
            {
                log.Info("Validate request received");
                return new OkObjectResult(
                    new
                    {
                        validationResponse = messages[0]["data"]["validationCode"]
                    });
            }

            // The request is not for subscription validation, so it's for one or more events.
            foreach (JObject message in messages)
            {
                // Handle one event.
                EventGridEvent eventGridEvent = message.ToObject<EventGridEvent>();
                log.Info($"Subject: {eventGridEvent.Subject}");
                log.Info($"Time: {eventGridEvent.EventTime}");
                log.Info($"Event data: {eventGridEvent.Data.ToString()}");
            }

            return new OkObjectResult("");


        }
```

# Subscription の作成

さて、`Topic` と、受け付ける Function が出来たらFunctionの URL を用いて Subscription を作ることができます。エンドポイントの型はEventHub だとか、IoTHub だとかいろいろ選べますが、今は WebHook を選択します。実際には上記の`Topic` から作成できるし、Azure CLIからもできます。Full Endpoint URL のところに、上記の FunctionのURLを入れます。すると、Topic からメッセージが来たら、そのURLをフックするという感じです。ちなみに、 Prefix / Suffix Filter で受け取るメッセージをフィルタすることも可能です。空白の場合は、フィルタなしでメッセージが来たら常にURLにメッセージを送ります。

![Subscription.JPG](https://qiita-image-store.s3.amazonaws.com/0/3470/088b21c3-0855-3382-4345-81a667b6c670.jpeg)

# メッセージを送付する Function を作成する。

Event Grid のトピックにメッセージを送るのは簡単で、Https の POST で、`aeg-sas-key` に Topic で、Keyが取得できますが、それをそこにヘッダーとして渡して、メッセージボディに次の形態のjsonメッセージを送れば終わり。簡単ですね。

```
[ { "id": "21797", 
    "eventType": "recordInserted", 
    "subject": "myapp/vehicles/motorcycles", 
    "eventTime": "2018-04-01T22:43:31+0900", 
    "data":{ "make": "Ducati", "model": "Monster" }, 
    "dataVersion": "1.0" } ]
```

ちなみに、data のところは、Json Serializable なオブジェクトであれば何でもよいです。

コードで書くとこんな感じです。ちなみに、Azure Functions の中では、HttpClient はインスタンス化しないようにしましょう。HttpClientはスレッドセーフなので、static で保持します。実はこれはすごくパフォーマンスに影響しますので、イディオムとして覚えておきましょう。

```csharp
       static Function1() {
            client = new HttpClient();
            client.DefaultRequestHeaders.Add("aeg-sas-key", System.Environment.GetEnvironmentVariable("EventGridSasKey"));
        }

        public class OrchestratorEvent
        {
            public const string STARTED = "Started";
            public const string COMPLETED = "Completed";
            public string InstanceId { get; set; }
            public string EventType { get; set; }
            public DateTime EventTime { get; set; }
        }

        [FunctionName("Sender")]
        public static async Task<IActionResult> Sender([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequest req, TraceWriter log)
        {
            log.Info("Send message to the Topic!");
            var newId = Guid.NewGuid().ToString();
            var events = new EventGridEvent[]
            {
                new EventGridEvent
                {
                    Id = newId,
                    EventType = "orchestratorEvent",
                    Subject = $"durable/orchestrator/{newId}",
                    EventTime = new DateTime(2018, 3, 10, 10,10, 10,DateTimeKind.Local),
                    Data = new OrchestratorEvent
                    {
                        InstanceId = Guid.NewGuid().ToString(),
                        EventType = OrchestratorEvent.COMPLETED,
                        EventTime = new DateTime(2018, 3, 10, 10, 10, 10, DateTimeKind.Local)
                    },
                    DataVersion = "1.0"
                }
            };
            var resp = await client.PostAsJsonAsync<EventGridEvent[]>(System.Environment.GetEnvironmentVariable("EventGridTopicUrl"), events);

            return (ActionResult)new OkObjectResult($"Sucessfully sent");
        }

```

これは、POSTMAN などから、`Content-Type : application/json` と `aeg-sas-key` を渡して、ボディに先ほどの jsonを貼って送信しているのと変わりません。単純ですね。このFunctionにhttpリクエストを送ると、一番最初の画面のように、サブスクライブしたFunctionに最終的に通知されます。

# Resource 

* [Azure Event Grid の概要](https://docs.microsoft.com/ja-jp/azure/event-grid/overview)
* [Azure Functions の Event Grid トリガー](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-event-grid#use-an-http-trigger-as-an-event-grid-trigger)
* [Azure CLI と Event Grid を使用したカスタム イベントの作成とルーティング](https://docs.microsoft.com/ja-jp/azure/event-grid/custom-event-quickstart)
* [HttpClient を使って JSON データを POST する方法](https://blogs.msdn.microsoft.com/japan_platform_sdkwindows_sdk_support_team_blog/2013/12/09/httpclient-json-post/) 
* [今回のサンプル](https://github.com/TsuyoshiUshio/EventGridSpike/blob/master/EventGridSpike/Function1.cs)

NOTE 今回のサンプルは、ライブラリを入れていますが、該当部分では使っていません。ちなみに、EventGridEventはそのままなので、使っていますが、自分でも同じ型で書いて自分でシリアライズしても結果は同じです。