🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

Azure DevOpsでClient Side BlazorをCI/CDしてみる

こちらの記事の続きです。

blog.piyosi.com

本エントリでは

GitHubに作ったClient Side BlazorのPrivate Repositoryのmaster branchへのコミットをトリガーにして、Azure DevOpsでBuild, UnitTest, Publishした後、Azure Storageの静的サイトへ自動デプロイする。」

というCI/CDのベーシック(?)な流れを設定してみたので、その設定方法を記録として残します。

あくまで記録なので、大した説明もしていませんのでご留意ください。

続きを読む

Client Side BlazorをAzureの静的サイトにデプロイする

Azure StorageにClient Side Blazorをデプロイします。

Client Side Blazorの作成

Blazorのプロジェクトを作成し、Publish(発行)しておきます。

dotnet new blazorwasm -o ClientSideBlazorSample
cd ClientSideBlazorSample
dotnet publish -c Release -o out

Azure CLIのインストール

AzureをCLIで操作するためにAzure CLIをインストールします。

docs.microsoft.com

私はChocolately経由で入れた覚えがあります。

Chocolatelyでのインストールコマンドはこちら。

choco install azure-cli

Azure Storageを設定

az login
az group create -n {ResouceGroup名} -l japaneast
az storage account create -n {StorageAccount名} -g {ResouceGroup名} --kind StorageV2 -l japaneast --https-only true --sku Standard_LRS

今回は以下のように設定しました。

ResouceGroup名 : ClientSideBlazorSample
StorageAccount名 : blazorsamplestorage
※Storage名は文字列長3-24の英数の小文字のみ使用可能です。

設定が終わると、Azureでリソースグループに追加されているはずです。

f:id:piyo_esq:20191028080907p:plain
追加したResource Group

静的Webサイトの設定

Azure DevOpsから追加したStorage Accountのページを開きます。

追加したResourceGroupからStorage Accountを選択し、静的なWebサイトを選択します。

静的なWebサイトを有効にし、インデックス ドキュメント名とエラー ドキュメントのパスをindex.htmlに指定します。

f:id:piyo_esq:20191028103237p:plain
静的なWebサイトの設定

保存ボタンを押下すると、プライマリ エンドポイントが表示されます。

このエンドポイントがBlazorのURLになります。

Deploy

ファイルをアップロードすることでアプリをデプロイします。
デプロイするオブジェクトはdistフォルダ以下のファイル群です。

PowerShellの場合は以下の通りです。

cd out\ClientSideBlazorSample\dist
az storage blob upload-batch --account-name {StorageAccount名} -s . -d `$web

注意点としては、使用しているTerminalの種類に応じて-dのEscape文字を変更する必要があります。

blog.piyosi.com

WASMのContent Typeの設定

ファイルのアップロードが完了しましたが、前途したコマンドでアップロードした場合

_framework\wasm\mono.wasmのContent Typeはapplication/octet-streamになってしまいます。

確認のコマンドは以下の通り。

 az storage blob show --account-name {StorageAccount名} -c `$web -n _framework\wasm\mono.wasm

f:id:piyo_esq:20191028081421p:plain
mono.wasmのプロパティ

Microsoft Azure Storage Explorerで確認した画面は以下の通り。

f:id:piyo_esq:20191028103502p:plain
Azure Storage Explorerでmono.wasmの確認

以下のコマンドでwasmのContent Typeをapplication/wasmに変更します。
例によって、-dのオプションは使用しているTerminalに応じて書き換えてください。

