ASP.NET Core WebAPI を ECS・Fargate にデプロイしてみた
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を作成していきます。
.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があるだけの単純なクラスです。
public class TodoItem { public long Id { get; set; } public string Name { get; set; } public bool IsComplete { get; set; } }
データベースコンテキストクラスの作成
先程作成したモデルクラスをDBと紐付けるためのデータベースコンテキストクラスを作成します。
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
としました。
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
が自動的に作成されていると思います。
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
と設定します。
# 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.