ASP.NET Core WebAPI を ECS・Fargate にデプロイしてみた

2020.07.15

CX事業本部の佐藤です。

ふと思い立って、ASP.NET Coreを触ってみました。.NET Core になってからはほとんど触れていなかったのですが、 dotnet CLIを使うことで、雛形のアプリケーションを簡単に作ることができ、非常に便利でした。そこで、簡単なTodo WebAPIを ASP.NET Core で作成して、ECS・Fargate にデプロイしてみたので、手順を残します。

ASP.NET Coreとは?

Microsoftがオープンソースで開発している、クロスプラットフォームなWebフレームワークの一種です。元々、ASP.NET 4.xのときはWindowsでしか開発ができませんでした。ASP.NET Coreは ASP.NET 4.xを再設計し、macOS, Linux環境でも開発できるようにしたフレームワークです。基本的にはC#を使ってWebアプリケーションを構築します。クライアント側のフレームワーク(React, Angular)などとも統合されており、雛形アプリケーションを作成することですぐに開発を始めることができます。

ASP.NET Coreで WebAPIを作成する

まずは、WebAPIを作成していきます。

今回は以下のASP.NET Core WebAPIのチュートリアルをベースに、TodoAPIを作成していきます。

ASP.NET Core WebAPIチュートリアル

.NET Core SDK 3.1をインストール

最新版である、.NET Core SDKをインストールします。以下のサイトにアクセスして、使っているOSのインストーラーをダウンロードしてきて、起動します。画面の指示に従ってインストールします。

https://dotnet.microsoft.com/download/dotnet-core/3.1

dotnet --version
3.1.301

.NET Core SDKで作成できるプロジェクト

現在のバージョン3.1では、以下のアプリケーションの雛形を作成できます。ASP.NETの他にもコンソールアプリケーションやWPF、Blazorなども作成できます。

