使用 Azure Functions 搭配 Azure SignalR Service 實現無伺服器架構 | The Will Will Web

The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

使用 Azure Functions 搭配 Azure SignalR Service 實現無伺服器架構

要在 Azure 實現即時連線式的無伺服器架構(Serverless architecture)其實相當容易,只要學會 Azure Functions 的開發方式,搭配著 Azure SignalR Service 就可以輕鬆實現,本篇文章我將說明實現這個架構的相關說明與注意事項。

開發環境

要開發 Azure Functions 建議先安裝以下開發工具:

  1. Visual Studio Code

    choco install vscode -y
    

    請一併安裝 Azure Functions 擴充套件

  2. Azure Functions Core Tools v3

    choco install azure-functions-core-tools -y
    

    Azure Functions runtime versions overview

  3. Azure Storage Emulator

    由於 Azure Functions 在執行時需要 Azure Storage 才能運作,在本機開發時建議你先啟動 Azure Storage Emulator 再來執行測試。

    我在安裝 Visual Studio 2019 的時候都會安裝 Azure development 工作負載(Workload),所以預設就會安裝 Azure Storage Emulator 起來。如果你沒有安裝的話,可以用以下命令進行安裝,或是直接下載 MSI 檔進行安裝。

    choco install azurestorageemulator -y
    

    這個 Chocolatey 套件雖然顯示稍微舊版,但事實上預設會安裝 Azure Storage Emulator 最新版!

    第一次使用 Azure Storage Emulator 的人,可以參考以下兩個命令初始化並啟動模擬器:

    "C:\Program Files (x86)\Microsoft SDKs\Azure\Storage Emulator\AzureStorageEmulator.exe" init
    "C:\Program Files (x86)\Microsoft SDKs\Azure\Storage Emulator\AzureStorageEmulator.exe" start
    

    如果你有啟動 Docker Desktop for Windows 的話,可能會佔用 10000 (Blob), 10001 (Queue), 10002 (Table) 這三個本機通訊埠(Ports),若遇到這種情況,只要重開機就能解決。(重開治百病)

  4. .NET Core SDK 3.1

    choco install dotnetcore-sdk -y
    
  5. 建立 Azure SignalR Service 服務

