LoginSignup
4
6

More than 1 year has passed since last update.

BlazorとSQL Serverをdocker-composeで立ち上げる

Last updated at Posted at 2022-04-10

はじめに

DockerもBlazorも、ほぼ触ったことがないけど、BlazorをDockerで動してみたい

データベースも含めてdocker-composeする

具体的には公式チュートリアルのTodoアプリを発展させて、アイテムを永続化(DBから読み書き)したい

環境

  • Windows 10 Home
  • Visual Studio 2022 Community
    • .NET 6
  • SQL Server 2019
  • Docker version 20.10.13, build a224086
  • Docker Compose version v2.3.3

Blazorプロジェクトをつくる

dotnet new引数(こちらも)を参考に、プロジェクトをつくる

dotnet new blazorwasm --hosted -o ./project -n BlazorHosted
オプション 意味
-o プロジェクトを配置する場所
-n 作成されるプロジェクトの名前
--hosted ASP.NET Core ホストが含まれる

ベースとなるコード

BlazorでTodoアプリを作る公式チュートリアルを参考にして、Razorページとモデルをプロジェクトに追加する

この時点で実行すると、ナビゲーションにTodoが追加されて、任意のアイテムを追加できるようになっている

ただし、データの永続化については未着手なので、リロードするたびにデータは初期状態(空のデータ)になる

image.png

※Visual Studioからデバッグ実行してitem1Add todoした時の様子

Tips

dotnet watch runしておくとデバッグが楽

2022/04/16追記
dotnet watchのみでいいかも?

データベースをつかう準備

アプリ側でデータベースをつかう準備をする
去年のアドベントカレンダーにて、参考になる記事がある

NuGetのパッケージをインストールする

SQL Serverを扱うために、必要なライブラリを揃える

Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools

データベースコンテキストの中身や、シードデータの与え方は、手前味噌過去記事参照

体系立ててまとめられておらず、我ながら情報が探しにくい

データベースに接続する設定をする

.NET 6でつくったので、いろいろ過去の記事と齟齬が生じている
(たとえばStartup.csファイルが無いなど)

サーバー側のProgram.cs

.NET 6でプロジェクトをつくると、丁寧に// Add services to the container.とガイドされているので、そこにサービスを追加する

Program.cs
builder.Services.AddDbContext<BlazorHosted.Server.Context.TodoDbContext>(
    options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

appsettings.jsonに接続文字列を追加する

デバッグ中はlocaldbをつかう
dockerで動かすときは、docker-compose.ymlで接続文字列を上書きする(後述)

appsettings.json
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BlazorAppDb;Trusted_Connection=True;",
  },

過去記事ではまったポイントとして、パスワードが8文字以下であるとうまくいかないので注意する

モデルを変更する

チュートリアルでつくったTodoItem.csを変更する
データベースに格納するので、Idを追加する

Sharedプロジェクト
namespace BlazorHosted.Shared;

public class TodoItem
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public bool IsDone { get; set; }
}

慣例により、Id または Id という名前のプロパティがエンティティの主キーとして構成されます。(引用元

DBコンテキストを新規作成する

サーバー側にDBコンテキストを生成する
これにより、EF CoreのツールからDBを新規作成したり、テーブルの定義を更新できたりする

シードデータとして2つ先にデータを用意しておく

Server/Context
using Microsoft.EntityFrameworkCore;
using BlazorHosted.Shared;

namespace BlazorHosted.Server.Context
{
    public class TodoDbContext : DbContext
    {
        public TodoDbContext(DbContextOptions<TodoDbContext> options) : base(options) { }

        public virtual DbSet<TodoItem> TodoItems { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TodoItem>().HasData(
                new TodoItem { Id = 1, Title = "item1st", IsDone = true },
                new TodoItem { Id = 2, Title = "item2nd", IsDone = false }
                );
        }
    }
}

スキャフォールディングでコントローラーを追加する

サーバー側のコントローラーを右クリックして、"コントローラー"を追加する
クライアント(ウェブブラウザ)からサーバーにGETやPOSTなどリクエストがあった場合の挙動を記すのがコントローラー
(じっさいのところ、ユーザーはボタンを押すだけでGETなどは意識しないが・・)

image.png

APIのEFをつかったアクション(GET,POSTなど)があるコントローラーをえらぶ

image.png

今さらながら、モデルはSharedに、コンテキストはサーバーに、それぞれ配置した

image.png

はまったポイント:GETしたらAPIの生の結果がかえってきた

テストのためにlocalhost/todoアクセスすしたところ、ページではなくAPIの結果が生でかえってきた

image.png

解決した

先述の記事のコードではそんなことなかったので、比較してみたところ
戻値に違いがあった

TodoItemsController.cs
// 自分のコード
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
    return await _context.TodoItems.ToListAsync();
}

