このエントリは2019/07/12現在の情報に基づいています。将来の機能追加や廃止に伴い、このエントリの内容との乖離が発生する可能性があります。
Azure SignalR Serviceとは
リアルタイムWebを実現するためのコンポーネントを提供するマネージドサービス。簡単に言うと、WebSocketsやServer Sent Events、Long Pollingなどの機能を提供する。詳細は以下のドキュメントを参照。
Azure SignalR サービスとは / What is Azure SignalR Service?
https://docs.microsoft.com/azure/azure-signalr/signalr-overview
単純にTutorialをやるのも何かあれなので、FucntionsでCosmos DBの変更フィードを読み取り、SignalR Serviceに投げ込んで、投げ込まれたメッセージをリアルタイムで確認する、というある種の技術の無駄遣いを通じて、動作確認をしてみた。使う言語はいつも通りJavaのみである。
なお、Cosmos DBの変更フィードを取得して複数のサービスに伝播する場合、Functionsの後続にEvent HubsやEvent Grid、もしくはService Busを配置して連携するのが王道である。
やりたいこと(動作確認のためのシナリオ)
IoTデバイス等からCosmos DBに温度、湿度、デバイスID、測定時刻がPOSTされるので、そのデータ着信を変更フィードで受け取り、WebSocket経由でデータを受け取りたい。具体的には、下図の赤枠で囲んだ部分。簡単のため、認証の仕組みは入れていない。

必要なもの
- Azure SignalR Service
- Functions
- Cosmos DB
準備
Azure SignalR Service
ふつうにインスタンスを作成する。接続文字列は後ほどFunctionsで使うため、記録しておく。モードはServelessにする(設定>Settings。インスタンス生成時に設定してもよい)。

Cosmos DB
こちらもふつうにPortalもしくはAzure CLIでCosmos DBのアカウントを作成する。変更フィードを利用するため、SQL APIもしくはGremlin APIを選択しなければならないが、今回はSQL APIを使う。接続文字列はFunctionsで使うため、記録しておく。 Database、Containerも作成しておく。スループットはデフォルトでかまわない。