開發 Azure Functions 應用程式

  1. 初始化 Azure Functions 應用程式

    mkdir demo1
    cd demo1
    func init --worker-runtime dotnet
    

    這個步驟會建立 5 個檔案,其中 local.settings.json 會儲存「應用程式組態」設定,預設會被 .gitignore 排除在版控之外,要特別注意!

    .vscode\extensions.json
    .gitignore
    demo1.csproj
    host.json
    local.settings.json
    

    這裡的 host.json 用來定義 Azure Functions 的執行環境相關組態(Configurations),而 local.settings.json 則是只會存留於本機的組態設定,當你要將 Function App 部署到 Azure App Service 時,這些組態要手動註冊到 App Service 的應用程式組態設定中!

  2. 啟動 Visual Studio Code 編輯器並初始化開發 Azure Functions 所需的編輯器環境

    初始化開發 Azure Functions 所需的編輯器環境

  3. 加入 Microsoft.Azure.WebJobs.Extensions.SignalRService 套件 (NuGet)

    dotnet add package Microsoft.Azure.WebJobs.Extensions.SignalRService
    
  4. 加入 Functions.cs 程式碼

    using System;
    using System.IO;
    using System.Net.Http;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Azure.WebJobs;
    using Microsoft.Azure.WebJobs.Extensions.Http;
    using Microsoft.Azure.WebJobs.Extensions.SignalRService;
    using Newtonsoft.Json;
    
    namespace CSharp
    {
        public static class Function
        {
            private static HttpClient httpClient = new HttpClient();
    
            [FunctionName("index")]
            public static IActionResult Index([HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req, ExecutionContext context)
            {
                var path = Path.Combine(context.FunctionAppDirectory, "content", "index.html");
                return new ContentResult
                {
                    Content = File.ReadAllText(path),
                    ContentType = "text/html",
                };
            }
    
            [FunctionName("negotiate")]
            public static SignalRConnectionInfo Negotiate(
                [HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req,
                [SignalRConnectionInfo(HubName = "serverlessSample")] SignalRConnectionInfo connectionInfo)
            {
                return connectionInfo;
            }
    
            [FunctionName("broadcast")]
            public static async Task Broadcast([TimerTrigger("*/5 * * * * *")] TimerInfo myTimer,
            [SignalR(HubName = "serverlessSample")] IAsyncCollector<SignalRMessage> signalRMessages)
            {
                var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/azure/azure-signalr");
                request.Headers.UserAgent.ParseAdd("Serverless");
                var response = await httpClient.SendAsync(request);
                var result = JsonConvert.DeserializeObject<GitResult>(await response.Content.ReadAsStringAsync());
                await signalRMessages.AddAsync(
                    new SignalRMessage
                    {
                        Target = "newMessage",
                        Arguments = new[] { $"Current star count of https://github.com/Azure/azure-signalr is: {result.StarCount}" }
                    });
            }
    
            private class GitResult
            {
                [JsonRequired]
                [JsonProperty("stargazers_count")]
                public string StarCount { get; set; }
            }
        }
    }
    

    注意:呼叫 GitHub API 時,在未通過身份驗證的狀態下,一小時只能發出 60 個 Requests 而已喔!你可以用 curl -I https://api.github.com/repos/azure/azure-signalr 查出你的目前 IP 還剩餘多少 API 的 RateLimit 呼叫次數(X-RateLimit-Remaining)以及下次重置時間(X-RateLimit-Reset)。

  5. 加入 content/index.html 預設首頁 HTML 原始碼

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Azure SignalR Serverless Sample</title>
    </head>
    <body>
    
      <h1>Azure SignalR Serverless Sample</h1>
      <div id="messages"></div>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.20/signalr.min.js" integrity="sha512-Tmi7k+eXKdibvPJfx/L8uCogEzWFQvC+heO3urDkTr5px1QfpwY1auftPq85zpGG++KbekSpPL7wn6bOS0oJVg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
      <script>
        let messages = document.querySelector('#messages');
        const apiBaseUrl = window.location.origin;
        const connection = new signalR.HubConnectionBuilder()
            .withUrl(apiBaseUrl + '/api')
            .configureLogging(signalR.LogLevel.Information)
            .build();
          connection.on('newMessage', (message) => {
            document.getElementById("messages").innerHTML = message;
          });
    
          connection.start()
            .catch(console.error);
      </script>
    
    </body>
    </html>
    

    關於 microsoft-signalr 的 CDN 位址可以從 https://cdnjs.com/libraries/microsoft-signalr/3.1.20 取得。

    SignalR 用戶端函式庫的 .withUrl(apiBaseUrl + '/api') 會自動找出 apiBaseUrl + '/api/negotiate' 進行協商,這個過程會呼叫 /api/negotiate 這個 API 並取得 Azure SignalR Service 的服務網址與 Access Token,接著就會讓瀏覽器用戶端直接連接 Azure SignalR Service 服務。

  6. 更新 demo1.csproj 並設定 content/index.html 檔案會複製到輸出目錄

    <ItemGroup>
      <None Update="content/index.html">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      </None>
    </ItemGroup>
    
  7. local.settings.json 加入名為 AzureSignalRConnectionString 的組態設定

    func settings add AzureSignalRConnectionString "<signalr-connection-string>"
    

    注意:上述命令只會修改 local.settings.json 這個檔案,並在該檔案加入名為AzureSignalRConnectionString 的應用程式參數,這個參數主要提供給 SignalR Service bindings for Azure Functions 之用。

  8. 在本機啟動 Azure Functions 應用程式

    func start
    

    由於 Azure Function 在本機開發時,預設使用 7071 通訊埠,如果你不幸這個 Port 被佔用的話,一樣重開機可能會好。或者你也可以改另一個 Port 進行測試,例如:

    func start -p 17071
    

    func start -p 17071

部署 Azure Functions 應用程式

  1. 安裝 Azure CLI 命令列工具

    由於部署 Azure Functions 的時候,需要先安裝好 Azure CLI 命令列工具,並在 az login 登入後選好預設訂用帳戶 (Subscription)

    choco install azure-cli -y
    
    az login --use-device-code
    
    az account set -s "Microsoft Azure Sponsorship"
    
  2. 建立 Function App

    你可以透過 Azure Portal 先建立好 Serverless 的 Function App,或是直接透過 Azure CLI 建立 Function App 服務。

    以下命令先建立「資源群組」,再建立「儲存體帳戶」,再建立名為 mydemo1func 的 Serverless Function App:

    az group create -n demo1 -l japaneast
    az storage account create -n myfuncstor1 -g demo1 -l japaneast --sku Standard_LRS
    az functionapp create -n mydemo1func -s myfuncstor1 -g demo1 -c japaneast --functions-version 3
    

    透過 Azure CLI 建立 Function App 的方法可以參考 Create a function app for serverless code execution 文件,如果要建立 Premium plan 的 Function App 可以參考 Create a function app in a Premium plan - Azure CLI 文件。

  3. 將現有 Function App 發行到 Azure Functions 中

    func azure functionapp publish mydemo1func --publish-local-settings
    

    加入 --publish-local-settings 參數可以將你的 local.settings.json 組態設定全部自動同步到 Azure 上,執行過程如果看到 Would you like to overwrite value in azure? [yes/no/show] 提示,請記得輸入 no 才對,否則你的 AzureWebJobsStorage 組態會被設定為本機開發環境的 Storage 連接字串!

相關連結