az storage blob update --account-name {StorageAccount名} -c `$web -n _framework/wasm/mono.wasm --content-type application/wasm

完成

先ほどのエンドポイントにアクセスします。

f:id:piyo_esq:20191028081520p:plain
DeployしたClient Side Blazor

おわり

Client Size Blazorを簡単にDeployすることができました

Azure CLI経由でファイルをアップロードするとContent Typeを変更しなければならない点が面倒ですね。
Microsoft Azure Storage Explorerでアップロードすると、きちんとContent Typeが設定されるんですけどねー...。

追記:2019/10/29

@jsakamoto様の仰る通り、ポイントですね。

エラードキュメントのパスをindex.htmlに指定するのは、404とかのエラー発生時にBlazor内で用意したエラー画面を表示するためです。
※Blazorにエラーハンドリングを任せています。

試しに存在しないアドレスにアクセスしてみます。

f:id:piyo_esq:20191029054226p:plain
Blazorによる404のハンドリング結果

Blazorがちゃんとエラーハンドリングしてくれています。
※このページはApp.razorのタグの内容です。

もちろん別途Htmlファイルを用意してAzureのエラードキュメントのパスを用意したHtmlパスを指定すれば、そちらのエラー画面が表示されます。

az storage blob upload-batchでErrorCode: InvalidUriが出た時の原因

Azure StorageのBlob Containerにファイルをアップロードしようとした際に以下のエラーが表示されました。

error: The requested URI does not represent any resource on the server. ErrorCode: InvalidUri

原因

原因は-dで指定したContainer NameのEscape文字でした...。

$webという静的サイトのContainerを指定しようとしていたのですが、先頭につけるEscape文字が異なっていました。

Terminal -dオプション
Powershell -d `$web
CommandLine -d $web
Basb -d \$web

参考はこちら

github.com

おわり

Escape文字とは盲点でした...お恥ずかしい。

.NET Core 3.0の新機能Roll Forwardオプションを試してみる

.NET Core 3.0 リリース記念 C# Tokyoで登壇された@nuits_jpさんの.NET Core 3.0のPublish Single File概要を見て、そういえば.NET Core 3.0の新機能でRuntimeのバージョンをコントロールする機能あったなぁ…と思い、ちょっと調べてみました。

Microsoft Docsでいうと、このあたりの話です。

docs.microsoft.com

RollForwardとは?

.NET Coreアプリの実行時に使用するRuntimeバージョンは.runtimeconfig.jsonで指定されたFrameworkバージョンを元にフォワードされます。このフォワードされるルールをコントロールするというオプションがRollForwardのようです。

実際に試してみる

インストール済み SDK の確認

下記コマンドでインストール済みのSDKを表示できます。

dotnet --list-sdks

私の環境ではこんな感じでした。

> dotnet --list-sdks
2.1.402 [C:\Program Files\dotnet\sdk]
2.1.602 [C:\Program Files\dotnet\sdk]
2.1.801 [C:\Program Files\dotnet\sdk]
2.2.101 [C:\Program Files\dotnet\sdk]
2.2.105 [C:\Program Files\dotnet\sdk]
2.2.202 [C:\Program Files\dotnet\sdk]
2.2.401 [C:\Program Files\dotnet\sdk]
3.0.100-preview7-012821 [C:\Program Files\dotnet\sdk]
3.0.100 [C:\Program Files\dotnet\sdk]

作成アプリのバージョン指定

.NET Core のプロジェクトを作成します。
使用するSDKのバージョンはあえて古いバージョンで作成します。

mkdir RFSample
cd RFSample
dotnet new global

生成されたglobal.jsonを以下の内容に変更します。

{
  "sdk": {
    "version": "2.1.402"
  }
}

アプリの作成

以下のコマンドでコンソールアプリケーションを作成します。

dotnet new console

コードはこんな感じです

using System;

namespace sample
{
    class Program {
        static void Main (string[] args) {
            Console.WriteLine(typeof(Object).Assembly.Location);
        }
    }
}

dotnet runで実行すると、私の環境ではこんな感じになります。

> cd .\bin\Debug\netcoreapp2.1
> dotnet RFSample.dll
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.13\System.Private.CoreLib.dll

global.jsonSDKのバージョンは2.1.402を指定しましたが、実行時のRuntimeは2.1.13を使用しているっぽいですね。

ちなみにインストール済み Runtime の一覧はdotnet --list-runtimesで確認できます。

> dotnet --list-runtimes
Microsoft.AspNetCore.All 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
... 途中割愛 ...
Microsoft.AspNetCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 3.0.0-preview7-27912-14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.0.0-preview7-27912-14 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

--roll-forward を使ってみる

