Visual StudioでDockerComposeを使って開発してみる

Visual StudioでDockerComposeを使って開発してみる

目次

はじめに
プロジェクトの作成
Entity Framework Coreを使って事前準備
Docker Composeの設定
動作確認
終わり

はじめに

最近、Visual Studioで .Net 5 を使った開発に携わることがあり、Dockerを使ってデバッグをしておりました。
その時にDBが必要で、出来ればDBも Docker を使いたいと思い調べていたところ、Visual Studioの機能でデバッグ用に Docker でDBも使えることが分かったのでそちらを記事にしようと思います。

環境は以下で行います。(インストール手順は省きます)

環境
Windows10
Visual Studio 2019 Enterprise
Docker 4.4.4
.Net 5

プロジェクトの作成

まずテスト用のプロジェクトを作成します。
今回はAsp.Net Core MVCで作ります。
プロジェクト名は「DockerComposeTest」にします。


次にDocker コンテナでデバッグをしたいので「Dockerを有効にする」にチェックを入れます。

プロジェクトが作成されるので、まずはデバッグをしてページが表示されるかを確認します。

以下のページが表示されればOKです。

Entity Framework Coreを使って事前準備

今回はDBを使うので、DBに接続したということを分かりやすくするためEntity Framework Coreを使います。
まずはDbContextを作成します。
ファイル名は「DockerComposeTestDbContext」にします。
中身は以下のように作成しております。

    public class DockerComposeTestDbContext : DbContext
    {
        public DockerComposeTestDbContext(DbContextOptions<DockerComposeTestDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Person>().Property(x => x.Name).IsRequired();
            base.OnModelCreating(modelBuilder);
        }

        public DbSet<DockerComposeTest.Models.Person> Person { get; set; }
    } 

次にModelが必要なので、「Person」モデルを作成します。

    public class Person
    {
        [Key]
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }

後はコントローラーとビューを作成します。
(スキャフォールディングを使って作成したので、詳細はスキャフォールディングなどで検索してください)
ちなみに以下のようになります。Viewは5つあるので、Index.cshtmlとCreate.cshtmlのみ記載します。

public class PeopleController : Controller
    {
        private readonly DockerComposeTestDbContext _context;

        public PeopleController(DockerComposeTestDbContext context)
        {
            _context = context;
        }

        // GET: People
        public async Task<IActionResult> Index()
        {
            return View(await _context.Person.ToListAsync());
        }

        // GET: People/Details/5
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var person = await _context.Person
                .FirstOrDefaultAsync(m => m.Id == id);
            if (person == null)
            {
                return NotFound();
            }

            return View(person);
        }

        // GET: People/Create
        public IActionResult Create()
        {
            return View();
        }

        // POST: People/Create
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("Id,Name,Age")] Person person)
        {
            if (ModelState.IsValid)
            {
                _context.Add(person);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }
            return View(person);
        }

        // GET: People/Edit/5
        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var person = await _context.Person.FindAsync(id);
            if (person == null)
            {
                return NotFound();
            }
            return View(person);
        }

        // POST: People/Edit/5
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("Id,Name,Age")] Person person)
        {
            if (id != person.Id)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    _context.Update(person);
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!PersonExists(person.Id))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            return View(person);
        }

        // GET: People/Delete/5
        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var person = await _context.Person
                .FirstOrDefaultAsync(m => m.Id == id);
            if (person == null)
            {
                return NotFound();
            }

            return View(person);
        }

        // POST: People/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var person = await _context.Person.FindAsync(id);
            _context.Person.Remove(person);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }

        private bool PersonExists(int id)
        {
            return _context.Person.Any(e => e.Id == id);
        }
    }
@model IEnumerable<DockerComposeTest.Models.Person>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Age)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Age)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

@model DockerComposeTest.Models.Person

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Person</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Age" class="control-label"></label>
                <input asp-for="Age" class="form-control" />
                <span asp-validation-for="Age" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}


またプロジェクトに使用するDbContextを登録します。
コードはStartup.csファイルに記載します。

