1923

都内と業界の隅っこで生活しているエンジニアのノート

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ポータルからGUIAzure 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アプリからメッセージの送信・受信を確認して完了です。

f:id:taka1923:20201105164749p:plain

参考

クイック スタート:デバイスから IoT ハブに利用統計情報を送信してバックエンド アプリケーションで読み取る (.NET)

Azure-Samples/azure-iot-samples-csharp

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 の設定については以下に詳しく記載されています。

docs.microsoft.com

Key VaultではなくVariables を使う場合は以下をご覧ください。

docs.microsoft.com

React Native (Expo) から Azure Functions と SignalR Service のクイック スタートのサンプルアプリを試してみた

クイック スタート:C# を使用した Azure Functions と SignalR Service によるチャット ルームの作成のクライアントテストアプリケーションを Expoで作成して試してみます。

docs.microsoft.com

記事の中で紹介されている サンプルのシングル ページ Web アプリケーション を React Native + Expo 用にちょっと書き直しです。

クリックスタートに従って、ブラウザで動くところまで準備して

  1. Azure SignalR Service作成
  2. サンプル アプリケーションの準備と実行
  3. 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 アプリケーションと同じように動きました。

f:id:taka1923:20200813055457p:plainf:id:taka1923:20200813055014p:plain

GraphQL for .NET で Azure Functions + Cosmos DB を試してみた

Azure Functions で .NET を使って GraphQL の QueryとMutation を試してみました。
以下のブログと紹介されているコードを読めば特に説明不要ですね。

www.tpeczek.com

www.tpeczek.com

追加するするパッケージも以下の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 を指定して動作確認して無事完了。 f:id:taka1923:20200804111244p:plain

クエリはこんな感じです。

取得

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 の変更通知でしたら、以下で紹介されているシナリオと同じです。

docs.microsoft.com

ASP.NET Core ではないですが、Azure Functions と Azure SignalR Service は Microsoft Learn で試すことも可能です。

docs.microsoft.com

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に対応したサーバーを作成します。

ざっくり以下な感じです。

  1. gRPC サービスのプロジェクトを作成
  2. Grpc.AspNetCore パッケージを 2.29 以降に更新し、Grpc.AspNetCore.Web パッケージを追加
  3. UseGrpcWeb と EnableGrpcWeb を Startup.cs に追加
  4. 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は後程使いますのでメモっておきます。

  • 認証
    • シングルページ アプリケーションにリダイレクト URIを登録
  • API のアクセス許可
    • Azure Service Management / user_impersonationを追加

クイック スタート: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);
      }