かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Azure Functions で Service Bus の Topic 使ってみたよ(1つの関数の後に複数の関数をキックしたいみたいなのに使える)

Azure Functions で1つの処理をキックしたとします。 処理をキックしたあとに後続に処理結果を渡して続きの処理をしてほしいとします。

後続が1つなら Storage Account の Queue に Message を投げ込んでおけば、後続の処理は QueueTrigger でキックされる関数で作ればOKですね。

後続が2つなら…?3つなら…?

例えば後続の処理は HttpTrigger で作っておいて以下のようにコードからキックするという手が思いつきます。 こんな関数を用意しておいて

var http = require("http");

function invokeFunction(appName, functionName, code, callback) {
    var req = http.request({
        host: `${appName}.azurewebsites.net`,
        path: `/api/${functionName}?code=${code}`,
        method: "POST",
    }, function (res) {
        callback(res);
    });
    req.end();
}

こういう感じで呼ぶイメージですね。

var appName = "myAppName";
var functionName = "myFunctionName";
var code = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

invokeFunction(appName, functionName, code, function(res) {
    console.log(res.statusCode);
});

これを後続の処理の関数の数だけやればOKと。

いやいや、後続の処理とべったり紐づいてる感じがなんかやだ。それなら別の関数じゃなくて普通に内部メソッドで呼ぶわ!っていう気にもなりますね。まぁ非常に重たい処理とかならあれかもですが…。あ、でも重たい処理だと実装によってはレスポンスがいつまでも返ってこないとかありえそうですね。とまぁ色々辛い。

Service Bus のトピックを使おう

とまぁ、こういうときに使えるサービスとしてトピックというのがあります。 要は1つメッセージ投げ込んだら定義してる受信する人たちの受け口に対してメッセージをばらまいてくれるという感じです。Functions ではトピックをサポートしてるので簡単に使うことが出来ます。

Azure 上にリソースを配置

ということでやってみます。Azure上にFunction App(と、紐づくストレージアカウント)と、Service Busを作ります。

f:id:okazuki:20170819163344p:plain

Service Bus の接続文字列の設定

共有アクセスポリシーのRootManageSharedAccessKeyからサクッと接続文字列をもらいましょう。

接続文字列は、Function Appのアプリケーションの設定のアプリ設定(not 接続文字列)に適当なキー名で追加します。

f:id:okazuki:20170819163630p:plain

ここでは、ServiceBusConnectionStringという名前にしました。

Service Bus にトピックを作成

今回は、トピックに1つのメッセージが投げ込まれたら2つの配信先に配るみたいにしたいと思います。まず、トピックを1つ作ります。

f:id:okazuki:20170819163842p:plain

トピックを作ったら、その中にサブスクリプションを2つ作ります。

f:id:okazuki:20170819164121p:plain

これで下準備は完了です。

関数を作ろう

C# でも node.js でもどっちでもいいんですが、たまたま Visual Studio 2017 が 15.3.1 の更新走ってるので node.js で行こうと思います。

func init

で初期化して

func azure functionapp fetch 関数アプリ名

で設定を吸い出します。

func new

を3回やってInvoker(HttpTrigger), Func1(ServiceBusTopicTrigger), Func2(ServiceBusToppicTrigger)を作ります。

まず、Invokerです。こいつはURL叩かれたらログを吐いてServiceBusのトピックにメッセージを投げ込みます。

Invoker/function.json

{
  "disabled": false,
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "name": "topic",
      "type": "serviceBus",
      "direction": "out",
      "topicName": "functopic",
      "connection": "ServiceBusConnectionString"
    }
  ]
}

Invoker/index.js

module.exports = function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');
    context.bindings.topic = "timestamp: " + Date.now();
    context.bindings.res = {
        status: 200,
        body: "invoked",
    };
    context.done();
};

次に、トピックからメッセージを受け取った時の関数を作ります。

Func1/function.json

{
  "disabled": false,
  "bindings": [
    {
      "name": "serviceBusMessage",
      "type": "serviceBusTrigger",
      "direction": "in",
      "topicName": "functopic",
      "subscriptionName": "func1",
      "connection": "ServiceBusConnectionString"
    }
  ]
}

Func1/index.js

module.exports = function(context, serviceBusMessage) {
    context.log('Func1 called.', serviceBusMessage);
    context.done();
};

Func2も同じ要領で(function.jsonに指定しているtopicNameが違うだけ)

Func2/function.json

