Azure Event Grid でAzure サブスクリプションの管理イベントを抽象化しつつSlackへ通知してみる

この記事は、Microsoft Azure Advent Calendar 2018 の 23 日目の記事です。

はじめに

今回 Azure Event Grid を主体として考えることがなかったので、考えてみました。
2018/12/22日時点の情報を元に記事を作成しました。
2018/12/23 誤字訂正

Azure Event Grid って?

一言で言ってしまえば、メッセージフォーマットの決められた Webhook のマネージドなサービスです。
サーバーを意識することもないため、サーバーレスアーキテクチャの一部にできます。再送信機能、再送信失敗後のStorageへの保存機能を有しています。

Azure Event Grid とは
https://docs.microsoft.com/ja-jp/azure/event-grid/overview

Event Grid によるメッセージの配信と再試行
https://docs.microsoft.com/ja-jp/azure/event-grid/delivery-and-retry

Azure にはメッセージングサービスとしていくつもあります。このEvent Grid の特徴は状態変更で発生し、通知する点を特化しているため、常駐させたプロセスやサーバーで大きく口を開けて待ち受けて処理することではなく。必要に応じてリソースを使うイベント駆動である点が、イベントベースで実装されるモダンなアプリ開発に向けてという、サーバーレスアーキテクチャの一部とできる点で、そのために開発されたものというのが公開時のBlogアナウンスで上がっていました。

何かあったから、何か処理するという単純な考え方ですね。

公式Blog記事

2017/8/17 Public Preview
Public preview: Azure Event Grid
https://azure.microsoft.com/en-us/updates/public-preview-azure-event-grid/
Introducing Azure Event Grid – an event service for modern applications
https://azure.microsoft.com/ja-jp/blog/introducing-azure-event-grid-an-event-service-for-modern-applications/

2018/1/18 GA
Announcing the general availability of Azure Event Grid
https://azure.microsoft.com/ja-jp/blog/announcing-the-general-availability-of-azure-event-grid/

2018/5/7 CloudEvents サポート Public Preview
Cloudベンダー共通フォーマットに対応
Better integrations and higher productivity with Azure Event Grid
https://azure.microsoft.com/ja-jp/blog/better-integrations-and-higher-productivity-with-azure-event-grid/

2018/7/24 配信に失敗したイベントをBlobに書き込む機能
Improving the development experience worldwide with Event Grid
https://azure.microsoft.com/ja-jp/blog/event-grid-june-updates-dead-lettering-retry-policies-global-availability-and-more/

2018/10/31 Event Domains 機能
https://azure.microsoft.com/ja-jp/blog/deliver-the-right-events-to-the-right-places-with-event-domains/

アーキテクチャ上のメリット

サーバーレスアーキテクチャで全体を構築するときに複雑化した場合や、どこかで接続を切り離すということを考えた時、このEvent Grid で区切りを作ることは、外部通知とか、外部接続を抽象化されたレイヤーを一つ作ることができるメリットがあるんじゃないかなと思います。

例えば、応答を求めないバックグラウンドのタスク・・・データを蓄積させることなど。蓄積させるという部分をどのような形が今後考えるものであるとしたら、このEvent Grid に通知を流すでとどめることができる。それは別のチームが作る。

運用向けの拡張として定義し、受け取り可能な状態にする。その後、ハンドラーとしてリソースのスケーリング、担当への通知を実装いくなど。

まだPreviewではあるものの、CloudEvents の発展により異なるクラウドベンダーが同じフォーマットで通信できるようになるため外部サービスへの連携も。

概念と仕組み

Event Grid の要素は、「Azure Event Grid とは 」にあるように、イベントソース、トピック、イベントサブスクリプション、イベントハンドラーに分かれています。
送信元を主語として、日本語で入出力を表現するとこんな感じでしょうか?
入力:イベントソースは、イベントが発生すると、イベントグリッドのトピックにイベントを送信する。
出力:イベントグリッドは、イベントを受け取ると イベントハンドラ のイベントサブスクリプションにイベントを送信する。

Azure Event Grid の イベントソース