Microsoft Docs によると RollForward の指定は複数の方法でできるようです。

  • プロジェクト ファイルのプロパティ: RollForward
  • ランタイム構成ファイルのプロパティ: rollForward
  • 環境変数: DOTNET_ROLL_FORWARD
  • コマンドライン引数: --roll-forward

ここでは一番簡単なコマンド引数の--roll-forwardを使用して実行してみます。

まずはLatestMajorから。

> dotnet --roll-forward LatestMajor .\RFSample.dll
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.0.0\System.Private.CoreLib.dll

使用する Runtime が2.1.13から3.0.0に変わりましたね!
色々試してみましょう。

> dotnet --roll-forward LatestPatch .\RFSample.dll
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.13\System.Private.CoreLib.dll

> dotnet --roll-forward Minor .\RFSample.dll
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.13\System.Private.CoreLib.dll

> dotnet --roll-forward LatestMinor .\RFSample.dll
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.2.7\System.Private.CoreLib.dll

> dotnet --roll-forward Major .\RFSample.dll
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.13\System.Private.CoreLib.dll

> dotnet --roll-forward LatestMajor .\RFSample.dll
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.0.0\System.Private.CoreLib.dll

> dotnet --roll-forward Disable .\RFSample.dll
It was not possible to find any compatible framework version
The specified framework 'Microsoft.NETCore.App', version '2.1.0' was not found.
  - The following frameworks were found:
      2.1.4 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
      2.1.9 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
      2.1.12 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
      2.1.13 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
      2.2.0 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
      2.2.3 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
      2.2.5 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
      2.2.6 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
      2.2.7 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
      3.0.0-preview7-27912-14 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
      3.0.0 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]

You can resolve the problem by installing the specified framework and/or SDK.

The .NET Core frameworks can be found at:
  - https://aka.ms/dotnet-download

変化をちゃんと網羅できていませんが、まぁこんなもんでしょう。

Disable2.1.0のRuntimeがないので怒られてしまいましたね。
Disable一般的な用途としては非推奨なのでテスト用途に使ってね、らしいのです。

.NET Core 2.1で作ったMVCアプリを.NET Core 3.0にRollForwardしたらどうなるのか?

当たり前ですが動作しません。

以下は.NET Core 2.1で作ったASP.NET Core MVC--roll-forward LatestMajorを指定することで強制的に.NET Core 3.0で環境で動かしたログです。

> dotnet --roll-forward LatestMajor MVCSample.dll
Application startup exception: System.InvalidOperationException: Endpoint Routing does not support 'IApplicationBuilder.UseMvc(...)'. To use 'IApplicationBuilder.UseMvc' set 'MvcOptions.EnableEndpointRouting = false' inside 'ConfigureServices(...).
   at Microsoft.AspNetCore.Builder.MvcApplicationBuilderExtensions.UseMvc(IApplicationBuilder app, Action`1 configureRoutes)
   at MVCSample.Startup.Configure(IApplicationBuilder app, IHostingEnvironment env) in C:\Users\piyosi\work\RFSample\MVCSample\Startup.cs:line 55
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Microsoft.AspNetCore.Hosting.ConfigureBuilder.Invoke(Object instance, IApplicationBuilder builder)
   at Microsoft.AspNetCore.Hosting.ConfigureBuilder.<>c__DisplayClass4_0.<Build>b__0(IApplicationBuilder builder)
   at Microsoft.AspNetCore.Hosting.ConventionBasedStartup.Configure(IApplicationBuilder app)
   at Microsoft.AspNetCore.Mvc.Filters.MiddlewareFilterBuilderStartupFilter.<>c__DisplayClass0_0.<Configure>g__MiddlewareFilterBuilder|0(IApplicationBuilder builder)
   at Microsoft.AspNetCore.HostFilteringStartupFilter.<>c__DisplayClass0_0.<Configure>b__0(IApplicationBuilder app)
   at Microsoft.AspNetCore.Hosting.WebHost.BuildApplication()
crit: Microsoft.AspNetCore.Hosting.WebHost[6]
      Application startup exception
System.InvalidOperationException: Endpoint Routing does not support 'IApplicationBuilder.UseMvc(...)'. To use 'IApplicationBuilder.UseMvc' set 'MvcOptions.EnableEndpointRouting = false' inside 'ConfigureServices(...).
   at Microsoft.AspNetCore.Builder.MvcApplicationBuilderExtensions.UseMvc(IApplicationBuilder app, Action`1 configureRoutes)
   at MVCSample.Startup.Configure(IApplicationBuilder app, IHostingEnvironment env) in C:\Users\piyosi\work\RFSample\MVCSample\Startup.cs:line 55
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Microsoft.AspNetCore.Hosting.ConfigureBuilder.Invoke(Object instance, IApplicationBuilder builder)
   at Microsoft.AspNetCore.Hosting.ConfigureBuilder.<>c__DisplayClass4_0.<Build>b__0(IApplicationBuilder builder)
   at Microsoft.AspNetCore.Hosting.ConventionBasedStartup.Configure(IApplicationBuilder app)
   at Microsoft.AspNetCore.Mvc.Filters.MiddlewareFilterBuilderStartupFilter.<>c__DisplayClass0_0.<Configure>g__MiddlewareFilterBuilder|0(IApplicationBuilder builder)
   at Microsoft.AspNetCore.HostFilteringStartupFilter.<>c__DisplayClass0_0.<Configure>b__0(IApplicationBuilder app)
   at Microsoft.AspNetCore.Hosting.WebHost.BuildApplication()
