Blazor WebAssembly(ASP.NET Coreホスト)を無料利用可能なFly.ioにデプロイする(EF Core+PostgreSQL使用)

# 概要
今回はBlazor WebAssembly(ASP.NET Coreホスト)を、RDBMS含めて無料で利用可能なFly.ioへでデプロイする方法を紹介します。

- Fly.ioへデプロイ (テンプレートそのまま)
- Entity Framework Coreを使用した簡単なアプリによるPostgreSQLへの接続

の２段階に分けて説明します。


## Blazor WebAssemblyのホスティング方法
Blazor WebAssemblyは生成したファイルをそのままウェブサーバーに配置する方法と、ASP.NET Core上でホストして動かす方法があります。（以後スタンドアロン版、ホスト版と呼びます。）
それぞれの特徴は下記のようになります。

- スタンドアロン版
    - .NET実行環境が不要
    - FirebaseやGitHub.ioなどの静的ウェブサイトのホスティングサービスでお手軽にアプリがデプロイ可能
- ホスト版
    - .NET実行環境が必要
    - ASP.NET Coreで作成したREST APIとの連携やデプロイがやりやすい


今回はホスト版のBlazor WebAssemblyを利用することとします。
この中で問題となるのがホビー目的で無料で使用したい場合に、デプロイできるサービスが限られてしまうことです。
スタンドアロン版は前述の通り実行環境の選択肢が多く、FirebaseやGitHub.ioなど選択肢が数多くあります。


そこで、今回紹介するFly.ioはこのような問題において解決策の１つになります。

## Fly.ioって？

Webアプリケーションの実行プラットフォームを提供するサービスです。

https://fly.io

Ruby on Railsなど一部のフレームワークは手軽にデプロイできる一方で、dockerfileを使用する事で様々なプラットフォームに対しても利用可能なフレキシブルさがあります。
このようなサービスで有名なのはHerokuになりますが、今年になって無料プランが廃止となり、その代替サービスの１つとしてよく聞かれるようになりました。

### 特徴

アプリの実行環境はもちろんのこと、RDBMS(PostgreSQL)やRedisも無料枠が用意されているのが特徴です。
AWSやAzure等にもアプリの実行環境の無料枠はありますが、NoSQLではないRDBMSに関しては無料で使えるのは期間限定であったりなど、制限が厳しかったと思います。(あまり細かくは調べてないので間違ってたらすみません。)

そういった意味でFly.ioは両方に関して無料枠があるため、ホスト版Blazorを使用すれば、下記のような構成のアプリが無料で作れるようになります！
- フロントエンド:Blazor
- サーバーサイド:ASP.NET Core (+RDBMS)


なお無料枠で使用可能なのは3つのVMとなります。(RDBMSを使用する場合は1つのVMを使用する形となる。)
詳細は公式のドキュメントを参照してください。

https://fly.io/docs/about/pricing/

# 手順

## 実施環境

- MacOS 12.6.1
- Visual Studio 2022 for Mac
- .NET 7


実施環境はMacですが、コマンドベースですの基本的に他のOSでも同じになると思います。



## ★STEP-1 Fly.ioへのデプロイ

まずは、素のテンプレートアプリをFly.ioへデプロイするまでを紹介します。

### Fly.ioへのサインアップ
未実施の方は下記のURLから実施してください。 (GitHubアカウント連携で簡単に登録できます。)

https://fly.io/app/sign-up

### flyctlのインストール
Fly.ioにおける各種操作を行うためのCLIツールです。
OSごとのインストール手順が公式のドキュメントにあるので、インストールしてください。

https://fly.io/docs/hands-on/install-flyctl/

インストール後に下記のコマンドで、アカウントのサインインを行なってください。
```
flyctl auth login
```

### Blazorプロジェクトの作成と公開用のファイルの作成

Blazor WebAssemblyをプロジェクトを作成します。
ASP.NET Core Hostedにチェックを入れるのを忘れずに。
(下記はVisual Studio for Macの画面ですが、Visual Studioでも同様なチェックボックスがあります。)
![スクリーンショット 2022-12-09 21_Fotor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/7f253387-ebee-7ce6-22b8-973dc7c431dd.png)

### 設定変更

Serverプロジェクトの構成ファイルを編集します。
ASP.NET Coreの事例ですが、無料プランのメモリ使用量は256MBしかないので、メモリ使用量を減らすのにガーベージコレクションの設定を変更した方が良いようです。

参考
https://community.fly.io/t/deploying-asp-net-core-app-in-fly-io-in-free-tier/5856

``` (App名).Server.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    ...

    <!-- 下記を追加 -->
    <ServerGarbageCollection>false</ServerGarbageCollection>
  </PropertyGroup>

</Project>
```

