BlazorアプリケーションをNetlifyにデプロイする(Netlify Identityによる認証サンプル付き)

# 概要

Blazor WebAssemblyアプリケーションと静的サイトのホスティングサービスのNetlifyに関して下記の点を説明しようと思います。

- NetlifyにBlazorアプリケーションをデプロイする方法の紹介
- Netlifyの認証サービス(Netlify Identity)をBlazorで使用してログイン機能の実装


# 背景

Blazor WebAssembly含めSPAアプリケーションのウリの一つとして、従来のWEBアプリケーションのようなサーバ実行環境を必要とせず、htmlファイルなど静的なファイルをホスティングするだけのサービスでアプリケーションがデプロイできる点が挙げられます。

AWS,GCP,Azure等のメジャーなクラウドサービスも静的サイトをホスティングするサービスを提供していますが、個人のちょっとした趣味で開発する上ではシンプルかつ無料もしくは、無料枠が大きいサービスを使用できるのが望ましいです。

そこでよく取り上げられるのが、Firebase,GitHub Page,Netlifyといった静的サイトのホスティングサービスになるかと思います。

Blazorの例ですとGitHub Page、Firebaseのデプロイの手順を見かけます。
(手前味噌ですが以前私のほうでも記事を書いています。)
AdventCalenderで@jsakamotoさんがGitHub Pageで簡単にデプロイできるためのライブラリをNugetで公開していますね。
[Blazor WebAssembly アプリの GitHub Pages への発行を、より楽にする](https://qiita.com/jsakamoto/items/0be0005d9b30acbfe1e4)

今回はNetlifyにBlazorアプリケーションをデプロイする方法と認証機能の実装を試してみたいと思います。
本題ではないため詳細は省きますが、Netlifyの特徴として下記の点などが挙げられます。

- 無料枠が大きい
- SPAで使用するための認証機能やREST API(AWS Lambdaベース)が提供されている
- CD/CI機能との連携

一方でFirebaseで提供されている、DBなどのデータストア機能は提供されていないので別サービスを使用する必要があります。

# NetlifyにBlazorアプリケーションをデプロイする方法

## 前提条件

- 対象はBlazor WebAssembly
- GitHub Actionによる連携 (GitHub上のソースコードをNetlifyにデプロイする。)

## 事前準備

予め下記を実施しておいてください。

- BlazorのソースコードをGitHub上にリポジトリとして作成する
- Netlifyのアカウントを作成済みでGitHubアカウントと連携させる
 - (無料プランだけであればクレカ不要でGitHubアカウントでサインアップするだけで簡単に作成可能です。)

## 1.各種トークンの取得

今回はGitHub Actionとの連携を行い、GitHubにコミットしたソースをビルドしてNetlifyにデプロイします。
そのためにはGitHub Action側に連携を行うための各種情報が必要になるので取得を行います。

### トークンの発行

[Netlifyの個人ページ](https://app.netlify.com/user/applications/personal)にアクセスして、New 　access tokenを選択します。


![netli1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/a9f8d4c9-2009-ec03-aca0-d8f5110da9d1.png)

入力欄にNETLIFY_AUTH_TOKENと入力して、Generate tokenを選択します。
![netli2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/58f67000-2df0-4d1a-8740-20e9258ba95b.png)

トークンが発行されるのでメモします。
![netli3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/f78233fc-7b51-9882-d890-29e9630d1f51.png)

### サイトIDの取得

次にサイトIDの取得を行います。
先にIDの取得だけしたいので、フォルダと空のindex.htmlファイルを作成してください。

```console

dummy (任意のフォルダ名)
|- index.html (空のファイル)

```

[Siteのページ](https://app.netlify.com/teams/nobu17/sites)にアクセスして、Want to...のエリアにdummyフォルダをドラッグ＆ドロップしてください。

![netli4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/bb5e8ff7-a160-1273-f986-751a735f7673.png)

ダミーのサイトが作成されます。
https://～のURLが今後デプロイ後にも使われるURLなのでメモします。
次にSite settingsを選択します。

![netli5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/a7963f9f-14ba-1d1c-50fe-602572a73852.png)

API IDの部分に記載された値をメモします。

![netli6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/6dd5c387-d630-7764-6567-7976ededac96.png)

## 2.GitHub Actionの設定

Netlifyから取得した情報を登録してGitHub Actionでデプロイできるように設定します。

### GitHubへのシークレットの登録

デプロイしたいBlazorのGitHubレポジトリを開いて、Settings→SecretsからNew repository Sercretを選択します。

![netli7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/4a0b778b-6fee-9ccb-1688-db775821dc19.png)

Netlifyで取得した２つのキーを登録します。
なお。APPIDはNETLIFY_SITE_IDというキーで設定します。

![netli8.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/1cfa83df-a641-cc18-7df4-6349fa9a9cb6.png)

下記のように２つのSecretsが登録できればOKです。
![netli9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/6491ae8d-9a71-181f-6803-9993293fdb64.png)


### GitHub Actionの作成

Actionのタブを選択します。

![netli10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/2af1c938-e751-691c-ebe3-c03aa457e687.png)

スクロールして、More continuous integration workflows...を選択して .NET Coreの項目を見つけてSet up this workflowを選択します。 

![netli12.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/65fc0e26-2e81-bb29-306d-76bf0f6ec7d7.png)

dotnet-core.ymlの編集画面が表示されます。
このymlファイルを編集して、BlazorアプリをNetlifyにデプロイできるように設定します。

![netli13.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/90a8aed1-1cee-c025-a924-a8b0f0f8841c.png)

具体的に実施することとしては、下記になります。

- dotnetコマンドを使用してビルドして、発行
- netlifyのCLIでデプロイ

下記の例では、.NET5.0.100でビルドを行ってデプロイしています。

```yml

name: BlazorNetlifySample #任意の名称を指定
on: [push] #push時に実行

jobs:
  build:
    runs-on: ubuntu-latest #最新のubuntu環境をビルドに使用。(バージョンを固定したほうが良いかも)
    steps:
    - uses: actions/checkout@v1
    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 5.0.100 #ビルドに用いる.NETバージョン(2020/12/08時点での最新SDK ver)
    - name: Build with dotnet
      run: dotnet build --configuration Release
    - name: Publish Blazor webassembly using dotnet 
      #distディレクトリにpublish
      run: dotnet publish -c Release --no-build -o dist
    - name: Publish generated Blazor webassembly to Netlify
      uses: netlify/actions/cli@master #uses Netlify Cli actions
      env: # Netlifyの設定を環境変数として定義
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
      with:
          args: deploy --dir=dist/wwwroot --prod #dist/wwwrootディレクトリをデプロイ
          secrets: '["NETLIFY_AUTH_TOKEN", "NETLIFY_SITE_ID"]' 

```

ymlファイルの編集が終わったら、Start Commit → Commit new fileを選択します。

![netli14.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/371d0c6f-c8a5-4eb8-32f5-a68393509a58.png)


以上で、Actionの設定は完了です。
今後GitHubにpushする度に自動でデプロイが実行されます。
(本格的に使用するならば、relaseブランチやmainブランチなど特定のブランチに限定してデプロイするなどの設定を行う必要があるかと思います。)

## ビルドの確認



Actionタブを選択
![action.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/d1e7dba5-e04f-f849-ca42-5151b575fd54.png)

Create dotnet-core.ymlが☑マークになっていればビルド完了です。
(ビルドに2-3分程度かかります。)

![action2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/673fe7ee-9d5e-83a6-91d7-ca15c045b954.png)

Netlifyの設定時にメモしたURLにアクセスして下記のようにBlazorアプリが起動すればデプロイ成功です。
![netli15.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/1e819255-aaef-cde7-1d15-cb3ccc7cb815.png)


以降、GitHubのmainにpushするたびにこのワークフローが実行されてNetlifyにデプロイされます。

![git.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/12a29210-b95a-ea5e-de33-a5cc5c4dcb34.png)

個々のコマンド実行時の実行結果が確認できるので、どの処理に問題があるのかも判別しやすいかと思います。

![build.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/b7751524-9b56-fee6-19fc-8b7536e40bb2.png)


Netlifyにデプロイする手順は以上です。

# Netlifyの認証サービス(Netlify Identity)をBlazorで使用してログイン機能の実装

次にNetlifyの機能をBlazorに組み込む方法を考えてみましょう。
Netlifyのサービスの１つであるNetlify Identityを使ってログイン機能の実装を考えてみます。
Netlify Identityには、下記の方法でログイン機能が実装できます。

- [ウィジェット](https://github.com/netlify/netlify-identity-widget)の使用
- [GoTrue API](https://github.com/netlify/gotrue-js)の使用

ウィジェットの使用を使用することでjsファイルの参照とタグを配置するだけでお手軽にログインが実装できますが、Blazorの動的にタグを配置するような仕組みだとうまくコンポーネントに適応されないようで諦めて、後者のGoTrue APIによる実装を行いました。

初期アカウントの登録等に関しては割愛しますので、別途登録等を行ってください。
既に登録済みのアカウントを使用した例を紹介します。

1. [GitHub](https://github.com/netlify/gotrue-js/tree/master/browser)からgotrue.jsファイルを取得して、wwwwroot/jsに配置してindex.htmlに参照を追加。
2. 下記のようなGoTrueAPIをラップしたjsを作成する

```javascript:netlifyIdentity.js

let auth;
function initAuth() {
    auth = new GoTrue({
        APIUrl: 'https://取得したホスティングのURL/.netlify/identity',
        setCookie: true
    });
}

async function loginAsync(email, passowrd) {
    return await new Promise((resolve, reject) => {
        auth.login(email, passowrd, true)
            .then((response) => {
                const result = { accessToken: response.token.access_token, userId: email };
                resolve(result);
            })
            .catch((error) => {
                reject(error);
            });
    });
}

async function logoutAsync() {
    return await new Promise((resolve, reject) => {
        const user = auth.currentUser();
        if (user) {
            resolve(null);
            return;
        }
        user.logout()
            .then(response => {
                resolve(response);
            })
            .catch(error => {
                reject(error);
            });
    });
}

function getCurrentUser() {
    const user = auth.currentUser();
    if (user) {
        return { accessToken: user.token.access_token, userId: user.email }
    } else {
        return null;
    }
}

```

次に、これらを呼び出すサービスをクラスを作成します。
AuthenticationStateProviderなどの実装に関しては以前の認証方法に関して記載した記事で説明しているので参照ください。
https://qiita.com/nobu17/items/91c96ede1bd043fe1373

```csharp:NetlifyAuthService.cs

    public class NetlifyAuthService : IAuthService
    {
        private readonly IJSRuntime _JSRuntime;
        private readonly AuthenticationStateProvider _authenticationStateProvider;

        public NetlifyAuthService(AuthenticationStateProvider authenticationStateProvider, IJSRuntime jSRuntime)
        {
            _authenticationStateProvider = authenticationStateProvider;
            _JSRuntime = jSRuntime;
        }

        public async Task InitAsync()
        {
            await _JSRuntime.InvokeVoidAsync("initAuth");
            var user = await _JSRuntime.InvokeAsync<NetlifyLoginResult>("getCurrentUser");
            if (user == null)
            {
                await LogoutAsync();
            }
        }

        public async Task<LoginResult> LoginAsync(LoginModel loginModel)
        {
            try
            {
                var result = await _JSRuntime.InvokeAsync<NetlifyLoginResult>("loginAsync", new object[] { loginModel.UserID, loginModel.Password }).ConfigureAwait(false);
                // トークンを取得
                var res = new LoginResult()
                {
                    IsSuccessful = true,
                    IDToken = result.AccessToken
                };
                await ((SpaAuthticateProvider)_authenticationStateProvider).MarkUserAsAuthenticated(loginModel.UserID, res.IDToken);
                return res;
            }
            catch (Exception e)
            {
                return new LoginResult()
                {
                    IsSuccessful = false,
                    Error = e
                };
            }
        }

        public async Task LogoutAsync()
        {
            await _JSRuntime.InvokeAsync<string>("logoutAsync", new object[] { }).ConfigureAwait(false);
            await ((SpaAuthticateProvider)_authenticationStateProvider).MarkUserAsLoggedOut();
        }
    }

    [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
    public class NetlifyLoginResult
    {
        public string UserId { get; set; }
        public string AccessToken { get; set; }
    }

```

これを組み込むことで下図のようなログイン機能を実装しています。

![login2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/16031fe7-6e3e-9a04-1712-71ca90101ba5.gif)

時間がなかったため、かなり雑で本当に少し参考になる程度ですが下記にソースコードも入れていますので興味ある方は参照ください。

[GitHub Link](https://github.com/nobu17/BlazorNetlifySample)

# まとめ

Blazor WebAssemblyをGitHub Actionを使用してNetlifyにデプロイする方法を紹介しました。
GitHubと連携してデプロイ可能な点は便利ですね。
今後、Functionとの連携なども試す事ができればと思います。

下記のページなどを参考に試してみましたので、より詳細な情報をなどを確認したい場合には下記も合わせて参照ください。

[Hosting Blazor on Netlify using Github Actions (Part 1 of Series)](https://dev.to/ramhemasri/hosting-blazor-on-netlify-using-github-actions-part-1-of-series-3a1k)
[Deploying a .NET Core Blazor App to Netlify using GitHub Actions](https://exceptionnotfound.net/deploying-a-net-core-blazor-app-to-netlify-using-github-actions/)