チュートリアル: 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 アカウントで 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-zA-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 --model V4
    
  2. Microsoft.Azure.WebJobs.Extensions.WebPubSubをインストールする。

    host.json の extensionBundle を確認し、バージョン 4.* 以降に更新して、Web PubSub サポートを取得します。

    {
      "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle",
        "version": "[4.*, 5.0.0)"
      }
    }
    
  3. クライアントの静的 Web ページを読み取ってホストする index 関数を作成します。

    func new -n index -t HttpTrigger
    
    • src/functions/index.js を更新して次のコードをコピーします。
      const { app } = require('@azure/functions');
      const { readFile } = require('fs/promises');
      
      app.http('index', {
          methods: ['GET', 'POST'],
          authLevel: 'anonymous',
          handler: async (context) => {
              const content = await readFile('index.html', 'utf8', (err, data) => {
                  if (err) {
                      context.err(err)
                      return
                  }
              });
      
              return { 
                  status: 200,
                  headers: { 
                      'Content-Type': 'text/html'
                  }, 
                  body: content, 
              };
          }
      });
      
  4. クライアントがアクセス トークンを含むサービス接続 URL を取得するのに役立つ negotiate 関数を作成します。

    func new -n negotiate -t HttpTrigger
    

    Note

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

    • src/functions/negotiate を更新して次のコードをコピーします。
      const { app, input } = require('@azure/functions');
      
      const connection = input.generic({
          type: 'webPubSubConnection',
          name: 'connection',
          userId: '{headers.x-ms-client-principal-name}',
          hub: 'simplechat'
      });
      
      app.http('negotiate', {
          methods: ['GET', 'POST'],
          authLevel: 'anonymous',
          extraInputs: [connection],
          handler: async (request, context) => {
              return { body: JSON.stringify(context.extraInputs.get('connection')) };
          },
      });
      
  5. サービスを使用してクライアント メッセージをブロードキャストするための message 関数を作成します。

    func new -n message -t HttpTrigger
    
    • src/functions/message.js を更新して次のコードをコピーします。
      const { app, output, trigger } = require('@azure/functions');
      
      const wpsMsg = output.generic({
          type: 'webPubSub',
          name: 'actions',
          hub: 'simplechat',
      });
      
      const wpsTrigger = trigger.generic({
          type: 'webPubSubTrigger',
          name: 'request',
          hub: 'simplechat',
          eventName: 'message',
          eventType: 'user'
      });
      
      app.generic('message', {
          trigger: wpsTrigger,
          extraOutputs: [wpsMsg],
          handler: async (request, context) => {
              context.extraOutputs.set(wpsMsg, [{
                  "actionName": "sendToAll",
                  "data": `[${context.triggerMetadata.connectionContext.userId}] ${request.data}`,
                  "dataType": request.dataType
              }]);
      
              return {
                  data: "[SYSTEM] ack.",
                  dataType: "text",
              };
          }
      });
      
  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>
    

Azure 関数アプリを作成してデプロイする

関数コードを Azure にデプロイする前に、3 つのリソースを作成する必要があります。

  • リソース グループ。関連リソースの論理コンテナーです。
  • ストレージ アカウント。関数についての情報 (状態など) を維持する目的で使用されます。
  • 関数アプリ。関数コードを実行するための環境となります。 関数アプリは、ローカルの関数プロジェクトと対応関係にあります。これを使用すると、リソースの管理、デプロイ、共有を容易にするための論理ユニットとして関数をグループ化できます。

以下のコマンドを使用してこれらの項目を作成します。

  1. まだ Azure にサインインしていない場合は、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 18 --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>
    

    Note

    Azure Functions ランタイム バージョンに関するドキュメントを確認して、--runtime-version パラメーターをサポートされている値に設置します。

  5. Azure に関数プロジェクトをデプロイする:

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

    func azure functionapp publish <FUNCIONAPP_NAME>
    
  6. 関数アプリ用に WebPubSubConnectionString を構成します。

    最初に、Azure portal から Web PubSub リソースを見つけ、 [キー] から接続文字列をコピーします。 次に、Azure portal[設定][構成]、[関数アプリの設定] の順に移動します。 [アプリケーション設定] の下に新しい項目を追加します。名前は WebPubSubConnectionString にし、値は Web PubSub リソースの接続文字列とします。

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>
  • ユーザー イベント パターン: *
  • システム イベント: (このサンプルでは構成する必要はありません)

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

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

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. 新しいウィンドウで、削除するリソース グループの名前を入力し、 [削除] を選択します。

次のステップ

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