次の方法で共有


チュートリアル: サーバーレス モードの Azure Functions でチャット アプリを構築する (プレビュー)

このチュートリアルでは、サーバーレス モードで Web PubSub for Socket.IO サービスを作成し、Azure Functions と統合するチャット アプリを構築する方法について説明します。

このチュートリアルで使用されている完全なコード サンプルについては、以下を参照してください。

重要

デフォルト モードには永続的なサーバーが必要です。デフォルト モードの Web PubSub for Socket.IO を Azure Functions と統合することはできません。

重要

この記事に示す生の接続文字列は、デモンストレーションのみを目的としています。

接続文字列には、アプリケーションが Azure Web PubSub サービスにアクセスするために必要な認可情報が含まれています。 接続文字列内のアクセス キーは、サービスのルート パスワードに似ています。 運用環境では、常にアクセス キーを保護してください。 Azure Key Vault を使ってキーの管理とローテーションを安全に行い、WebPubSubServiceClient を使って接続をセキュリティ保護します

アクセス キーを他のユーザーに配布したり、ハードコーディングしたり、他のユーザーがアクセスできるプレーンテキストで保存したりしないでください。 キーが侵害された可能性があると思われる場合は、キーをローテーションしてください。

前提条件

サーバーレス モードの Web PubSub for Socket.IO リソースを作成する

Web PubSub for Socket.IO を作成するには、次の Azure CLI コマンドを使用できます。

az webpubsub create -g <resource-group> -n <resource-name>--kind socketio --service-mode serverless --sku Premium_P1

Azure Functions プロジェクトをローカルに作成する

ローカルの Azure Functions プロジェクトを開始するには、この手順に従う必要があります。

  1. 最新の Azure Functions Core Tools をインストールする手順に従う

  2. ターミナル ウィンドウまたはコマンド プロンプトで次のコマンドを実行して、SocketIOProject フォルダーにプロジェクトを作成します。

    func init SocketIOProject --worker-runtime javascript --model V4
    

    このコマンドを実行すると JavaScript プロジェクトが作成されます。 フォルダー SocketIOProject に移動して、次のコマンドを実行します。

  3. 現時点では、関数バンドルには Socket.IO 関数バインドが含まれていないため、パッケージを手動で追加する必要があります。

    1. 関数バンドル リファレンスを削除するには、host.json ファイルを編集し、次の行を削除します。

      "extensionBundle": {
          "id": "Microsoft.Azure.Functions.ExtensionBundle",
          "version": "[4.*, 5.0.0)"
      }
      
    2. 次のコマンドを実行します。

      func extensions install -p Microsoft.Azure.WebJobs.Extensions.WebPubSubForSocketIO -v 1.0.0-beta.4
      
  4. ネゴシエーションのための関数を作成します。 クライアントによるサービスへのアクセスを可能にするエンドポイントとトークンを生成するために使用されるネゴシエーション関数です。

    func new --template "Http Trigger" --name negotiate
    

    src/functions/negotiate.js でファイルを開き、次のコードに置き換えます。

    const { app, input } = require('@azure/functions');
    
    const socketIONegotiate = input.generic({
        type: 'socketionegotiation',
        direction: 'in',
        name: 'result',
        hub: 'hub'
    });
    
    async function negotiate(request, context) {
        let result = context.extraInputs.get(socketIONegotiate);
        return { jsonBody: result };
    };
    
    // Negotiation
    app.http('negotiate', {
        methods: ['GET', 'POST'],
        authLevel: 'anonymous',
        extraInputs: [socketIONegotiate],
        handler: negotiate
    });
    

    この手順では、HTTP トリガーと negotiate 出力バインドを使用して関数 SocketIONegotiation を作成します。つまり、HTTP 呼び出しを使用して関数をトリガーし、SocketIONegotiation バインドによって生成されたネゴシエーション結果を返すことができます。

  5. メッセージを渡す関数を作成します。

    func new --template "Http Trigger" --name message
    

    ファイル src/functions/message.js を開き、次のコードに置き換えます。

    const { app, output, trigger } = require('@azure/functions');
    
    const socketio = output.generic({
    type: 'socketio',
    hub: 'hub',
    })
    
    async function chat(request, context) {
        context.extraOutputs.set(socketio, {
        actionName: 'sendToNamespace',
        namespace: '/',
        eventName: 'new message',
        parameters: [
            context.triggerMetadata.socketId,
            context.triggerMetadata.message
        ],
        });
    }
    
    // Trigger for new message
    app.generic('chat', {
        trigger: trigger.generic({
            type: 'socketiotrigger',
            hub: 'hub',
            eventName: 'chat',
            parameterNames: ['message'],
        }),
        extraOutputs: [socketio],
        handler: chat
    });
    

    これは、Socket.IO クライアント メッセージによってトリガーされるように SocketIOTrigger を使用し、SocketIO バインドを使用して名前空間にメッセージをブロードキャストします。

  6. アクセス用のインデックス HTML を返す関数を作成します。

    1. public の下にフォルダー src/ を作成します。

    2. 次の内容を含む HTML ファイル index.html を作成します。

      <html>
      
      <body>
      <h1>Socket.IO Serverless Sample</h1>
      <div id="chatPage" class="chat-container">
          <div class="chat-input">
              <input type="text" id="chatInput" placeholder="Type your message here...">
              <button onclick="sendMessage()">Send</button>
          </div>
          <div id="chatMessages" class="chat-messages"></div>
      </div>
      <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
      <script>
          function appendMessage(message) {
          const chatMessages = document.getElementById('chatMessages');
          const messageElement = document.createElement('div');
          messageElement.innerText = message;
          chatMessages.appendChild(messageElement);
          hatMessages.scrollTop = chatMessages.scrollHeight;
          }
      
          function sendMessage() {
          const message = document.getElementById('chatInput').value;
          if (message) {
              document.getElementById('chatInput').value = '';
              socket.emit('chat', message);
          }
          }
      
          async function initializeSocket() {
          const negotiateResponse = await fetch(`/api/negotiate`);
          if (!negotiateResponse.ok) {
              console.log("Failed to negotiate, status code =", negotiateResponse.status);
              return;
          }
          const negotiateJson = await negotiateResponse.json();
          socket = io(negotiateJson.endpoint, {
              path: negotiateJson.path,
              query: { access_token: negotiateJson.token }
          });
      
          socket.on('new message', (socketId, message) => {
              appendMessage(`${socketId.substring(0,5)}: ${message}`);
          })
          }
      
          initializeSocket();
      </script>
      </body>
      
      </html>
      
    3. HTML ページを返すには、関数を作成し、コードをコピーします。

      func new --template "Http Trigger" --name index
      
    4. ファイル src/functions/index.js を開き、次のコードに置き換えます。

      const { app } = require('@azure/functions');
      
      const fs = require('fs').promises;
      const path = require('path')
      
      async function index(request, context) {
          try {
              context.log(`HTTP function processed request for url "${request.url}"`);
      
              const filePath = path.join(__dirname,'../public/index.html');
              const html = await fs.readFile(filePath);
              return {
                  body: html,
                  headers: {
                      'Content-Type': 'text/html'
                  }
              };
          } catch (error) {
              context.log(error);
              return {
                  status: 500,
                  jsonBody: error
              }
          }
      };
      
      app.http('index', {
          methods: ['GET', 'POST'],
          authLevel: 'anonymous',
          handler: index
      });
      
      