{
  "disabled": false,
  "bindings": [
    {
      "name": "serviceBusMessage",
      "type": "serviceBusTrigger",
      "direction": "in",
      "topicName": "functopic",
      "subscriptionName": "func2",
      "connection": "ServiceBusConnectionString"
    }
  ]
}

Func2/index.js

module.exports = function(context, serviceBusMessage) {
    context.log('Func2 called.', serviceBusMessage);
    context.done();
};

ここまで出来たら、以下のコマンドでデプロイしましょう。

func azure functionapp publish 関数アプリ名

動作確認

ポータルから Invoker 関数のURLを取得します。

f:id:okazuki:20170819165225p:plain

そして、関数が実行されたか確認するためにアプリログを有効にしてログストリーミングを開いておきます。ログを確認できる状態にしたら、PostmanとかブラウザでURLをたたいてみましょう。

f:id:okazuki:20170819165518p:plain

ログを見ると、ちゃんとInvokerが呼ばれた後にFunc1とFunc2が呼び出されていることが確認できます。

2017-08-19T07:58:03.375 Executing HTTP request: {
  "requestId": "6e79f229-a593-424c-89de-1c15f03ffce1",
  "method": "GET",
  "uri": "/api/Invoker"
}
2017-08-19T07:58:03.375 Function started (Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35)
2017-08-19T07:58:03.375 Executing 'Functions.Invoker' (Reason='This function was programmatically called via the host APIs.', Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35)
2017-08-19T07:58:03.375 JavaScript HTTP trigger function processed a request.
2017-08-19T07:58:03.375 Function started (Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35)
2017-08-19T07:58:03.375 JavaScript HTTP trigger function processed a request.
2017-08-19T07:58:03.640 Function completed (Success, Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35, Duration=268ms)
2017-08-19T07:58:03.640 Function started (Id=c29a7668-b9a9-4e49-af13-68e4f43a8747)
2017-08-19T07:58:03.655 Func1 called. timestamp: 1503129483375
2017-08-19T07:58:03.655 Function completed (Success, Id=c29a7668-b9a9-4e49-af13-68e4f43a8747, Duration=4ms)
2017-08-19T07:58:03.640 Function started (Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1)
2017-08-19T07:58:03.655 Func2 called. timestamp: 1503129483375
2017-08-19T07:58:03.655 Function completed (Success, Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1, Duration=4ms)
2017-08-19T07:58:03.640 Function completed (Success, Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35, Duration=268ms)
2017-08-19T07:58:03.640 Executed 'Functions.Invoker' (Succeeded, Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35)
2017-08-19T07:58:03.640 Executed HTTP request: {
  "requestId": "6e79f229-a593-424c-89de-1c15f03ffce1",
  "method": "GET",
  "uri": "/api/Invoker",
  "authorizationLevel": "Function"
}
2017-08-19T07:58:03.640 Response details: {
  "requestId": "6e79f229-a593-424c-89de-1c15f03ffce1",
  "status": "OK"
}
2017-08-19T07:58:03.640 Function started (Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1)
2017-08-19T07:58:03.640 Executing 'Functions.Func2' (Reason='New ServiceBus message detected on 'functopic/Subscriptions/func2'.', Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1)
2017-08-19T07:58:03.640 Function started (Id=c29a7668-b9a9-4e49-af13-68e4f43a8747)
2017-08-19T07:58:03.640 Executing 'Functions.Func1' (Reason='New ServiceBus message detected on 'functopic/Subscriptions/func1'.', Id=c29a7668-b9a9-4e49-af13-68e4f43a8747)
2017-08-19T07:58:03.655 Func2 called. timestamp: 1503129483375
2017-08-19T07:58:03.655 Func1 called. timestamp: 1503129483375
2017-08-19T07:58:03.655 Function completed (Success, Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1, Duration=4ms)
2017-08-19T07:58:03.655 Executed 'Functions.Func2' (Succeeded, Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1)
2017-08-19T07:58:03.655 Function completed (Success, Id=c29a7668-b9a9-4e49-af13-68e4f43a8747, Duration=4ms)
2017-08-19T07:58:03.655 Executed 'Functions.Func1' (Succeeded, Id=c29a7668-b9a9-4e49-af13-68e4f43a8747)

まぁ順番はばらっばらですけどね。仕方ないでしょう。

気になるのは、最初のほうで

JavaScript HTTP trigger function processed a request.

が2回出てるところでしょうか…なんぞこれ。あとで見てみよう。

まとめ

ということで、後続の関数が増えたらトピックでサブスクリプションを増やして、それをトリガーにする関数を追加するだけでいいというお手軽さ。すっかり忘れてたよトピック。