// This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
                     ・
                     ・
            services.AddDbContext<DockerComposeTestDbContext>(optioins =>
            {
               optioins.UseNpgsql(@"Host=test_db;Username=postgres;Password=password;Database=postgres");
            });

                     ・
                     ・
        }

またテーブルを生成するのが面倒なので、今回は実行時に自動で生成するようにします。
これはProgram.csのMain関数に記載します。

var context = services.GetRequiredService<DockerComposeTestDbContext>();
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();
            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    var context = services.GetRequiredService<DockerComposeTestDbContext>();
                    context.Database.EnsureCreated();
                    // DbInitializer.Initialize(context);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred creating the DB.");
                }
            }
            host.Run();
        }

最後にMigrationファイルを作成します。
Visual Studioの中央下にあるところから「パッケージマネージャーコンソール」をクリックします

そうすると「PM>」の次にコマンドが入力できるので「Add-Migration Initialize」と入力してMigrationファイルを作成します。

これでDBに関する記述は以上になります。

Docker Composeの設定

次にDB用のDockerコンテナを設定します。
ソリューションエクスプローラーで「プロジェクトを右クリック」→「追加」→「コンテナ オーケストレーターのサポート…」をクリックします。

そうするとダイアログが表示されるので、ドロップダウンを「Docker Compose」にして「OK」をクリックします。

次のダイアログは「Linux」のままで「OK」をクリックします。

そうするとプロジェクトと同じ階層に「docker-compose」が表示されます。

これでDocker Composeが使えるようになりました。
次にDockerにDBのコンテナを作成するように設定します。
今作成された「docker-compose」の直下にある「docker-compose.yml」を開き、以下のように設定します。

version: '3.4'

services:

  db:
    image: postgres:13-alpine
    ports:
      - "5434:5432"
    environment:
      TZ: Asia/Tokyo
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
    restart: always
    hostname: test_db

  dockercomposetest:
    image: ${DOCKER_REGISTRY-}dockercomposetest
    build:
      context: .
      dockerfile: DockerComposeTest/Dockerfile
    depends_on: 
     - db

こうすることで自動でDBのdockerコンテナを作成してくれます。

動作確認

では実際に動かしてみます。
Visual Studioの中央上にある「▷」をクリックします。
この時左のドロップダウンが「docker-compose」になっているか確認してください。

そうするとビルドが始まり、問題なければデバッグ中になります。
ブラウザも立ち上がるので、ブラウザのURLに続けて「/People」と入力します。
(今回はコントローラ名が「PeopleController」なのでURLも「People」にしております)

問題なければIndexページが表示されます。
実際にDBにつながっているかを確認するため、「Create New」をクリックしてデータを新規登録してみます。

入力データはNameを「Test2」、Ageを「16」にしてみます。

CreateボタンをクリックするとIndexページに戻り、画面に今作成した「Test2」が表示されているかと思います。

これで動作確認は終わりですが、これだと実際にDBを使っているのかが分からないので、Dockerコンテナに入って確認してみます。
Dockerを起動して、対象のコンテナのCLIを起動します。
今回は「dockercompose・・・」という名前がついているものをクリックします。

そうするとコンテナの一覧が表示されるので、次も「dockercompose・・・」という名前のコンテナを選択して、「CLI」ボタンをクリックします。

コマンドプロンプトに似た画面が開くので「psql -U {dbユーザー名} -d {データベース名}」を入力してDBに接続します。
今回はdbユーザー名、データベース名ともに「postgres」にしているので以下のようになります。

これでDBに接続が出来たので、対象のテーブルにデータが入っているか確認します。

これで無事にDocker コンテナにあるDBにもデータが登録されていることが確認できたので今回はここまでとなります。

終わり

今回はVisual StudioのDocker Composeを使ってデバッグ用のDBをDockerコンテナに用意する方法でした。
これを使えば今後はDBを毎回立てずとも、プロジェクトにあったDBのコンテナを簡単に作れるので楽になるかもしれません。