アプリをローカルで実行するには

コードの準備が完了したら、手順に従ってサンプルを実行します。

Azure Functions 用の Azure Storage を設定する

Azure Functions が機能するには、ローカルで実行されている場合でも、ストレージ アカウントが必要になります。 次の 2 つのオプションのいずれかを選択します。

  • 無料の Azurite エミュレーターを実行します。
  • Azure Storage サービスを使用します。 使い続けるとコストが発生する可能性があります。
  1. Azurite をインストールします
npm install -g azurite
  1. Azurite ストレージ エミュレーターを起動します。
azurite -l azurite -d azurite\debug.log
  1. AzureWebJobsStorageUseDevelopmentStorage=true に設定されていることを確認します。

Web PubSub for Socket.IO の構成を設定する

  1. Function APP に接続文字列を追加します。
func settings add WebPubSubForSocketIOConnectionString "<connection string>"
  1. Web PubSub for Socket.IO にハブ設定を追加する
az webpubsub hub create -n <resource name> -g <resource group> --hub-name hub --event-handler url-template="tunnel:///runtime/webhooks/socketio" user-event-pattern="*"

接続文字列は、Azure CLI コマンドで取得できます。

az webpubsub key show -g <resource group> -n <resource name>

出力には primaryConnectionStringsecondaryConnectionString が含まれており、いずれかを使用できます。

トンネルを設定する

サーバーレス モードでは、サービスは Webhook を使用して関数をトリガーします。 アプリをローカルで実行しようとするときの重要な問題は、サービスがローカルの関数エンドポイントにアクセスできるようにすることです。

それを達成する最も簡単な方法は、トンネル ツールを使用することです

  1. トンネル ツールのインストール:

    npm install -g @azure/web-pubsub-tunnel-tool
    
  2. トンネルを実行する

    awps-tunnel run --hub hub --connection "<connection string>" --upstream http://127.0.0.1:7071
    

    --upstream は、ローカルの Azure Functions で公開される URL です。 ポートが異なる場合があり、次の手順で関数を開始するときに出力を確認できます。

サンプル アプリを実行する

トンネル ツールが実行されたら、Function App をローカルで実行できます。

func start

次に http://localhost:7071/api/index の Web ページにアクセスします。

サーバーレス チャット アプリのスクリーンショット。

次の手順

次に、Bicep を使用して、ID ベースの認証でオンラインでのアプリのデプロイを試してみましょう。