Unhandled exception. System.InvalidOperationException: Endpoint Routing does not support 'IApplicationBuilder.UseMvc(...)'. To use 'IApplicationBuilder.UseMvc' set 'MvcOptions.EnableEndpointRouting = false' inside 'ConfigureServices(...).
   at Microsoft.AspNetCore.Builder.MvcApplicationBuilderExtensions.UseMvc(IApplicationBuilder app, Action`1 configureRoutes)
   at MVCSample.Startup.Configure(IApplicationBuilder app, IHostingEnvironment env) in C:\Users\piyosi\work\RFSample\MVCSample\Startup.cs:line 55
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Microsoft.AspNetCore.Hosting.ConfigureBuilder.Invoke(Object instance, IApplicationBuilder builder)
   at Microsoft.AspNetCore.Hosting.ConfigureBuilder.<>c__DisplayClass4_0.<Build>b__0(IApplicationBuilder builder)
   at Microsoft.AspNetCore.Hosting.ConventionBasedStartup.Configure(IApplicationBuilder app)
   at Microsoft.AspNetCore.Mvc.Filters.MiddlewareFilterBuilderStartupFilter.<>c__DisplayClass0_0.<Configure>g__MiddlewareFilterBuilder|0(IApplicationBuilder builder)
   at Microsoft.AspNetCore.HostFilteringStartupFilter.<>c__DisplayClass0_0.<Configure>b__0(IApplicationBuilder app)
   at Microsoft.AspNetCore.Hosting.WebHost.BuildApplication()
   at Microsoft.AspNetCore.Hosting.WebHost.StartAsync(CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Hosting.WebHostExtensions.RunAsync(IWebHost host, CancellationToken token, String startupMessage)
   at Microsoft.AspNetCore.Hosting.WebHostExtensions.RunAsync(IWebHost host, CancellationToken token, String startupMessage)
   at Microsoft.AspNetCore.Hosting.WebHostExtensions.RunAsync(IWebHost host, CancellationToken token)
   at Microsoft.AspNetCore.Hosting.WebHostExtensions.Run(IWebHost host)
   at MVCSample.Program.Main(String[] args) in C:\Users\piyosi\work\RFSample\MVCSample\Program.cs:line 18

ディスコンされた関数を使用している場合もきっと同じでしょう。

おわり

バグ修正された新しいバージョンで動作させたいとか、そんな感じのシチュエーションで使用するのでしょうか?

個人的には--self-containedやら/p:PublishSingleFileでRuntimeを同封して配布することが多いので、すぐに使う機会は少ないかなぁ、といった感想です。

以上です。