Azure IoT Hub でメッセージの送信と受信を試してみた
IoT Hubでメッセージの送信と受信を試したメモです。 UWPアプリからIoT Hubにメッセージを送信して、 Functions で受信したメッセージをそのままデバイスに送信しているだけです。
公式サイトのドキュメントや、サンプルも豊富にあるので簡単に試すことできます。
作業はこんな感じです。
- Azure にリソース作成
- IoT Hubにデバイスを登録
- Functions で動作させるEcho Function作成
- Device用アプリの作成
- 動作確認
Azure にリソース作成
IoT Hub と Functions を作成。cloud-to-device メッセージングを使うので、IoT Hub は無料又はStandard。
Azure IoT Hubにデバイスを登録
IoT Hubにデバイスを登録します。
Azure Cloud Shell を使用する を参考にコマンド、
az extension add --name azure-iot az iot hub device-identity create --hub-name HubName11--device-id device1
AzureポータルからGUI、Azure IoT エクスプローラーなどで簡単に作成。
Functions で動作させるEchoプログラムを作成
受信した値をそのまま送信しているだけです。
public static class Function1 { private static readonly ServiceClient ServiceClient = ServiceClient.CreateFromConnectionString("接続文字列"); [FunctionName("Function1")] public static async Task Run( [EventHubTrigger("HubName11", Connection = "EventHubConnectionAppSetting")] EventData[] events, ILogger log) { foreach (var eventData in events) { var id = eventData.SystemProperties.FirstOrDefault(m => m.Key == "iothub-connection-device-id"); var commandMessage = new Message(eventData.Body.Array); await ServiceClient.SendAsync(id.Value.ToString(), commandMessage); await Task.Yield(); } } }
"接続文字列"は、Azureポータルの「設定」> 「共有アクセス ポリシー」にある「接続文字列」の値。EventHubConnectionAppSettingは、「設定」 > 「組み込みのエンドポイント」の「イベントハブ互換エンドポイント」の値です。
Device用アプリ作成
ここにあるサンプルを参考に、今回はUWPアプリでメッセージの送信と受信アプリを作成。
MainPage.xaml.cs
public sealed partial class MainPage : Page { private readonly DeviceClient _deviceClient = DeviceClient.CreateFromConnectionString("接続文字列", TransportType.Http1); private readonly bool _receiving = true; public MainPage() { InitializeComponent(); ReceiveMessagesAsync(); } private readonly ObservableCollection<string> _contacts = new ObservableCollection<string>(); public ObservableCollection<string> Contacts => this._contacts; private async void SendButton_Click(object sender, RoutedEventArgs e) { var msg = new Message(Encoding.UTF8.GetBytes(SendMessageTextBox.Text)); await _deviceClient.SendEventAsync(msg); var s = "送信:" + SendMessageTextBox.Text + " >>"; _contacts.Insert(0, s); } private async void ReceiveMessagesAsync() { while (_receiving) { var receivedMessage = await _deviceClient.ReceiveAsync(); if (receivedMessage != null) { var s = "受信:" + "<< " + Encoding.UTF8.GetString(receivedMessage.GetBytes()); _contacts.Insert(0, s); await _deviceClient.CompleteAsync(receivedMessage); } } } private void MessageTextBox_TextChanged(object sender, TextChangedEventArgs e) { SendButton.IsEnabled = !string.IsNullOrEmpty(SendMessageTextBox.Text); } }
MainPage.xaml
<Page x:Class="App1.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App1" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Width="1000" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid HorizontalAlignment="Left" Width="1000"> <TextBox HorizontalAlignment="Left" Margin="57,56,0,0" Name="SendMessageTextBox" Text="" TextWrapping="Wrap" VerticalAlignment="Top" Header="送信メッセージ" Width="726" TextChanged="MessageTextBox_TextChanged"/> <Button Content="送信" Margin="800,80,0,0" VerticalAlignment="Top" Name="SendButton" IsEnabled="False" Click="SendButton_Click"/> <ListView x:Name="FruitsList" Margin="57,157,57,80" ItemsSource="{x:Bind Contacts}"> <UIElement.RenderTransform> <MatrixTransform/> </UIElement.RenderTransform> <ListView.ItemTemplate> <DataTemplate x:DataType="x:String"> <Grid> <TextBlock Text="{x:Bind}" FontSize="14" Grid.Column="0"/> </Grid> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid> </Page>
動作確認
Functions をAzureにデプロイし、Deviceアプリからメッセージの送信・受信を確認して完了です。
参考
クイック スタート:デバイスから IoT ハブに利用統計情報を送信してバックエンド アプリケーションで読み取る (.NET)
Azure Pipelines で Expo にデプロイ
Expo公式サイトの Setting up Continuous Integration に Azure Pipelines の設定なかったので記載。
やってることは同じですし、他とちょっとタスク名が違う程度です。
- Jestでテスト
- Expoのユーザー名とパスワードを Key Vault から取得
- expo-cli で login と publish
Deploy to Expo
trigger: - master pool: vmImage: 'ubuntu-latest' variables: npm_config_cache: $(Pipeline.Workspace)/.npm steps: - task: NodeTool@0 inputs: versionSpec: '12.x' displayName: 'Install Node.js' - task: Cache@2 inputs: key: 'npm | "$(Agent.OS)" | package-lock.json' restoreKeys: | npm | "$(Agent.OS)" path: $(npm_config_cache) displayName: Cache npm - script: | npm ci npm install expo npm test displayName: 'npm test' - task: AzureKeyVault@1 inputs: azureSubscription: '[SERVICE CONNECTION]' KeyVaultName: '[KeyVault]' SecretsFilter: '*' RunAsPreJob: false - script: | npx expo-cli login -u $(expo-username) -p $(expo-password) npx expo-cli publish --non-interactive displayName: 'expo publish'
OTAではなくビルドの場合は、「publish」を「build」に変更。iOSの場合は以下のような感じです。
npx expo-cli build:ios --no-wait --non-interactive --release-channel staging
Key Vault の設定については以下に詳しく記載されています。
Key VaultではなくVariables を使う場合は以下をご覧ください。
React Native (Expo) から Azure Functions と SignalR Service のクイック スタートのサンプルアプリを試してみた
クイック スタート:C# を使用した Azure Functions と SignalR Service によるチャット ルームの作成のクライアントテストアプリケーションを Expoで作成して試してみます。
記事の中で紹介されている サンプルのシングル ページ Web アプリケーション を React Native + Expo 用にちょっと書き直しです。
クリックスタートに従って、ブラウザで動くところまで準備して
- Azure SignalR Service作成
- サンプル アプリケーションの準備と実行
- Web アプリケーションで動作確認
クライアントを作成です。
Expo のプロジェクト作成 (TypeScript)とパッケージを必要なパッケージの追加
expo init chat cd chat npm i @microsoft/signalr npm i axios
あとは出来たプロジェクトのApp.tsxをちょい修正してサンプルアプリは完了。
import React from 'react' import { StyleSheet, Text, TextInput, Button, SafeAreaView, FlatList } from 'react-native' import { HubConnectionBuilder } from '@microsoft/signalr' import axios from 'axios' const name = 'ヤス' const apiBaseUrl = 'http://192.168.XXX.XXX:7071' export default class App extends React.Component { state:IState = { isReady: false, count: 1, text: '', messages: [] }; increment (): number { const count = this.state.count + 1 this.setState({ count: count }) return count } componentDidMount = () => { const hubConnection = new HubConnectionBuilder() .withUrl(`${apiBaseUrl}/api`) .build() hubConnection.onclose(() => console.log('disconnected')) hubConnection .start() .then(() => this.setState({ isReady: true })) hubConnection.on('newMessage', (m:IMessage) => { const key = this.increment() const messages = this.state.messages.slice() as Message[] const message = new Message(key.toString(), m.sender, m.text) messages.unshift(message) this.setState({ messages: messages }) }) } sendMessage = (text:string) => { axios.post(`${apiBaseUrl}/api/messages`, { sender: name, text: text }).then(_ => {}) this.setState({ text: '' }) } render () { if (!this.state.isReady) { return <SafeAreaView style={styles.container}> <Text>Connecting</Text> </SafeAreaView> } else { return <SafeAreaView style={styles.container}> <Text>Serverless Chat</Text> <TextInput style={styles.textBox} onChangeText={value => this.setState({ text: value })} value={this.state.text} /> <Button title="送信" onPress={() => { this.sendMessage(this.state.text) }} /> <FlatList data={this.state.messages} renderItem={({ item }) => <Text style={styles.item}> {item.sender} : {item.text} </Text>} /> </SafeAreaView > } } } class Message implements IMessage { key: string sender: string text: string constructor (key: string, sender: string, text: string) { this.key = key this.sender = sender this.text = text } } interface IMessage { sender: string text: string } interface IState { isReady: boolean count: number text: string messages: Message[] } const styles = StyleSheet.create({ container: { flex: 1 }, item: { padding: 10, height: 44 }, textBox: { height: 50, margin: 10, padding: 10, borderColor: 'gray', borderWidth: 1 } })
サンプルのシングル ページ Web アプリケーションと同じように動きました。
GraphQL for .NET で Azure Functions + Cosmos DB を試してみた
Azure Functions で .NET を使って GraphQL の QueryとMutation を試してみました。
以下のブログと紹介されているコードを読めば特に説明不要ですね。
追加するするパッケージも以下の3つだけ。
- GraphQL.Server.Core
- Microsoft.Azure.Functions.Extensions
- Microsoft.Azure.WebJobs.Extensions.CosmosDB
デモのInfrastructure内のコードやStartupなどは、ほぼそのまま。モデルやスキーマをちょっと変えて試してみました。
Model と Type
public class Person { public string Id { get; set; } public string Firstname { get; set; } public int Age { get; set; } }
public sealed class PersonType : ObjectGraphType<Person> { public PersonType() { Name = "Person"; Field(x => x.Id, type: typeof(IdGraphType)).Description("ID"); Field(x => x.Firstname).Description("Firstname"); Field(x => x.Age).Description("Age"); } }
Query
public class SampleQuery : ObjectGraphType { private static readonly Uri CollectionUri = UriFactory.CreateDocumentCollectionUri("SampleDB", "People"); public SampleQuery(IDocumentClient documentClient) { // 全件取得 Field<ListGraphType<PersonType>>( "people", resolve: context => documentClient.CreateDocumentQuery<Person>(CollectionUri, null) ); // IDを指定して取得 FieldAsync<PersonType>( "person", arguments: new QueryArguments( new QueryArgument<IdGraphType> { Name = "id", Description = "ID" }), resolve: async context => { var id = context.GetArgument<string>("id"); var uri = UriFactory.CreateDocumentUri("SampleDB", "People", id); var document = await documentClient.ReadDocumentAsync<Person>(uri, new RequestOptions { PartitionKey = new PartitionKey(id) }); return document.Document; }); } }
Mutation
public class SampleMutation : ObjectGraphType { private static readonly Uri CollectionUri = UriFactory.CreateDocumentCollectionUri("SampleDB", "People"); public SampleMutation(IDocumentClient documentClient) { // 追加 FieldAsync<PersonType>( "createPerson", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "firstname" }, new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "age" }), resolve: async context => { var firstname = context.GetArgument<string>("firstname"); var age = context.GetArgument<int>("age"); var id = Guid.NewGuid().ToString(); var person = new Person { Id = id, Firstname = firstname, Age = age }; await documentClient.CreateDocumentAsync(CollectionUri, person); return person; } ); // 更新 FieldAsync<PersonType>( "updatePerson", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "id" }, new QueryArgument<StringGraphType> { Name = "firstname" }, new QueryArgument<IntGraphType> { Name = "age" } ), resolve: async context => { var id = context.GetArgument<string>("id"); var firstname = context.GetArgument<string>("firstname"); var age = context.GetArgument<int>("age"); var uri = UriFactory.CreateDocumentUri("SampleDB", "People", id); var person = new Person { Id = id, Firstname = firstname, Age = age }; await documentClient.ReplaceDocumentAsync(uri, person); return person; }); // 削除 FieldAsync<StringGraphType>( "deletePerson", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "id" } ), resolve: async context => { var id = context.GetArgument<string>("id"); var uri = UriFactory.CreateDocumentUri("SampleDB", "People", id); await documentClient.DeleteDocumentAsync(uri, new RequestOptions { PartitionKey = new PartitionKey(id) }); return id; }); } }
Schema
public class SampleSchema : GraphQL.Types.Schema { public SampleSchema(IDependencyResolver dependencyResolver) : base(dependencyResolver) { Query = dependencyResolver.Resolve<SampleQuery>(); Mutation = dependencyResolver.Resolve<SampleMutation>(); } }
クエリ
GraphiQL はないで、Postman で GraphiQL を指定して動作確認して無事完了。
クエリはこんな感じです。
取得
query { people { id firstname age } }
query { person (id : "00000000-0000-0000-bed0-3d36769a33c0") { id firstname age } }
追加
mutation { createPerson( firstname: "太郎" age: 10 ) { id firstname age } }
更新
mutation { updatePerson( id : "00000000-0000-0000-bed0-3d36769a33c0" firstname: "次郎" age: 20 ) { id firstname age } }
削除
mutation { deletePerson( id : "00000000-0000-0000-bed0-3d36769a33c0" ) }
Subscription
Subscription には対応していないので、通知は SignalR を使うのが簡単ですね。Cosmos DB の変更通知でしたら、以下で紹介されているシナリオと同じです。
ASP.NET Core ではないですが、Azure Functions と Azure SignalR Service は Microsoft Learn で試すことも可能です。
gRPC-Web for .NET のサービスを Azure App Service にデプロイしてブラウザからアクセスする
gRPC-Web for .NETが正式リリースされましたので、簡単なアプリを Azure App Service にデプロイしてWebブラウザからアクセスしてみます。
詳しい手順が書かれた公式ドキュメントが揃っていますのでメモ程度です。
環境 - Windows 10 / Visual Studio 2019
Server
チュートリアル: ASP.NET Core で gRPC のクライアントとサーバーを作成する とブラウザー アプリでの gRPC の使用を参考にgRPC・gRPC-webに対応したサーバーを作成します。
ざっくり以下な感じです。
- gRPC サービスのプロジェクトを作成
- Grpc.AspNetCore パッケージを 2.29 以降に更新し、Grpc.AspNetCore.Web パッケージを追加
- UseGrpcWeb と EnableGrpcWeb を Startup.cs に追加
- CORSの設定。CORSはローカルで開発する際に必要ですが、App Service では設定で対応できます。
public class Startup { public Startup(IConfiguration configuration, IWebHostEnvironment env) { _configuration = configuration; _env = env; } private readonly IConfiguration _configuration; private readonly IWebHostEnvironment _env; public void ConfigureServices(IServiceCollection services) { services.AddGrpc(); if (_env.IsDevelopment()) { services.AddCors(o => o.AddPolicy("AllowAll", builder => { builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding"); })); } } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); app.UseGrpcWeb(); if (env.IsDevelopment()) { app.UseCors(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb().RequireCors("AllowAll"); }); } else { app.UseEndpoints(endpoints => { endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb(); }); } } }
あとは 普通に App Service にデプロイすればサーバーの準備は完了です。App Serviceの構成にHTTPバージョンの設定がありますので2.0に変更も可能です。
Client
デプロイした gRPC-web にWebブラウザでアクセスするクライアントを作成します。
最初にprotobufスキーマからJavaScriptなどを生成する protoc と grpc-web を準備します。それぞれ実行可能なバイナリファイルを以下からダウンロード。適当にパスの通ったフォルダにコピーし、grpc-webは protoc-gen-grpc-web.exe に名前を変更します。
protoc
https://github.com/protocolbuffers/protobuf/releases
grpc-web
https://github.com/grpc/grpc-web/releases
今回は TypeScriptで書きたいのでimport_styleにcommonjs, typescriptを指定してコマンドを実行し、gRPCのJavaScript・型定義ファイル、grpc-web の TypeScriptのファイルを作成します。また、protoファイルは Server で使用した greet.proto をそのまま使っています。
コマンド例
protoc --proto_path= greet.proto --js_out=import_style=commonjs:./ --grpc-web_out=import_style=typescript,mode=grpcwebtext:./
生成されたファイル
- greet_pb.js
- greet_pb.d.ts
- GreetServiceClientPb.ts
Vue CLI などでクライアントプロジェクトを作成して、上記で作成したファイルを適当な場所にコピーすれば準備完了です。 以下のようにアクセスできます。
import Vue from "vue"; import { HelloRequest } from "../models/greet_pb"; import { GreeterClient } from "../models/GreetServiceClientPb"; export default Vue.extend({ name: "Home", components: { HelloWorld }, data() { return { msg: "" }; }, mounted: function() { const request = new HelloRequest(); request.setName("匿名希望"); const client = new GreeterClient( "https://xxxxx.azurewebsites.net", {}, {} ); client.sayHello(request, {}, (err, ret) => { if (err || ret === null) { console.log(err); } else { this.msg = ret.getMessage(); } }); } });
参考
MSAL.js(v2) で取得したトークンを使用して JavaScript 用 Azure Blob Storage v12を使う
Azure Functions や Web Apps を使わず静的なサイトでBlobを扱いたかったので、前回試していたMSAL.js(v2)のトークンを使って試しにBlobのコンテナ一覧を取得してみます。
REST APIを直接呼ぶ場合は、acquireTokenSilent で取得した accessToken を Bearer に設定。x-ms-version や x-ms-client-request-id などを Headers に追加する程度ですね。
今回は、JavaScript 用 Azure Blob Storage v12を使ってみます。サンプルは前回と同じく TypeScript + Vue.jsです。 TokenCredential と getToken の戻りとなる AccessToken を作成して、BlobServiceClient に作成した TokenCredential を渡せばコードは完了です。 あとはSASでのアクセスと同じくCORSやIAMの設定も必要です。
サンプル
import { TokenCredential } from "@azure/identity"; import * as Msal from "@azure/msal-browser"; import { AccessToken } from "@azure/core-auth"; class MsalAccessToken implements AccessToken { token = ""; expiresOnTimestamp = 0; } class MsalTokenCredential implements TokenCredential { private _context: Msal.PublicClientApplication; constructor(PublicClientApplication: Msal.PublicClientApplication) { this._context = PublicClientApplication; } getToken( scopes: string | string[], options?: import("@azure/core-auth").GetTokenOptions | undefined ): Promise<AccessToken | null> { const silentRequest = { scopes: ["https://storage.azure.com/user_impersonation"] }; const response = this._context.acquireTokenSilent(silentRequest); return response.then(res => { const ma = new MsalAccessToken(); ma.token = res.accessToken; ma.expiresOnTimestamp = Date.now() + 60 * 60 * 1000; return ma; }); } } import { BlobServiceClient } from "@azure/storage-blob"; import { ref, defineComponent, onMounted } from "@vue/composition-api"; export default defineComponent({ props: { storageAccount: { type: String, default: "" } }, setup(props, context) { const containers = ref<string[]>([]); onMounted(async () => { try { const tokenCredential = new MsalTokenCredential(context.root.$msal) const blobServiceClient = new BlobServiceClient( `https://${props.storageAccount}.blob.core.windows.net`, tokenCredential ); const iter = blobServiceClient.listContainers(); for await (const container of iter) { containers.value.push(container.name); } } catch (error) { console.log("failed: ", error); } }); return { containers }; } });
MSAL.js(v2)をTypeScriptで試してみる
MASL.js(V2)を使用してみます。まだ動いたので Azure Management Service API使ってサブスクリプションの一覧を取得してみます。
Microsoft ID プラットフォームのコード サンプル (v2.0 エンドポイント) の 認証コード フローと PKCE を使用した Microsoft Graph の呼び出し を参考に、 Vue.js + TypeScript で少し書き直しています。
アプリケーションを登録する
クイック スタート:Microsoft ID プラットフォームにアプリケーションを登録する を参考にアプリを登録します。
Azure ポータル のアプリの登録から作成して以下を設定するだけです。概要のアプリケーション (クライアント)IDは後程使いますのでメモっておきます。
クイック スタート:Microsoft ID プラットフォームにアプリケーションを登録する https://docs.microsoft.com/ja-jp/azure/active-directory/develop/quickstart-register-app
アプリケーションの作成
今回作成したコード(typescript-vuejs-masl-v2-sample)。作成した環境は以下となっています。
- Node.js 12.18.0 LTS
- Vue CLI 4.4.1
もとのサンプルの authRedirect.js を handleRedirectCallbackをhandleRedirectPromiseに変更、scopesを変更。あとはTypeScript + Vue.js な感じに少し手直しして完了です。
API関連を抜粋
設定
const msalConfig = { auth: { clientId: "Application(Client)ID", authority: "https://login.microsoftonline.com/common", redirectUri: "http://localhost:8080/" }, cache: { cacheLocation: "sessionStorage", storeAuthStateInCookie: false } };
ログイン
signIn() { const loginRequest = { scopes: ["https://management.core.windows.net//user_impersonation"] }; context.root.$msal.loginRedirect(loginRequest); },
ログアウト
signOut() { context.root.$msal.logout(); }
サブスクリプション一覧取得
const silentRequest = { scopes: ["https://management.core.windows.net//user_impersonation"] }; try { const response = await context.root.$msal.acquireTokenSilent( silentRequest ); const data = await callApi( "https://management.azure.com/subscriptions?api-version=2020-01-01", response.accessToken ); data.value.forEach(function(value: any) { const id: string = value.subscriptionId; subscriptions.push(id); }); } catch (error) { console.log("failed: ", error); }