はじめに
今回はBlazor ServerをCloud Run上で動作させてマウントさせたGCS上のファイルを一覧で表示する簡易サイトの作り方を紹介したいと思います。
概要
Blazor
はWeb UIコンポーネントを構築するためのWebフレームワークです。.NET使いであればWeb周りもC#で書きたいなーと思うかと思うのですが、それを叶えてくれるMS製フレームワークです。
Blazor は、さまざまな方法でホストできる Web UI コンポーネント (Razor コンポーネント) を構築するための Web フレームワークです。 Razor コンポーネントは、サーバー側では ASP.NET Coreで実行できるのに対し (Blazor Server)、クライアント側では WebAssembly ベースの .NET ランタイム上のブラウザーで実行できます (Blazor WebAssembly、Blazor WASM)。
ASP.NET Core Blazor のホスティング モデル | Microsoft Learn
ドキュメントの説明にもあるとおりいくつかのモードがあるのですが、その中でBlazor Server
を用いるとCloud Run
といったクラウド上での実装がかなり楽になります。といってもそれぞれのモード(代表的なものはBlazor WASM
とBlazor Server
)にはメリデメあるので詳しくは公式ドキュメント等を参照してみてください。
zenn.dev
Blazor Server ホスティング モデルを使用すると、コンポーネントは ASP.NET Core アプリ内からサーバー上で実行されます。 UI の更新、イベント処理、JavaScript の呼び出しは、WebSocket プロトコルを使用して SignalR 接続経由で処理されます。 接続されている各クライアントに関連付けられているサーバー上の状態は、"回線" と呼ばれます。 回線は特定のネットワーク接続に関連付けられず、一時的なネットワークの中断や、接続が切断されたときにクライアントがサーバーに再接続しようとすることを許容します。
ASP.NET Core Blazor のホスティング モデル | Microsoft Learn
今回はBlazor
布教の意味も兼ねて、割と利用されるケースがありそうなGCS上のファイルを表示するサイトを構築していきます。
作り方
.NET 8 Blazor web applicationを作成する
Blazor Server(対話型Serverのモード)であるプロジェクトを作成します。
$ dotnet new blazor -n SampleBlazorApp -f net8.0 --no-https テンプレート "Blazor Web アプリ" が正常に作成されました。 このテンプレートには、Microsoft 以外のパーティのテクノロジーが含まれています。詳しくは、https://aka.ms/aspnetcore/8.0-third-party-notices をご覧ください。 作成後の操作を処理しています... /Users/.../SampleBlazorApp/SampleBlazorApp.csproj を復元しています: Determining projects to restore... /Users/.../SampleBlazorApp/SampleBlazorApp.csproj を復元しました (31 ミリ秒)。 正常に復元されました。
. ├── Components │ ├── App.razor │ ├── Layout │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ └── NavMenu.razor.css │ ├── Pages │ │ ├── Counter.razor │ │ ├── Error.razor │ │ ├── Home.razor │ │ └── Weather.razor │ ├── Routes.razor │ └── _Imports.razor ├── Folder.DotSettings.user ├── Program.cs ├── Properties │ └── launchSettings.json ├── SampleBlazorApp.csproj ├── appsettings.Development.json ├── appsettings.json └── wwwroot ├── app.css ├── bootstrap │ ├── bootstrap.min.css │ └── bootstrap.min.css.map └── favicon.png
GCSのマウントしたファイルを読み込めるようにする
Blazorの細かい説明はしませんが、今回はGCS上のファイルを一覧で表示するStorage.razor
をPages
内に定義をしました。
<!-- Components/Pages/Storage.razor --> @page "/storage" <PageTitle>Storage</PageTitle> <h1>GCS Files</h1> @if (fileNames == null) { <p><em>Loading...</em></p> } else { <ul> @foreach (var fileName in fileNames) { <li>@fileName</li> } </ul> } @code { private string[]? fileNames = null; protected override void OnInitialized() { const string directoryPath = "/mnt/gcs"; fileNames = Directory.Exists(directoryPath) ? Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories) : ["ファイルが見つかりませんでした。"]; } }
本当にSystem.IOでファイルを探しているだけです。GCSをmountさせると、これだけでアクセスできてしまいます。
www.hanachiru-blog.com
またこれほど楽に操作できるのも、Blazor Serverの恩恵です。例えばBlazor WASMだと別途Web API Serverを立てないといけなく、セキュリティ面などで考慮することが多いです。しかしBlazor Serverはサーバー上で動作するため、これだけで良いのです。
サーバーのURL指定
サーバーが直接設定されていない場合にlistenするURLを指定しておきます。なんかわざわざ指定する必要ない(appsettings.json
や環境変数でいける気がする)と思うのですが、私は直接指定してしまいました。あんまり良くないかもです。
ASP.NET Core の Web ホスト | Microsoft Learn
ASP.NET Core Kestrel Web サーバーのエンドポイントを構成する | Microsoft Learn
// Program.cs using SampleBlazorApp.Components; // Kestrelを使用してWebサーバーを作成 var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); // URLを取得 var url = builder.Configuration["Kestrel:Endpoints:Http:Url"] ?? "http://0.0.0.0:8080"; var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); } app.UseStaticFiles(); app.UseAntiforgery(); app.MapRazorComponents<App>() .AddInteractiveServerRenderMode(); app.Run(url);
ポート番号を設定する
appsettings.json
でprodでの設定を変更することができます。appsettings.Development.json
は開発用ですね。
これはMicrosoft.Extensions.Configuration
を利用して実現しています。
learn.microsoft.com
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://*:8080" + } + } + } }
appsettings.json
にKestrel
の項目を追加しました。これはKestrelサーバーのエンドポイント設定を示しています。具体的には、HTTPプロトコルを使用して、ポート8080でリッスンするように設定されています。
Kestrelサーバーは、ASP.NET CoreアプリケーションのためのクロスプラットフォームなWebサーバーですね。HTTP/1.1
、HTTP/2
、HTTP/3
、WebSocket
にWebプロトコルは対応しています。
launchSettings.jsonを用いてデバッグ可能にする
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:11633",
"sslPort": 0
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
+ "applicationUrl": "http://localhost:8080",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
IIS Express
とはMicrosoftが提供する軽量で自己完結型のバージョンのInternet Information Services (IIS)
です。Webページを配信するようなサーバーソフトウェアで、ローカルマシンで簡単に実行できます。
learn.microsoft.com
commandName
がProject
のプロファイルに関しては、dotnet run
コマンドが打ち込まれたものと同等です。またASPNETCORE_ENVIRONMENT
がDevelopment
に設定されているので、appsettings.Development.json
が利用されます。
http
の左側にある再生ボタンを押すと、Blazorが立ち上がって画面が立ち上がるはずです。またなぜこのような設定をするかというと、launchSettings.json
を経由することでブレークポイントなどのデバッグができるので、開発がかなり捗るはずです。(やらなくても大丈夫ではあります)
プロジェクトをビルドする
プロジェクトをLinux向けにビルドします。なぜLinux
かというと、Cloud RunがLinux x64
だからです。
コンテナ イメージ内の実行可能ファイルは、Linux 64 ビット用にコンパイルする必要があります。Cloud Run は特に Linux x86_64 ABI 形式をサポートしています。
https://cloud.google.com/run/docs/container-contract?hl=ja#languages
# -c|--configuration <CONFIGURATION> : ビルド構成 # -f|--framework <FRAMEWORK> : ターゲットフレームワーク # -r|--runtime <RUNTIME_IDENTIFIER> : 指定されたランタイムのアプリケーションをビルド # -o|--output <OUTPUT_DIRECTORY> : 出力ディレクトリのパス # --sc|--self-contained [true|false] : アプリケーションと併せて .NET ランタイムを発行するか $ dotnet publish -c Release -f net8.0 -r linux-x64 -o ./bin/release/net8.0/publish/ --self-contained false Determining projects to restore... /Users/.../SampleBlazorApp/SampleBlazorApp.csproj を復元しました (1.07 秒)。 SampleBlazorApp -> /Users/.../SampleBlazorApp/bin/Release/net8.0/linux-x64/SampleBlazorApp.dll SampleBlazorApp -> /Users/.../SampleBlazorApp/bin/release/net8.0/publish/
といっても基本はDockerfileにてビルド処理を記述するので、自分で毎回叩く必要はありません。
Dockerfileを用意する
ビルド&実行するためのDockerfile
を記述します。上記で実験したdotnet publish
に加えて、それを実行する処理を追加しています。またlinux x64
周りが少しややこしいので注意です。
# .NET SDKのImage(https://mcr.microsoft.com/product/dotnet/sdk/about)を指定, .NET CLI + .NET runtime + ASP.NET Coreから成り立つ FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env ARG TARGETARCH WORKDIR /App # 全てのファイルをコンテナにコピーする COPY . ./ # csprojを見て依存関係を解決する RUN dotnet restore -a $TARGETARCH # ビルドしてpublishする RUN dotnet publish -c Release -o out -a $TARGETARCH # SampleBlazorAppの実行 FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /App COPY --from=build-env /App/out . ENTRYPOINT ["dotnet", "SampleBlazorApp.dll"]
またちゃんと動作確認するには、docker build
してdocker run
します。
# 手元でデバッグ用 $ docker build -t sampleblazorapp-img -f Dockerfile . # linux x64向け (手元で確認するためなら必要ないです) $ docker build -t sampleblazorapp-img -f Dockerfile . --build-arg BUILDPLATFORM=linux --build-arg TARGETARCH=x64 # コンテナを作成して実行 $ docker run -it -p 8088:8088 --name sampleblazorapp-cnt sampleblazorapp-img
Artifact Registoryにアップロードする
以下の記事でも書いたものと同様の手法をとっていきます。
【GCP, terraform】Cloud Runをterraformで構築して.NETで構築した最小構成のウェブサーバーをデプロイする - はなちるのマイノート
クイックスタート: Docker コンテナ イメージを Artifact Registry に保存する | Artifact Registry documentation | Google Cloud
まずはgcloudがインストールされている前提でログインをします。
$ gcloud auth login $ gcloud init
まだレポジトリを作成していない場合は作成します。
$ gcloud artifacts repositories create blazor-sample --location=asia-northeast1 --repository-format=docker Create request issued for: [blazor-sample] Waiting for operation [projects/---] to complete...done. Created repository [blazor-sample].
以下コマンドでDocker用のArtifact Registory認証を行い、build &pushを行います。
# Docker用Artifact Registory認証 $ gcloud auth configure-docker asia-northeast1-docker.pkg.dev # docker buildの際に、Mac M1なら --platform linux/amd64 を付与してください $ docker build ./ -t asia-northeast1-docker.pkg.dev/{PROJECT_ID}/blazor-sample/blazor-sample-image # push $ docker push asia-northeast1-docker.pkg.dev/{PROJECT_ID}/blazor-sample/blazor-sample-image
正しく動作すればArtifact Registoryに上がっているはずです。
terraformによりCloudRunを構築する
あとはこれらを組み合わせて、Cloud Runを構築します。
. ├── main.tf ├── outputs.tf ├── provider.tf └── variables.tf
↓ main.tf
# GCSバケット構築 resource "google_storage_bucket" "default" { # 重複できないので適当にnameを変える必要があります name = "blazor-sample" location = var.region force_destroy = true } # Cloud Run構築 resource "google_cloud_run_v2_service" "default" { name = "blazor-sample" location = var.region ingress = "INGRESS_TRAFFIC_ALL" deletion_protection = false template { service_account = google_service_account.cloud_run_service_account.email execution_environment = "EXECUTION_ENVIRONMENT_GEN2" containers { # Artifact RegisotryにあげたImageを指定する image = "asia-northeast1-docker.pkg.dev/----/blazor-sample/blazor-sample-image:latest" volume_mounts { name = "bucket" mount_path = "/mnt/gcs" } } volumes { name = "bucket" gcs { bucket = google_storage_bucket.default.name read_only = false } } } } # 外部からアクセスするようにIAM Policy設定 # allUsers(全てのユーザー)にroles/run.invoker(サービスとジョブの呼び出し、ジョブ実行のキャンセルが可能)を付与する data "google_iam_policy" "noauth" { binding { role = "roles/run.invoker" members = ["allUsers"] } } # IAMポリシーをCloud Runに適応する resource "google_cloud_run_v2_service_iam_policy" "policy" { location = var.region name = google_cloud_run_v2_service.default.name policy_data = data.google_iam_policy.noauth.policy_data } # Cloud Run用のサービスアカウントを作成 resource "google_service_account" "cloud_run_service_account" { account_id = "cloud-run-service-account" display_name = "Cloud Run Service Account" } # Cloud RunのサービスアカウントにGCSのAdmin権限を付与 resource "google_project_iam_member" "cloud_run_storage_admin" { project = var.project_id role = "roles/storage.objectAdmin" member = "serviceAccount:${google_service_account.cloud_run_service_account.email}" } # Cloud Runのサービスアカウントにroles/iam.serviceAccountUserロールを付与する resource "google_project_iam_member" "cloud_run_service_account_user" { project = var.project_id role = "roles/iam.serviceAccountUser" member = "serviceAccount:${google_service_account.cloud_run_service_account.email}" }
↓outputs.tf
output "service_url" { value = google_cloud_run_v2_service.default.uri description = "The URL on which the deployed service is available" }
↓provider.tf
terraform { required_providers { google = { source = "hashicorp/google" version = "6.2.0" } } } provider "google" { project = var.project_id region = var.region }
↓ variables.tf
variable "project_id" { default = "<your project id>" description = "Project ID" } variable "region" { default = "asia-northeast1" description = "Region" }
これらを実行するには以下コマンドを打ち込みます。
$ terraform init $ terraform apply ... Apply complete! Resources: 6 added, 0 changed, 0 destroyed. Outputs: service_url = "https://----run.app"
結果
正しく実行されるとOutputs
にURLが表示されるはずです。それを開くとBlazor Serverにより構築されたサイトが見れるはずです。
またGCS上にファイルを配置すると、それも表示してくれます。
さいごに
特定の範囲にだけ見れるようにしたい場合は、Cloud Load Balancing
とIdentity-Aware Proxy
を利用すれば実現できます。
どっかのタイミングでそこら辺も書きたいなと思ってます。