HTTPSのリダイレクト機能は使用しないので、エントリーポイントから削除します。
(やらなくても動きはするので任意で。)


```csharp:Program.cs

// app.UseHttpsRedirection();

```



### 公開用ファイルの作成

今回はそのままテンプレートのプロジェクトをデプロイします。
公開用のファイルをdotnetコマンド等で作成します。
(IDEから公開で作成しても問題ありません。)

```bash
# <App名>は作成したものに置き換えてください。

dotnet publish "<App名>/Server/<App名>.Server.csproj" --configuration "Release" --output "bin/Release/net7.0/publish"
```

今回は下記のディレクトリにファイルを出力しています。
"App名(作成時の名称)/Server/bin/Release/net7.0/publish"

### dockerfileの作成

実行環境のdockerfileをServerプロジェクトの直下に作成します。

```:ディレクトリ
App名
 |- Server
 |  |- dockerfile
```


```dockerfile:dockerfile
# ASP.NET Core 7.0用のイメージを使用
FROM mcr.microsoft.com/dotnet/aspnet:7.0

# appディレクトリを作成し、その中に先ほど作成したファイル一式をコピー
RUN mkdir /app
WORKDIR /app
COPY ./bin/Release/net7.0/publish .

# 4040ポートで実行
EXPOSE 4040
ENV ASPNETCORE_URLS=http://+:4040
ENTRYPOINT ["dotnet", "(App名).Server.dll"]
```

4040ポートは他の番号でも構いませんが、後ほど別の設定で使用するので覚えておいてください。


### デプロイのための設定作成

実際にFly.ioで実行するためのアプリケーション構成などを作成していきます。


```bash
# 予めカレントディレクトリはサーバプロジェクト直下にする
flyctl launch
```

以下対話的に入力を行なっていきます。
```bash

# 任意のアプリケーション名を入力（例：flyblazor
Choose an app name (leave blank to generate one): 
 flyblazor

# アプリケーションを動かすリージョンを選択。Tokyo, Japan (nrt)で良いかと思います。
Choose a region for deployment:  [Use arrows to move, type to filter]
 > Tokyo, Japan (nrt)

# PostgreSQLを使用したい場合はy (後から個別に作ることもできます。)
Would you like to set up a Postgresql database now? (y/N) 
 y

# 上でyを選択した場合。無料なのはDevelopmentなのでそれを選択
Select configuration:  [Use arrows to move, type to filter]
 > Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk

# しばらくすると下記のようなDBの接続情報が表示されるのでメモっておく。
# アプリから使用した場合には下記の接続情報でDBに接続可能になる。
Postgres cluster ********-db created
  Username:    ********
  Password:    ******** 
  Hostname:    ********-db.internal
  Proxy port:  5432
  Postgres port:  5433
  Connection string: postgres://********:********@********-db.internal:5432


# Redis(NoSQL)を使用するかどうか？ 今回は使用しない。
Would you like to set up an Upstash Redis database now? (y/N) 
 N

# この後設定をするので Nを入力
Would you like to deploy now? (y/N) 
 N

```

上記の対話式の入力が終わると、fly.tomlファイルが作成されています。


### tomlファイルの編集

作成されたfly.tomlファイルを編集します。
編集が必要なのは、internal_portの設定で、dokckerfileに設定した公開ポートを指定します。
実際に外部で使用されるのは80,443ポートですが、その際に4040ポートに飛ばすことでASP.NET Core側の処理が動くように設定しています。

```toml:fly.toml
# internal_port以外は基本的にCLIが生成したデフォルト値

app = "(設定したアプリ名)"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[env]

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 4040 # このポートをdockerfileで設定したポートに合わせる。
  processes = ["app"]
  protocol = "tcp"
  script_checks = []
  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"


```


### デプロイ
これで準備はできました。あとは下記のコマンドでデプロイするだけです。

```bash
flyctl deploy
```