dotnet new --help
Templates                                         Short Name               Language          Tags
----------------------------------------------------------------------------------------------------------------------------------
Console Application                               console                  [C#], F#, VB      Common/Console
Class library                                     classlib                 [C#], F#, VB      Common/Library
WPF Application                                   wpf                      [C#]              Common/WPF
WPF Class library                                 wpflib                   [C#]              Common/WPF
WPF Custom Control Library                        wpfcustomcontrollib      [C#]              Common/WPF
WPF User Control Library                          wpfusercontrollib        [C#]              Common/WPF
Windows Forms (WinForms) Application              winforms                 [C#]              Common/WinForms
Windows Forms (WinForms) Class library            winformslib              [C#]              Common/WinForms
Worker Service                                    worker                   [C#]              Common/Worker/Web
Unit Test Project                                 mstest                   [C#], F#, VB      Test/MSTest
NUnit 3 Test Project                              nunit                    [C#], F#, VB      Test/NUnit
NUnit 3 Test Item                                 nunit-test               [C#], F#, VB      Test/NUnit
xUnit Test Project                                xunit                    [C#], F#, VB      Test/xUnit
Razor Component                                   razorcomponent           [C#]              Web/ASP.NET
Razor Page                                        page                     [C#]              Web/ASP.NET
MVC ViewImports                                   viewimports              [C#]              Web/ASP.NET
MVC ViewStart                                     viewstart                [C#]              Web/ASP.NET
Blazor Server App                                 blazorserver             [C#]              Web/Blazor
Blazor WebAssembly App                            blazorwasm               [C#]              Web/Blazor/WebAssembly
ASP.NET Core Empty                                web                      [C#], F#          Web/Empty
ASP.NET Core Web App (Model-View-Controller)      mvc                      [C#], F#          Web/MVC
ASP.NET Core Web App                              webapp                   [C#]              Web/MVC/Razor Pages
ASP.NET Core with Angular                         angular                  [C#]              Web/MVC/SPA
ASP.NET Core with React.js                        react                    [C#]              Web/MVC/SPA
ASP.NET Core with React.js and Redux              reactredux               [C#]              Web/MVC/SPA
Razor Class Library                               razorclasslib            [C#]              Web/Razor/Library/Razor Class Library
ASP.NET Core Web API                              webapi                   [C#], F#          Web/WebAPI
ASP.NET Core gRPC Service                         grpc                     [C#]              Web/gRPC
dotnet gitignore file                             gitignore                                  Config
global.json file                                  globaljson                                 Config
NuGet Config                                      nugetconfig                                Config
Dotnet local tool manifest file                   tool-manifest                              Config
Web Config                                        webconfig                                  Config
Solution File                                     sln                                        Solution
Protocol Buffer File                              proto                                      Web/gRPC

ASP.NET Core WebAPIプロジェクトを作成する

以下のコマンドを実行します。

dotnet new webapi --o WebApiSample

以下のようなプロジェクトの雛形が作成されていればOKです。

tree -L 1                                                                                                                    [cm-sato]  火  7/14 22:29:18 2020
.
├── Controllers
├── Program.cs
├── Properties
├── Startup.cs
├── WeatherForecast.cs
├── WebApiSample.csproj
├── appsettings.Development.json
├── appsettings.json
└── obj

必要なパッケージのインストール

WebAPIを作成するためにはDBが必要不可欠なので、DBアクセスするためののパッケージをインストールします。ASP.NET Coreには EntityFrameworkCore と呼ばれる ORMが備わっているので、それのSqlServer用のパッケージをインストールします。 今回はサンプルなので、インメモリでDBを構成していくので、以下の2つのパッケージをインストールします。

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.InMemory

モデルクラスの作成

プロジェクト直下に、 Models というフォルダを作成して、その配下に TodoItem.cs というTodoを管理するためのモデルクラスを作成します。各プロパティのgetter setterがあるだけの単純なクラスです。

Models/TodoItem.cs

public class TodoItem
{
    public long Id { get; set; }
    public string Name { get; set; }
    public bool IsComplete { get; set; }
}

データベースコンテキストクラスの作成

先程作成したモデルクラスをDBと紐付けるためのデータベースコンテキストクラスを作成します。

Models/TodoContext.cs

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            : base(options)
        {
        }

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

データベースコンテキストクラスをDIコンテナに登録する

ASP.NET Coreでは標準でDI(依存性の注入)の機能が用意されています。データベースコンテキストのようなクラスはDIに登録することで、Controllerクラスのコンストラクタの引数に渡され使えるようになります。DIは プロジェクト直下の Startup.cs クラスで登録します。今回はインメモリでDBを使用するので、 services.AddDbContext<TodoContext>(opt => opt.UseInMemoryDatabase("TodoList")); として登録します。また、今回はHTTPSは使用しないので、 app.UseHttpsRedirection() はコメントアウトします。ALBのヘルスチェックを通すため、ヘルスチェックのサービスもDIに登録します。ヘルスチェックのパスは /health としました。

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<TodoContext>(opt =>
               opt.UseInMemoryDatabase("TodoList"));
            services.AddControllers();
						services.AddHealthChecks();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
            app.UseHealthChecks("/health");
        }
    }
}

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

Controller/ 配下にコントローラクラスを作成することでWebAPIを開発していくことができます。普通に実装していくこともできるのですが、ASP.NET Coreでは スキャフォールディング機能でCRUDの雛形を作成することができるので、それを使ってサクッと作ってしまいます。以下のコマンドを上から実行します。

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers

すると、 Controllers/ フォルダ配下に TodoItemsController.cs が自動的に作成されていると思います。

Controllers/TodoItemsController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace dotnet_web_api_sample
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    {
        private readonly TodoContext _context;

        public TodoItemsController(TodoContext context)
        {
            _context = context;
        }

        // GET: api/TodoItems
        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
        {
            return await _context.TodoItems.ToListAsync();
        }

        // GET: api/TodoItems/5
        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return todoItem;
        }

        // PUT: api/TodoItems/5
        // To protect from overposting attacks, enable the specific properties you want to bind to, for
        // more details, see https://go.microsoft.com/fwlink/?linkid=2123754.
        [HttpPut("{id}")]
        public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
        {
            if (id != todoItem.Id)
            {
                return BadRequest();
            }

            _context.Entry(todoItem).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!TodoItemExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/TodoItems
        // To protect from overposting attacks, enable the specific properties you want to bind to, for
        // more details, see https://go.microsoft.com/fwlink/?linkid=2123754.
        [HttpPost]
        public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
        {
            _context.TodoItems.Add(todoItem);
            await _context.SaveChangesAsync();

            // return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
            return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
        }

        // DELETE: api/TodoItems/5
        [HttpDelete("{id}")]
        public async Task<ActionResult<TodoItem>> DeleteTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
            {
                return NotFound();
            }

            _context.TodoItems.Remove(todoItem);
            await _context.SaveChangesAsync();

            return todoItem;
        }

        private bool TodoItemExists(long id)
        {
            return _context.TodoItems.Any(e => e.Id == id);
        }
    }
}

ここまでで、Todo WebAPIが作成できました。次からは、ECS・Fargateにデプロイしていきます。

ECS・Fargate に ASP.NET WebAPI をデプロイする

デプロイにはせっかくなので、先日発表された、 Copilot を使ってみたいと思います。 Copilot については、以下のブログで詳しく説明されていますので、参考にしてみてください。

Dockerfileを作成

ECSにデプロイするために、Dockerfileを作成します。やってることは単純で、dotnet core公式からsdkとasp.netのランタイムのイメージを持ってきて、 dotnet publish で リリースビルドを作成して、 ENTORYPOINT でWebAPIを実行しています。

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY --from=build-env /app/out .
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "dotnet-web-api-sample.dll"]

Copilotでデプロイする

次に、ECSにデプロイするためのCopilotの初期設定を行っていきます。以下のコマンドを入力すると対話形式でデプロイするアプリケーションやサービスタイプなどが聞かれます。

copilot init

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

Application Name : dotnet-webapi-sample

Service type : Load Balanced Web Service

Service Name : todo-webapi

Dockerfile : ./Dockerfile

Port : 80

Deploy : No

このままデプロイするかと聞かれますが、一旦Noにします。

Note: It's best to run this command in the root of your Git repository.
Welcome to the Copilot CLI! We're going to walk you through some questions
to help you get set up with an application on ECS. An application is a collection of
containerized services that operate together.

Your workspace is registered to application dotnet-webapi-sample.
Service type: Load Balanced Web Service
Service name: todo-webapi
Dockerfile: ./Dockerfile
Port: 80
Ok great, we'll set up a Load Balanced Web Service named todo-webapi in application dotnet-webapi-sample listening on port 80.

✔ Created the infrastructure to manage services under application dotnet-webapi-sample.

✔ Manifest file for service todo-webapi already exists at copilot/todo-webapi/manifest.yml, skipping writing it.
Your manifest contains configurations like your container size and port (:80).

✔ Created ECR repositories for service todo-webapi.

All right, you're all set for local development.
Deploy: No

No problem, you can deploy your service later:
- Run `copilot env init --name test --profile default --app dotnet-webapi-sample` to create your staging environment.
- Update your manifest copilot/todo-webapi/manifest.yml to change the defaults.
- Run `copilot svc deploy --name todo-webapi --env test` to deploy your service to a test environment.

ALBのヘルスチェックを通すためにmanifest.ymlを更新する

プロジェクト直下に copilot/todo-webapi/manifest.yml が作成されていると思うので、開きます。

healthcheck のコメントアウトを外して、 /health と設定します。

copilot/todo-webapi/manifest.yml

# The manifest for the "todo-webapi" service.
# Read the full specification for the "Load Balanced Web Service" type at:
#  https://github.com/aws/amazon-ecs-cli-v2/wiki/Manifests#load-balanced-web-svc

# Your service name will be used in naming your resources like log groups, ECS services, etc.
name: todo-webapi
# The "architecture" of the service you're running.
type: Load Balanced Web Service

image:
  # Path to your service's Dockerfile.
  build: ./Dockerfile
  # Port exposed through your container to route traffic to it.
  port: 80

http:
  # Requests to this path will be forwarded to your service. 
  # To match all requests you can use the "/" path. 
  path: '/'
  # You can specify a custom health check path. The default is "/"
  healthcheck: '/health'

# Number of CPU units for the task.
cpu: 256
# Amount of memory in MiB used by the task.
memory: 512
# Number of tasks that should be running in your service.
count: 1

# Optional fields for more advanced use-cases.
#
#variables:                    # Pass environment variables as key value pairs.
#  LOG_LEVEL: info
#
#secrets:                      # Pass secrets from AWS Systems Manager (SSM) Parameter Store.
#  GITHUB_TOKEN: GITHUB_TOKEN  # The key is the name of the environment variable, the value is the name of the SSM parameter.

# You can override any of the values defined above by environment.
#environments:
#  test:
#    count: 2               # Number of tasks to run for the "test" environment.

デプロイ

マニフェストファイルを更新したので、デプロイします。

copilot env init --name test --profile default --app dotnet-webapi-sample
copilot svc deploy --name todo-webapi --env test

すると、以下の用にデプロイが自動的に行われます。ECRリポジトリの作成、Dockerのビルド、イメージのPush、ECSクラスター・サービス・タスク定義、ALBの作成までを全自動で行ってくれています。すごく楽ですね。デプロイが完了したら、WebAPIにアクセスするためのELBの URLが表示されていると思うので、これを使ってWebAPIが動作するかを確認してみてください。うまく動いていればデプロイOKです。

Sending build context to Docker daemon  152.7MB
Step 1/12 : FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
 ---> 006ded9ddf29
Step 2/12 : WORKDIR /app
 ---> Using cache
 ---> 0f433a7bf7d7
Step 3/12 : COPY *.csproj ./
 ---> Using cache
 ---> 3e0a20235134
Step 4/12 : RUN dotnet restore
 ---> Using cache
 ---> ae857ea86578
Step 5/12 : COPY . ./
 ---> Using cache
 ---> b68a812366a1
Step 6/12 : RUN dotnet publish -c Release -o out
 ---> Using cache
 ---> 1dfc621dc5d2
Step 7/12 : FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
 ---> 014a41b1f39a
Step 8/12 : WORKDIR /app
 ---> Using cache
 ---> 66b824e06bd4
Step 9/12 : COPY --from=build-env /app/out .
 ---> Using cache
 ---> ed884684ed39
Step 10/12 : EXPOSE 80
 ---> Using cache
 ---> 602d8fa351e6
Step 11/12 : EXPOSE 443
 ---> Using cache
 ---> 94728c308636
Step 12/12 : ENTRYPOINT ["dotnet", "dotnet-web-api-sample.dll"]
 ---> Using cache
 ---> a893e31d8c55
Successfully built a893e31d8c55
Successfully tagged 00000000000.dkr.ecr.ap-northeast-1.amazonaws.com/dotnet-webapi-sample/todo-webapi:6ad7e95
Login Succeeded
The push refers to repository [00000000000.dkr.ecr.ap-northeast-1.amazonaws.com/dotnet-webapi-sample/todo-webapi]
f9b0341c8034: Pushed
779b18477ee6: Pushed
fd4268c201b9: Pushed
fca6f9b27f6b: Pushed
9c0a186e12e6: Pushed
c9d71dd19e66: Pushed
13cb14c2acd3: Pushed
6ad7e95: digest: sha256:c8e15914c5401da8b91623ed3171d94204748ab1c3829abf211ff3346657d427 size: 1794


✔ Deployed todo-webapi, you can access it at http://hoge.ap-northeast-1.elb.amazonaws.com.

参考