Logic Apps や Functions になじみのある方なら、Trigger(トリガー)という表現が近いと思います。何かのタイミングで発生するイベントの元がイベントソースとなっているようです。
例えば、Azure のサブスクリプションでリソースを作ろうとした、変更しようとした、削除しようとしたという操作をイベントソースとして受け取り利用者が何かをするためにつなげる(後述するイベントハンドラー)役目を担います。

  • Azure Subscription の管理操作
  • Azure Blob Storage
  • Azure Container Registry
  • Event Hubs
  • Azure IoT Hub
  • Azure Media Service
  • Azure Resource Group の管理操作
  • Service Bus
  • 独自のメッセージ(Event Grid のURLに Webhook)

Azure Event Grid のイベント ソース
https://docs.microsoft.com/ja-jp/azure/event-grid/event-sources

Azure Event Grid の イベント ハンドラー

イベントの送信先、イベントを受け取る先で、何かする役目を持つものを指してます。
連携可能なサービスは

  • Azure Automation
  • Azure Functions
  • Event Hubs
  • Logic Apps
  • Queue Storage
  • ハイブリッド接続(オンプレミス接続)
  • WebHook(カスタム)

Azure Event Grid のイベント ハンドラー
https://docs.microsoft.com/ja-jp/azure/event-grid/event-handlers

イベントのスキーマ

送信されるイベントのスキーマ、JSONの定義
https://docs.microsoft.com/ja-jp/azure/event-grid/event-schema

[
  {
    "topic": string,
    "subject": string,
    "id": string,
    "eventType": string,
    "eventTime": string,
    "data":{
      object-unique-to-each-publisher
    },
    "dataVersion": string,
    "metadataVersion": string
  }
]

上記が共通のもので、data の中に入るものが、イベントソースによって異なってきます。

イベントのフィルタリング(Event Type)

イベントグリッドが受け取ったイベントのイベントサブスクリプションごとにフィルターをかけることができます。
以下は、「イベントソース:Azure サブスクリプション」のものです。

イベントのフィルタリング(Subject)

subject はイベントの概要で、Storageのイベントでは、ファイルの拡張子によるフィルターリングを行えます。
Azure Event Grid の Blob Storage 用のイベント スキーマ
https://docs.microsoft.com/ja-jp/azure/event-grid/event-schema-blob-storage
スキーマの定義にあるように subject には作成または削除されたファイルのファイルパスが含まれます。

今回作ってみたもの

とりあえず、アーキテクチャ上のメリットじゃないかなと思う点を巻き込みました。

Slack への通知をする機能をイベントハンドラーとしてFunctionを作成。
管理するAzureサブスクリプションをイベントソースとして、Event Gridに投稿する
Function の作成。
これにより、管理するAzureサブスクリプションが増えた時は、Event Gridへの送信するFunction の関連付けを増やす。
通知の方法を変更したい場合は、イベントをサブスクライブするfunction を修正するか、新たに作成する。コードを書きたくないときは、Logic Appsで購読することもできます。

Slack へ自動的に投稿されたもの

Storageアカウントのキーを取得する操作が成功したことを通知しています。
このもっと表示するを選ぶとどのユーザー(もしくはサービスプリンシパル)が行ったというのも見えるようにしました。

Azure Event Grid のサブスクリプション用のイベント スキーマ
https://docs.microsoft.com/ja-jp/azure/event-grid/event-schema-subscriptions


ソースコード

https://github.com/darkcrash/AzureAdventCalendar2018-EventGrid

Azure サブスクリプションのイベントをサブスクライブして、Slack に投稿する run.csx

#r "Microsoft.Azure.EventGrid"
#r "Newtonsoft.Json"

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.Azure.EventGrid.Models;

static string webhookUrl = "https://hooks.slack.com/services/******/*********/*******************";