今回は以下のようなデータを使う。
{
"device":"deviceXXX",
"timestamp":"2019-07-01T01:00:00+09:00",
"humid":40.1,
"temp":22.1
}
Functions
PortalもしくはAzure CLIでFunctionsアプリを作成する。SignalRとCosmos DBの接続文字列を設定しておく(Fuction App > 構成)。
SignalRの接続文字列は、[アプリケーション設定]で設定する。名前をAzureSignalRConnectionString としておく。
Cosmos DBの接続文字列は、[接続文字列]で設定する。名前はAzureCosmosDBConnection としておく。データソースの種類はCustomでよい。
開発言語としてJavaを使うため、以下を実行してソースコードを生成しておく。リージョンなどは適宜変更する。
mvn archetype:generate \
-DarchetypeGroupId=com.microsoft.azure \
-DarchetypeArtifactId=azure-functions-archetype \
-DappName={functionAppName} \
-DappRegion={region} \
-DresourceGroup={resourceGroup} \
-DgroupId=com.{functionAppName}.group \
-DartifactId={functionAppName}-functions \
-Dpackage=com.{functionAppName} \
-DinteractiveMode=false
ローカル環境でテストできるよう、以下のコマンドを実行して、local.settings.jsonにAzure Portalで登録した値を反映させておく。
func azure functionapp fetch-app-settings {functionAppName}
上記コマンドを実行すると、Azure上の設定をlocal.settings.jsonに反映できる。以下は実行した結果のlocal.settings.jsonの例(文字列等はマスクしている)。
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "...",
"FUNCTIONS_WORKER_RUNTIME": "java",
"FUNCTIONS_EXTENSION_VERSION": "~2",
"APPINSIGHTS_INSTRUMENTATIONKEY": "...",
"AzureSignalRConnectionString": "..."
},
"ConnectionStrings": {
"AzureCosmosDBConnection": {
"ConnectionString": "..."
}
},
"Host": {
"CORS": "*"
}
}
negotiate function
クライアントが接続して、WebSocket接続のための情報を入手するためのFunction。これはお作法が決まっているので、Hubだけ変更し、それ以外はサンプルコードをそのまま使う。
Azure Functions における SignalR サービスのバインド / SignalR Service bindings for Azure Functions
https://docs.microsoft.com/azure/azure-functions/functions-bindings-signalr-service
具体的には以下。
@FunctionName("negotiate")
public SignalRConnectionInfo negotiate(
@HttpTrigger(name = "req",
methods = {HttpMethod.POST},
authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> req,
@SignalRConnectionInfoInput(name = "connectionInfo",
hubName = "climatehub") SignalRConnectionInfo connectionInfo) {
return connectionInfo;
}
このFunctionはHTTP Triggerなので、POSTで呼び出すと以下のような応答が返る。
{
"url": "https://xxxx.service.signalr.net/client/?hub=climatehub",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE1NjIwMzU3MTYsImV4cCI6MTU2MjAzNzUxNiwiaWF0IjoxNTYyMDM1NzE2LCJhdWQiOiJodHRwczovL2NsaW1hdGVpbmZvLnNlcnZpY2Uuc2lnbmFsci5uZXQvY2xpZW50Lz9odWI9Y2xpbWF0ZWh1YiJ9.z7WSaTmMGT34D5leeiqfJT4XUUet66eDW7ZMMKpDubk"
}
sendMessage function
こちらは、Cosmos DBの変更フィードをSignalRに投げ込む。そのため、バインドは以下のものを使う。
- Input : @CosmosDBTrigger
- Output : @SignalROutput
@CosmosDBTriggerでCosmos DB接続のための設定を追加し、文字列(JSON)として渡ってくる変更フィードを受けるためのArrayListを引数にしている。Functionの戻り値をSignalRへの出力にバインドしている。
@FunctionName("sendMessage")
@SignalROutput(name = "$return", hubName = "climatehub")
public SignalRMessage sendClimateInfo(
@CosmosDBTrigger(feedPollDelay = 1000,
name = "climateBinding",
databaseName = "ClimateDB",
collectionName = "ClimateContainer",
leaseCollectionName = "Leases",
createLeaseCollectionIfNotExists = true,
connectionStringSetting = "AzureCosmosDBConnection")
ArrayList<String> climateJSONList,
final ExecutionContext context) {
context.getLogger().info(climateJSONList.size() + " record(s) is/are inserted.");
climateJSONList.stream().forEach(s -> System.err.println("Changed: " + s));
ArrayList<ClimateInfo> climateInfoArrayList = new ArrayList<>();
for (String s : climateJSONList) {
JSONObject json = new JSONObject(s);
climateInfoArrayList.add(new ClimateInfo(json.getString("device"), json.getString("timestamp"), json.getFloat("humid"), json.getFloat("temp")));
}
SignalRMessage message = new SignalRMessage();
message.target = "climates";
message.arguments.addAll(climateInfoArrayList);
return message;
}
Cosmos DBの変更フィードはCosmos DBに格納されているデータがそのまま渡ってくるため、必要な情報だけSignalRに渡すように編集している。
クライアントからの接続時には、Hub名としてmessage.targetに指定しているclimatesを使う(climatehubではないことに注意)。
クライアント
簡単のため、Javaコンソールアプリケーションを使うことにする(本当はJavaScriptのほうがもっと楽だとは思うが)。
public static void main(String... args) {
connection = HubConnectionBuilder.create(apiBaseUrl).build();
connection.setServerTimeout(100000);
connection.start().blockingAwait();
connection.on("climates", (message) -> {
System.out.println("[climates] device: " + message.getDevice() + " timestamp: " + message.getTimestamp() + " humid: " + message.getHumid() + " temp: " + message.getTemp());
}, ClimateInfo.class);
}
URLを指定して接続を作成、開始し、Hub名を指定して着信を待つ。
動作確認
事前にクライアントを複数起動しておき、Cosmos DBにデータを追加していった場合の挙動を確認する。クライアントにはデータが届いていることがわかる。

SignalRのメトリックを見ると、接続クライアント数は2。

そして、流通しているメッセージの件数もトラッキングできていることを確認。