初回はコンテナのイメージビルド等で少し時間がかかりますが、完了後に(https\://XXXX.fly.dev)にアクセスしてBlazorのいつもの画面が表示されれば成功です。
(XXXXは対話形式の入力で設定したアプリケーション名)

テンプレート側で実装されているASP.NET Core側のREST APIとの連携画面も無事動いています。
![スクリーンショット 2022-12-12 8_Fotor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/4db0430c-0cbe-1a06-cb92-11fbdf5e7144.png)


以降は公開ファイルを再度作成するたびに、deployコマンドを実行すれば変更が反映されます。

## ★STEP-2 Entity Framefork(EF)を使用したPostgreSQLへの接続

先ほど作成を行なった、PostgreSQLへの接続を行う簡単なアプリを作ってみましょう。
(BlazorというよりもASP.NET Core側の話になっていしまいます。ご了承ください。)

### 各種インストールと設定

EF用のCLIを使用するので、未インストールの場合インストールします。

```bash
dotnet tool install --global dotnet-ef
```

ServerプロジェクトにEF関連のパッケージをNugetから追加します。
```bash
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 7.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 7.0.0
```

先ほどメモしたPostgreSQLの接続情報をappsetting.jsonに設定します。
なお、[flyctl secrets set]コマンドで環境変数の設定が可能なのでそちらを使用して設定する方法でも可能だと思います。
詳細は下記を参照してください。
https://fly.io/docs/flyctl/secrets-set/

```json:appsetting.json
{
  ...
  "ConnectionStrings": {
    "WeatherDatabase": "Host=********-db.internal;Port=5432;Database=weather;Username=********;Password=********;"
  }
}
```

### サーバーサイドの実装

EFを使ってテーブルに格納するエンティティを作成していきます。
今回は、テンプレートのWeatherForecastを流用します。
主キーのプロパティだけを追記。

```csharp:WeatherForecast.cs
namespace XXXX.Shared;

public class WeatherForecast
{
    public string Id { get; set; } // 追加
    ...
}
```

Serverプロジェクト内にDbContextを追加します。
```csharp:WeatherDbContext.cs
namespace XXXXX.Server.Data
{
    public class WeatherDbContext : DbContext
    {
        public WeatherDbContext(DbContextOptions options) : base(options)
        {
        }

        public DbSet<WeatherForecast> WeatherForecasts => Set<WeatherForecast>();

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            builder.Entity<WeatherForecast>()
                .HasKey(x => new { x.Id });
        }
    }
}
```


エントリーポイントにDbContextを追加します。
あわせて、マイグレーションと初期データの登録も行います。

```csharp:Program.cs
...
builder.Services.AddDbContext<WeatherDbContext>(option =>
{
    option.UseNpgsql(builder.Configuration.GetConnectionString("WeatherDatabase"));
});

...

// マイグレーションと初期データの作成
using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<WeatherDbContext>();
    context.Database.Migrate();

    // データがなければ初期データ作成
    if (!context.WeatherForecasts.Any())
    {
        await context.WeatherForecasts.AddRangeAsync(
            new WeatherForecast() { Id = "1", Date = DateOnly.FromDateTime(DateTime.Now.AddDays(1)), TemperatureC = 20, Summary = "Mild" },
            new WeatherForecast() { Id = "2", Date = DateOnly.FromDateTime(DateTime.Now.AddDays(2)), TemperatureC = 10, Summary = "Cool" },
            new WeatherForecast() { Id = "3", Date = DateOnly.FromDateTime(DateTime.Now.AddDays(3)), TemperatureC = 30, Summary = "Hot" });
        await context.SaveChangesAsync();
    }
}
```

なお、コードでマイグレーションを書かずにfly.toml側でデプロイ時に任意のコマンドが実行可能なので、そちらを利用してdotnet-efでマイグレーションをするといった方法も可能かも。(未検証)
詳細は下記を参照ください。
https://fly.io/docs/reference/configuration/


```toml:fly.toml
# deploy時に実行するコマンド (下記はRailsのマイグレーション用のコマンド)
[deploy]
  release_command = "bundle exec rails db:migrate"
```


コントローラにDbContextを使用したデータ取得処理を実装します。

```csharp:WeatherForecastController.cs

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly WeatherDbContext _context;
    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(WeatherDbContext context, ILogger<WeatherForecastController> logger)
    {
        _context = context;
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        return _context.WeatherForecasts.ToArray();
    }
}
```



### マイグレーションファイルの作成

Serverプロジェクト内でマイグレーションファイルを作成します。

```bash
dotnet ef migrations add InitialCreate
```

### 公開&デプロイ

STEP1と同様に公開用のファイルを作成し、flyctl deployでデプロイします。
作成した初期値が反映された画面になっていれば成功です。
![スクリーンショット 2022-12-12 21_Fotor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/3b5bfe76-0fa3-fb60-0b52-f2d3680410e8.png)


# まとめ

ASP.NET CoreホストしたBlazor WebAssemblyをFly.ioにデプロイする方法および、EFとPostgreSQLを使用した簡単なアプリの紹介をしました。
Blazor自体の話は少なかったと思いますが、RDBMSを使用したサーバーサイド＋Blazorでアプリを試しに無料で作ってみたいという方は、Fly.ioは選択肢の１つになるのではないかと思います。