public static void Run(JObject eventGridEvent, ILogger log)
{
    log.LogInformation(eventGridEvent.ToString(Formatting.Indented));

    var hc = new HttpClient();
    var data = eventGridEvent["data"];
    var authorization = data["authorization"];
    var claims = data["claims"];
    
    var colorText = (data["status"]?.ToString() == "Succeeded" ? "good" : "");
    var fieldsObject = new object [] {
                    new {
                        title = "id",
                        value = eventGridEvent["id"],
                        @short = true
                    },
                    new {
                        title = "Event Time",
                        value = DateTime.Parse(eventGridEvent["eventTime"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"),
                        @short = true
                    },
                    new {
                        title = "Status",
                        value = data["status"],
                        @short = true
                    },
                    new {
                        title = "Data Version",
                        value = eventGridEvent["dataVersion"],
                        @short = true
                    },
                    new {
                        title = "Metadata Version",
                        value = eventGridEvent["metadataVersion"],
                        @short = true
                    },
                    new {
                        title = "Subject",
                        value = eventGridEvent["subject"],
                        @short = false
                    },
                    new {
                        title = "Tenant Id",
                        value = data["tenantId"],
                        @short = true
                    },
                    new {
                        title = "Resource Provider",
                        value = data["resourceProvider"],
                        @short = true
                    },
                    new {
                        title = "Topic",
                        value = eventGridEvent["topic"],
                        @short = false
                    }
                };
    var attachmentsObject = new object[] { 
            new {
                title = data["operationName"],
                color = colorText,
                fields = fieldsObject
            },
            new {
                title = "data",
                text = data.ToString()
            }
        };
    var json = JsonConvert.SerializeObject(new
    {
        text = $"*{eventGridEvent["eventType"]}*",
        icon_emoji = ":ghost:",
        username = "Azure Subscription",
        attachments = attachmentsObject
    });

    var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
    var res = hc.PostAsync(webhookUrl, content).Result;
}

Azure サブスクリプションのイベントを Event Grid に転送する run.csx

#r "Microsoft.Azure.EventGrid"
#r "Newtonsoft.Json"

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.Azure.EventGrid.Models;

static string eventGridUrl = "https://[Event Grid Topic Name].[Region].eventgrid.azure.net/api/events?api-version=2018-01-01";
static string sasKey = "[Event Grid Topic Sas Key]";
public static async Task Run(JObject eventGridEvent, ILogger log)
{
    var data = eventGridEvent["data"];
    var httpRequest = data["httpRequest"];
    var url = httpRequest["url"]?.ToString() ?? "";
    var subject = eventGridEvent["subject"]?.ToString() ?? "";
    var hasMicrosoftAuthorization = subject.EndsWith("Microsoft.Authorization");
    var hasUrl = url.Contains("[this funciton hostname]/providers/Microsoft.Authorization/CheckAccess");
    var isSucceeded = (data["status"]?.ToString() ?? "") == "Succeeded";
    // 自分自身のサービス認証を通知しないためにfunction のアクセスをバイパスさせる
    if (hasMicrosoftAuthorization && hasUrl && isSucceeded) return;

    var hc = new HttpClient();
    eventGridEvent["topic"] = null;
    var json = $"[{eventGridEvent.ToString(Formatting.None)}]";
    hc.DefaultRequestHeaders.Add("aeg-sas-key", sasKey);
    var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
    var res = await hc.PostAsync(eventGridUrl, content);
    log.LogInformation(res.ToString());
}

上記のコードは、 Azure サブスクリプションを前提にしてますが、スキーマ定義に基づいて変更していくとStorageに追加・削除されたときにも応用が利くと思います。
認可されたClaim情報は、細かいところまで出てくるので、扱いはちょっと注意したほうがいいカモしれませんね。

Event Domain

サンプルアプリとは関係のないところで、独自のイベントソースから、複雑で大規模なEvent Grid への配信が必要になった場合に活用できそうです。

Event Grid トピックを管理するためのイベント ドメインについて
https://docs.microsoft.com/ja-jp/azure/event-grid/event-domains
(2018/12/22 日本語ポータルでデプロイすると失敗するので、言語を英語に切り替えてデプロイする。)

まとめ

サーバーレスアーキテクチャ、そうでないものにもイベントベースのアプリケーションや外部連携に扱える Event Grid を頭の片隅に置いておくと便利じゃないかなという話でした。