Azure App Service Static Web Apps に Blazor WASM と Azure Functions をデプロイする


1 つ前の記事で Vue.js + Azure Functions(中身は express) を Azure App Service Static Web Apps (長いので以下 Static Web Apps) にデプロイしてみました。そして気づいたのですが C# で SPA を開発するための Blazor WebAssembly が GA してました!!アツイ!!

ただ、これは .NET Core 3.1 (LTS) で動くけど Blazor WASM は LTS じゃない点が注意ですね。

.NET 5 が出たら .NET 5 に移らないとサポートが切れちゃう。あと .NET 5 も LTS じゃないので .NET 6 が出たら .NET 6 に行かないといけないはず。.NET 6 は LTS の予定。

まぁそれは置いといて、つまり Static Web Apps に Blazor WASM を置けるかもということです。API も Azure Functions でまとめてデプロイ出来ていい感じ。ただ、2020/05/20 時点の Public Preview の Static Web Apps は Azure Functions のランタイムが node.js 固定っぽい?ので、Azure Functions 側は JavaScript か TypeScript で作らないといけないみたいです。ちょっと残念。(記事書きながら C# で進めてたらデプロイのところでダメだと気づいて記事を書きなおしたりしてる)

やってみよう

ということで Blazor WASM + Azure Functions (TypeScript) を Static Web Apps にデプロイしてみようと思います。

適当なフォルダー(私は C:Labs\BlazorStaticWebApps というフォルダー使いました)で以下のコマンドを打ちます。

> dotnet new blazorwasm -o Client -n StaticWebApps.Client
> mkdir Server
> cd Server
> func init --language typescript --worker-runtime node

これでクライアントとサーバーの両方のプロジェクトが出来ました。クライアントが Blazor WebAssembly でサーバーが C# の Azure Functions です。

次のコマンドをうって、サーバーの方に GetMessage 関数を作りましょう。

> func new -l typescript -n GetMessage -t HttpTrigger

デフォルトで以下のような GET と POST を受け取り name パラメーターか POST の場合は Body の JSON の name の値を元にメッセージを返す関数が作られます。

GetMessage.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions"

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    context.log('HTTP trigger function processed a request.');
    const name = (req.query.name || (req.body && req.body.name));

    if (name) {
        context.res = {
            // status: 200, /* Defaults to 200 */
            body: "Hello " + (req.query.name || req.body.name)
        };
    }
    else {
        context.res = {
            status: 400,
            body: "Please pass a name on the query string or in the request body"
        };
    }
};

export default httpTrigger;

今回はサーバー側はこれをそのまま使いましょう。次はクライアント側です。

Index.razor を開いて GetMessage 関数を叩くようにします。これも非常にシンプルですが、こんな感じで。

Index.razor
@page "/"
@using Microsoft.Extensions.Configuration
@inject HttpClient Http
@inject IConfiguration Configuration

<h1>Hello, world!</h1>

Welcome to your new app.

<p>@Message</p>

@code {
    private string Message { get; set; }
    protected override async Task OnInitializedAsync()
    {
        var res = await Http.GetAsync($"{Configuration.GetValue<string>("API")}/GetMessage?name=BlazorWASM");
        Message = res.IsSuccessStatusCode ?
            await res.Content.ReadAsStringAsync() :
            "Failed";
    }
}

API の呼び先は、構成ファイルから読むようにしました。Blazor WASM の単体で実行するときには、特に Proxy を設定するような項目はなさそうなので、開発時はローカルの Azure Functions を呼んで、本番は自分と同じドメインのやつを呼ぶようにしました。

ということで、wwwroot の下に appsettings.jsonappsettings.Development.json を置いて以下のような内容にします。

appsettings.json
{
    "API": "/api"
}
appsettings.Development.json
{
    "API": "http://localhost:7071/api"
}

そして、Server 側の local.settings.json に CORS の設定を追加します。

local.settings.json
{
    "IsEncrypted": false,
    "Values": {
        "FUNCTIONS_WORKER_RUNTIME": "node",
        "AzureWebJobsStorage": "UseDevelopmentStorage=true"
    },
    "Host": {
        "CORS": "*",
        "CORSCredentials": false
    }
}

Server 側を起動させましょう。以下のコマンドでビルドして実行できます。

> npm install
> npm run build
> func host start

そして、クライアント側をデバッグ実行すると…

うまくいきましたね!!

Static Web Apps にデプロイ

じゃぁデプロイしてみましょう。とりあえず GitHub にソースを push します。ここにしました。

とりあえず、こんな感じでビルドの設定はしました。このまま作成して作られる GitHub Actions ではエラーになるので、まぁとりあえずひな型作ってくれるくらいの気持ちで入れました。

GitHub Actions に移動して YAML にちょっと追記します。

name: Azure Static Web Apps CI/CD

on:
  push:
    branches:
    - master
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
    - master

jobs:
  build_and_deploy_job:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
    - uses: actions/checkout@v2
    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.300
    - uses: actions/checkout@v1
    - name: Build Client
      run: dotnet publish ./Client/StaticWebApps.Client.csproj -c Release -o dist/website
    - name: Build And Deploy
      id: builddeploy
      uses: Azure/[email protected]
      with:
        azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_RED_SMOKE_0EB760D00 }}
        repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
        action: 'upload'
        ###### Repository/Build Configurations - These values can be configured to match you app requirements. ######
        app_location: 'dist/website/wwwroot' # App source code path
        api_location: 'Server' # Api source code path - optional
        ###### End of Repository/Build Configurations ######

  close_pull_request_job:
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    name: Close Pull Request Job
    steps:
    - name: Close Pull Request
      id: closepullrequest
      uses: Azure/[email protected]
      with:
        azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_RED_SMOKE_0EB760D00 }}
        action: 'close'

追加したのは .NET Core 3.1.300 を入れるステップと、Blazor WASM のプロジェクトをビルドして静的 Web サイトとしてデプロイ出来るファイルを生成するステップです。
そして、Static Web Apps へのデプロイのための設定の app_location に dotnet publish したフォルダーにある wwwroot を設定します。api_location は TypeScript の場合も自動でビルドしてデプロイしてくれるみたいなので、素直に Server と指定するだけで大丈夫でした。

GitHub Actions が成功したのを見届けて Static Web Apps の URL を開いてみると、ちゃんと動いてました!GetMessage API も、ちゃんとクラウドのを叩いてるのがわかりますね。

まとめ

ということで、Blazor WASM の正式版がリリースされて、さらに Azure App Service Static Web Apps も public preview になってたのでデプロイだけしてみました。
API は、今のところ node の Azure Functions じゃないといけないみたいなのですが、GA までにはランタイムが選べるようになると嬉しいなぁ。

後、純粋に Blazor WASM を API と共に開発してデプロイするなら ASP.NET Core でホストするプロジェクトテンプレートで作って、ASP.NET Core で API を作って Azure App Service の Web Apps にデプロイするのが一番楽だと思うということを最後に書いておこうと思います。