// こちらが正しい
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
    var items = await _context.TodoItems.ToListAsync();
    return Ok(items);
}

修正したところ、ちゃんとLoading...が表示されるようになった

image.png

Razor Page を変更する

こちらもチュートリアルから少し変更をくわえる

  • アイテムがまだnullの状態であれば、その旨を表示しておく
  • OnInitializedAsyncを追加して、DBにすでに存在するデータを表示する
  • アイテムが追加されたらDBに書き込む
@page "/todo"
@using BlazorHosted.Shared
@using System.Linq;
@inject HttpClient Http

<PageTitle>Todo</PageTitle>

@if (todos == null)
{
    <p>No Todo Items found.</p>
}
else
{
    <h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

    <ul>
        @foreach (var todo in todos)
        {
            <li>
                <input type="checkbox" @bind="todo.IsDone" />
                <input @bind="todo.Title" />
            </li>
        }
    </ul>

    <input placeholder="Something todo" @bind="newTodo" />
    <button @onclick="AddTodo">Add todo</button>
}

@code {
    private List<TodoItem>? todos;
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            TodoItem newTodoItem = new() { Title = newTodo };
            todos?.Add(newTodoItem);
            newTodo = string.Empty;

            Http.PostAsJsonAsync<TodoItem>(requestUri: "api/TodoItems", value: newTodoItem);
        }
    }

    protected override async Task OnInitializedAsync()
    {
        todos = await Http.GetFromJsonAsync<List<TodoItem>>(requestUri: "api/TodoItems");
    }
}

データベースをマイグレーションする

データベースをつくる

Add-Migration Init
Update-Database

動作している様子

  1. 2つのアイテムがはじめから追加されている
  2. 更新してもアイテムが残っている

Animation.gif

以上がdockerをつかわないデバッグまで

移行の適用によると、バンドルを使う方法が比較的あたらしいらしい
本番環境では安易にコマンド一発でDBを更新するのではなく、SQLスクリプトを出力して中身を精査してから実行すべし。

ということで、dockerfileではそのようにして移行する

dockerfileとcompose.ymlを準備する

ここからdockerで動かすためのファイルを準備する

dockerfile

ASP.NET Core 向けの Docker イメージを参考にした
こちらこちらも参考にした

命令 意味
FROM 以降の命令で使う ベース・イメージ。ASで別名をつけられる
WORKDIR RUNCMD, ENTRYPOINTが実行される場所
COPY コピー元はdockerfileが置かれいてるカレントパス.、コピー先はWORKDIRからの相対パス。コピー先を.とした場合は、すなわちWORKDIRを指す。

主な命令の一覧はこちらに掲載されている

。。。と、自分で書くのも勉強になるが、ミスを連発しそうなのでVisual Studioの機能を使う

image.png

マルチステージビルドなる方法により、最終的なイメージサイズが削減している

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["Server/BlazorHosted.Server.csproj", "Server/"]
COPY ["Shared/BlazorHosted.Shared.csproj", "Shared/"]
COPY ["Client/BlazorHosted.Client.csproj", "Client/"]
RUN dotnet restore "Server/BlazorHosted.Server.csproj"
COPY . .
WORKDIR "/src/Server"
RUN dotnet build "BlazorHosted.Server.csproj" -c Release -o /app/build

FROM build AS publish
ENV PATH="$PATH:/root/.dotnet/tools"
RUN dotnet publish "BlazorHosted.Server.csproj" -c Release -o /app/publish
# ここから
RUN dotnet tool install --global dotnet-ef --version 6.0.3
RUN dotnet ef migrations bundle -o /app/publish/bundle
RUN chmod +x /app/publish/bundle
# ここまでを追加

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "BlazorHosted.Server.dll"]

publishのところでapp/publish直下にbundleファイルを生成している
docker compose upしたあとにこのバンドルをつかってDBをマイグレーションする

docker-compose.yml

ConnectionStringより上まではVisual Studioが生成してくれている

  • ConnectionString__で接続文字列を上書きしている
  • portsは8080を指定しているが、VS Codeからupしても8080以外のポートが接続先になることがある(よくわからない)
  • DBは十分に長いパスワードを設定しないと正常にたちあがらない
    • ポート11433へSSMSからもアクセスできる
version: '3.4'

