チュートリアル: Azure Functions と Azure Web PubSub サービスを使用してサーバーレスのリアルタイム チャット アプリを作成する

Azure Web PubSub サービスは、WebSocket とパブリッシュ-サブスクライブ パターンを使用して、リアルタイム メッセージング Web アプリケーションを作成するのに役立ちます。 Azure Functions は、インフラストラクチャを管理することなくコードを実行できるサーバーレス プラットフォームです。 このチュートリアルでは、Azure Web PubSub サービスと Azure Functions を使用して、リアルタイム メッセージングとパブリッシュ-サブスクライブ パターンによるサーバーレス アプリケーションを作成する方法について説明します。

このチュートリアルでは、以下の内容を学習します。

  • サーバーレスのリアルタイム チャット アプリを構築する
  • Web PubSub 関数トリガーのバインドと出力バインドを使用する
  • 関数を Azure Function App にデプロイする
  • Azure 認証を構成する
  • イベントとメッセージをアプリケーションにルーティングするように Web PubSub イベント ハンドラーを構成する

必須コンポーネント

Azure サブスクリプションをお持ちでない場合は、開始する前に無料アカウントを作成してください。

Azure へのサインイン

Azure アカウントで Azure portal (https://portal.azure.com/) にサインインします。

Azure Web PubSub サービス インスタンスを作成する

アプリケーションは Azure 内の Web PubSub サービス インスタンスに接続します。

  1. Azure portal の左上にある [新規] ボタンを選択します。 [新規] 画面で、検索ボックスに「Web PubSub」と入力して Enter キーを押します。 (Azure Web PubSub を Web カテゴリから検索することもできます)。

    Azure Web PubSub をポータルで検索しているスクリーンショット。

  2. 検索結果の [Web PubSub] を選択し、 [作成] を選択します。

  3. 次の設定を入力します。

    設定 推奨値 説明
    リソース名 グローバルに一意の名前 新しい Web PubSub サービス インスタンスを識別するグローバルで一意の名前。 有効な文字は、a-z0-9、および - です。
    サブスクリプション 該当するサブスクリプション この新しい Web PubSub インスタンスが作成される Azure サブスクリプション。
    リソース グループ myResourceGroup Web PubSub サービス インスタンスの作成先となる新しいリソース グループの名前。
    場所 米国西部 近くのリージョンを選択します。
    [価格レベル] 無料 まず Azure Web PubSub サービスを無料でお試しいただけます。 Azure Web PubSub サービスの価格レベルの詳細をご覧ください。
    [ユニット数] - ユニット数は、Web PubSub サービス インスタンスで受け入れることができる接続の数を指定します。 各ユニットで最大 1,000 のコンカレント接続がサポートされます。 Standard レベルでのみ構成可能です。

    ポータルで Azure Web PubSub インスタンスを作成しているスクリーンショット。

  4. [作成] を選択して Web PubSub サービス インスタンスのデプロイを開始します。

関数を作成する

  1. Azure Functions Core Tools がインストールされていることを確認します。 次に、プロジェクトの空のディレクトリを作成します。 この作業ディレクトリの下でコマンドを実行します。

    func init --worker-runtime javascript
    
  2. Microsoft.Azure.WebJobs.Extensions.WebPubSub 関数拡張機能パッケージを明示的にインストールします。

    a. host.jsonextensionBundle セクションを削除して、次の手順で特定の拡張機能パッケージをインストールできるようにします。 または、ホスト json を下のように単純にします。

    {
        "version": "2.0"
    }
    

    b. 特定の関数拡張機能パッケージをインストールするコマンドを実行します。

    func extensions install --package Microsoft.Azure.WebJobs.Extensions.WebPubSub --version 1.0.0-beta.3
    
  3. クライアントの静的 Web ページを読み取ってホストする index 関数を作成します。

    func new -n index -t HttpTrigger
    
    • index/function.json を更新して次の json コードをコピーします。
      {
          "bindings": [
              {
                  "authLevel": "anonymous",
                  "type": "httpTrigger",
                  "direction": "in",
                  "name": "req",
                  "methods": [
                    "get",
                    "post"
                  ]
              },
              {
                  "type": "http",
                  "direction": "out",
                  "name": "res"
              }
          ]
      }
      
    • index/index.js を更新して次のコードをコピーします。
      var fs = require('fs');
      module.exports = function (context, req) {
          fs.readFile('index.html', 'utf8', function (err, data) {
              if (err) {
                  console.log(err);
                  context.done(err);
              }
              context.res = {
                  status: 200,
                  headers: {
                      'Content-Type': 'text/html'
                  },
                  body: data
              };
              context.done();
          });
      }
      
  4. クライアントがアクセス トークンを含むサービス接続 URL を取得するのに役立つ negotiate 関数を作成します。

    func new -n negotiate -t HttpTrigger
    

    注意

    このサンプルでは、AAD ユーザー ID ヘッダー x-ms-client-principal-name を使用して userId を取得します。 また、これはローカル関数では機能しません。 ローカルで実行する際には、これを空にするか他の方法に変えることで userId を取得または生成できます。 たとえば、negotiate 関数を呼び出してサービスの接続 URL を取得する際に、クライアントがユーザー名を入力し、それを ?user={$username} のようなクエリで渡すようにします。 また、negotiate 関数で userId を値 {query.user} に設定します。

    • negotiate/function.json を更新して次の json コードをコピーします。
      {
          "bindings": [
              {
                  "authLevel": "anonymous",
                  "type": "httpTrigger",
                  "direction": "in",
                  "name": "req"
              },
              {
                  "type": "http",
                  "direction": "out",
                  "name": "res"
              },
              {
                  "type": "webPubSubConnection",
                  "name": "connection",
                  "hub": "simplechat",
                  "userId": "{headers.x-ms-client-principal-name}",
                  "direction": "in"
              }
          ]
      }
      
    • negotiate/index.js を更新して次のコードをコピーします。
      module.exports = function (context, req, connection) {
          context.res = { body: connection };
          context.done();
      };
      
  5. サービスを使用してクライアント メッセージをブロードキャストするための message 関数を作成します。

    func new -n message -t HttpTrigger
    

    注意

    この関数では、実際には WebPubSubTrigger が使用されています。 ただし、このサービスはまだプレビュー段階にあり、関数のテンプレートには WebPubSubTrigger が組み込まれていません。 HttpTrigger を使用して関数テンプレートを初期化し、コードでトリガーの種類を変更します。

    • message/function.json を更新して次の json コードをコピーします。
      {
          "bindings": [
              {
                  "type": "webPubSubTrigger",
                  "direction": "in",
                  "name": "message",
                  "dataType": "binary",
                  "hub": "simplechat",
                  "eventName": "message",
                  "eventType": "user"
              },
              {
                  "type": "webPubSub",
                  "name": "webPubSubEvent",
                  "hub": "simplechat",
                  "direction": "out"
              }
          ]
      }
      
    • message/index.js を更新して次のコードをコピーします。
      module.exports = async function (context, message) {
          context.bindings.webPubSubEvent = {
              "operationKind": "sendToAll",
              "message": `[${context.bindingData.connectionContext.userId}] ${message}`,
              "dataType": context.bindingData.dataType
          };
          // MessageResponse directly return to caller
          var response = { 
              "message": '[SYSTEM] ack.',
              "dataType" : "text"
          };
          return response;
      };
      
  6. プロジェクトのルート フォルダーにクライアントのシングル ページ index.html を追加し、次のようにコンテンツをコピーします。

    <html>
        <body>
            <h1>Azure Web PubSub Serverless Chat App</h1>
            <div id="login"></div>
            <p></p>
            <input id="message" placeholder="Type to chat...">
            <div id="messages"></div>
            <script>
                (async function () {
                    let authenticated = window.location.href.includes('?authenticated=true');
                    if (!authenticated) {
                        // auth
                        let login = document.querySelector("#login");
                        let link = document.createElement('a');
                        link.href = `${window.location.origin}/.auth/login/aad?post_login_redirect_url=/api/index?authenticated=true`;
                        link.text = "login";
                        login.appendChild(link);
                    }
                    else {
                        // negotiate
                        let messages = document.querySelector('#messages');
                        let res = await fetch(`${window.location.origin}/api/negotiate`, {
                            credentials: "include"
                        });
                        let url = await res.json();
                        // connect
                        let ws = new WebSocket(url.url);
                        ws.onopen = () => console.log('connected');
                        ws.onmessage = event => {
                            let m = document.createElement('p');
                            m.innerText = event.data;
                            messages.appendChild(m);
                        };
                        let message = document.querySelector('#message');
                        message.addEventListener('keypress', e => {
                            if (e.charCode !== 13) return;
                            ws.send(message.value);
                            message.value = '';
                        });
                    }
                })();
            </script>
        </body>
    </html>
    

Create and Deploy the Azure Function App

Before you can deploy your function code to Azure, you need to create 3 resources:

  • A resource group, which is a logical container for related resources.
  • A storage account, which is used to maintain state and other information about your functions.
  • A function app, which provides the environment for executing your function code. A function app maps to your local function project and lets you group functions as a logical unit for easier management, deployment and sharing of resources.

Use the following commands to create these items.

  1. If you haven't done so already, sign in to Azure:

    az login
    
  2. リソース グループを作成します。または、Azure Web PubSub サービスのいずれかを再利用してスキップできます。

    az group create -n WebPubSubFunction -l <REGION>
    
  3. リソース グループとリージョン内に汎用ストレージ アカウントを作成します。

    az storage account create -n <STORAGE_NAME> -l <REGION> -g WebPubSubFunction
    
  4. Azure に関数アプリを作成します。

    az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime node --runtime-version 12 --functions-version 3 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>
    
  5. Azure に関数プロジェクトをデプロイする:

    Azure への関数アプリの作成に成功したら、func azure functionapp publish コマンドを使用して、ローカル関数プロジェクトをデプロイすることができます。

    func azure functionapp publish <FUNCIONAPP_NAME> --publish-local-settings
    

    注意

    ここでは、ローカル設定 local.settings.json をコマンド パラメーター --publish-local-settings と共にデプロイしています。 Microsoft Azure ストレージ エミュレーターを使用している場合は、プロンプト メッセージ App setting AzureWebJobsStorage is different between azure and local.settings.json, Would you like to overwrite value in azure? [yes/no/show] に続いて「no」と入力して、 Azure でこの値の上書きをスキップできます。 さらに、Function App の設定を [Azure portal] -> [設定] -> [構成] で更新できます。

Web PubSub サービス Event Handler を構成する

このサンプルでは、WebPubSubTrigger を使用して、サービスのアップストリーム メッセージ要求をリッスンしています。 そのため、Web PubSub では、ターゲット クライアント要求を送信するために、関数のエンドポイント情報を知る必要があります。 また、Azure Function App には、拡張機能固有の Webhook メソッドに関するセキュリティのためのシステム キーが必要です。 前の手順で message 関数を使用して Function App をデプロイした後、システム キーを取得できます。

Azure portal に移動して、自分の Function App リソースを見つけ、 [アプリ キー] -> [システム キー] -> webpubsub_extension の順に移動します。 <APP_KEY> として、値をコピーします。

関数のシステム キーを取得するスクリーンショット。

Azure Web PubSub サービスに Event Handler を設定します。 Azure portal に移動して、自分の Web PubSub リソースを見つけ、 [設定] に移動します。 次のように、新しいハブ設定と使用中の 1 つの関数とのマッピングを追加します。 <FUNCTIONAPP_NAME><APP_KEY> を自分のものに置き換えます。

  • ハブ名: simplechat
  • URL テンプレート: https://<FUNCTIONAPP_NAME>.azurewebsites.net/runtime/webhooks/webpubsub?code=<APP_KEY>
  • ユーザー イベント パターン: *
  • システム イベント: (このサンプルでは構成する必要はありません)

イベント ハンドラーの設定のスクリーンショット。

注意

ローカルで関数を実行している場合、 関数の開始後にコマンド ngrok http 7071ngrok を使用して関数の URL を公開できます。 また、Web PubSub サービス Event Handlerhttps://<NGROK_ID>.ngrok.io/runtime/webhooks/webpubsub という URL で設定します。

クライアント認証を有効に構成する

Azure portal に移動して、自分の Function App リソースを見つけ、 [認証] に移動します。 ページの下部にある Add identity provider 」の説明に従って、アプリケーションにシングル サインオンできるようになります。 App Service 認証の設定[認証されていないアクセスを許可する] に設定すると、認証にリダイレクトされる前に、匿名のユーザーがクライアントのインデックス ページにアクセスできます。 その後、 [保存] を選択します。

ここでは、ID プロバイダーとして Microsoft を選択します。これにより、negotiate 関数で userId として x-ms-client-principal-name が使用されます。 なお、下のリンクから他の ID プロバイダーを構成することもできます。それに応じて negotiate 関数の userId 値を忘れずに更新してください。

アプリケーションを試す

これで、関数アプリからページをテストできるようになりました (https://<FUNCTIONAPP_NAME>.azurewebsites.net/api/index)。 下のスナップショットを参照してください。

  1. login をクリックして自分を認証します。
  2. 入力ボックスにメッセージを入力してチャットします。

メッセージ関数では、呼び出し元のメッセージをすべてのクライアントにブロードキャストし、呼び出し元にメッセージ [SYSTEM] ack を返します。 そのため、下のサンプルのチャット スナップショットでは、最初の 4 つのメッセージは現在のクライアントからのものであり、最後の 2 つのメッセージは別のクライアントからのものであることがわかります。

チャット サンプルのスクリーンショット。

リソースをクリーンアップする

このアプリの使用を続けない場合は、次の手順に従って、このドキュメントで作成したすべてのリソースを削除して、課金が発生しないようにします。

  1. Azure Portal の左端で [リソース グループ] を選択し、作成したリソース グループを選択します。 検索ボックスを使用して名前でリソース グループを検索することもできます。

  2. 表示されたウィンドウでリソース グループを選択し、 [リソース グループの削除] を選択します。

  3. 新しいウィンドウで、削除するリソース グループの名前を入力し、 [削除] を選択します。

次のステップ

このクイックスタートでは、サーバーレス チャット アプリケーションを実行する方法について説明しました。 これで、独自のアプリケーションの作成を始められます。