services:
  blazorhosted.server:
    image: ${DOCKER_REGISTRY-}blazorhostedserver
    build:
      context: .
      dockerfile: Server/Dockerfile
    environment:
      - ConnectionStrings__DefaultConnection=Server=db;Database=master;User=sa;Password=SqlPass1234;
    ports:
      - '127.0.0.1:8080:80'
      - '127.0.0.1:10443:443'
    depends_on:
      - db

  db:
    image: "mcr.microsoft.com/mssql/server:2019-latest"
    environment:
      - SA_PASSWORD=SqlPass1234
      - ACCEPT_EULA=Y
    ports:
      - 11433:1433

versionとは?

versionから何を指定すればよいかわからない

  • 公式のGet Startedでは3.9をつかっている
  • 3系2系があるらしい
  • にdockerエンジンとの対応が書かれている
    • 自分のdockerはocker version 20.10.13なので、とりあえず最新のバージョンでもよさそう

ということで、docker runのオプションを参考にymlファイルを書いてみる
前記事から引き続き、こちらの記事も参考にさせていただいた

ビルドに必要なイメージをpullする

SQL Server 2019のイメージを取得しておく --->>> 過去記事

Microsoftのdocker-hubから必要なイメージをpullする

docker pull mcr.microsoft.com/dotnet/sdk:6.0
docker pull mcr.microsoft.com/dotnet/aspnet:6.0

docker imagesで確認すると、イメージが追加されている

> docker images
REPOSITORY                        TAG           IMAGE ID       CREATED        SIZE
mcr.microsoft.com/dotnet/sdk      6.0           2d1a2c7481f5   11 days ago    721MB
mcr.microsoft.com/dotnet/aspnet   6.0           683c56113596   11 days ago    208MB
mcr.microsoft.com/mssql/server    2019-latest   56beb1db7406   8 months ago   1.54GB

.ymlの書き方について:こちらの記事に各リファレンスへのリンクもまとまっている

イメージを作成して起動する

自作Blazorプロジェクトのイメージをつくる
ビルドにはそれなりの時間がかかるので、Visual Studioであらかじめよくデバッグしておく

自分のPCでは[+] Building 1070.4s (23/23) FINISHEDとのことで、ビルドに1070秒、15分以上かかった・・・(これはどうなんだ?)

# .ymlファイルがある階層で build->up する
cd ./project
docker compose build
docker compose up -d

生成物を確認する

# イメージ
> docker images
REPOSITORY                        TAG           IMAGE ID       CREATED         SIZE
blazorhostedserver                latest        7a9a0dc4eabd   3 minutes ago   464MB
...後略...

# コンテナー
> docker ps
CONTAINER ID   IMAGE                                        COMMAND                  CREATED          STATUS          PORTS                                            NAMES
403182a89b2c   blazorhostedserver                           "dotnet BlazorHosted…"   16 seconds ago   Up 7 seconds    127.0.0.1:8080->80/tcp, 0.0.0.0:63927->443/tcp   project-blazorhosted.server-1    
376c37326b66   mcr.microsoft.com/mssql/server:2019-latest   "/opt/mssql/bin/perm…"   17 seconds ago   Up 12 seconds   0.0.0.0:11433->1433/tcp                          project-db-1

現時点ではDBにテーブルができていないので、サイトにアクセスしてもTodoページはエラーが表示される

DBマイグレーションする

dockerfileでbundleを書き出してある。エントリーポイントを書きかえて実行する

一時的にしか使わないので--rmをつけておく

docker compose run --rm --entrypoint /app/bundle blazorhosted.server

バンドルについて

バンドルは、.NET SDK または EF ツールを (または、自己完結型の場合は .NET ランタイムさえも) インストールせずに実行でき、プロジェクトのソース コードは必要ありません。

余計な(?)ものを環境に入れなくても、バンドルファイルさえあればよい

サイトにアクセスする

ブラウザからlocalhost:8080にアクセスすると、Blazorページが表示される
※タイミングによって?は8080ではないポートがバインドされている。ここはあとでよく調べないと・・・

VS Codeの左ペインのdockerアイコンを押すと、コンテナーの一覧が表示されるので、そこからブラウザを開く

image.png

やはり8080ポートではなかったものの、TodoページにはシードしたDBのデータが表示された

image.png

後片付け

コンテナーを破棄
docker compose down

イメージを破棄
docker rmi blazorhostedserver

おわりに

以下、まだ腑に落ちないところがあるが、とりあえずやりたいことができた

  • docker-compose.ymlでportを8080:80などと指定しても、ちがうポートが開く
  • docker compose buildに15分かかる

追記(22' 4/11)

http://localhost:8080でページが表示されないのは、安全が確保されていないからっぽい
コンテナーが立ち上がっていないわけではないっぽい

image.png

参考にさせていただいたサイトなど

また、上記オフィシャルな記事を補完してくださる記事

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6