.NET/ASP.NET

[ASP.NET Core] Shopping mall project - 보안과 배포

클리엘 2022. 9. 6. 17:14
728x90

ASP.NET Core는 ASP.NET Core platform과 각각의 개별 Application에 정교하게 통한된 Identity system을 통해서 인증과 권한 부여에 관한 기능을 제공하고 있습니다. 이러한 인증 기능을 통해 Admin이라는 관리 사용자에 대한 보안설정을 project에 적용함으로써 인증된 관리 사용자 만이 관리자 기능으로 접근할 수 있도록 처리할 것입니다. 물론 ASP.NET Core Identity는 그것 자체로 사용자에 대한 다양한 인증 및 권한에 대한 기능을 제공하고 있는데 이에 대한 추가적인 내용은 추후에도 알아볼 것입니다.

 

1. Identity Database 생성

 

ASP.NET Identity system은 유연한 구성 및 확장이 가능하며 사용자 data가 저장되는 방식에 대한 많은 option들을 제공하고 있습니다. 다만 여기서는 Entity Framework Core를 통해 Access 되는 MSSQL Server를 사용하여, 필요한 data를 저장하는 일반적인 방법을 사용해볼 것입니다.

 

(1) Identity Package for Entity Framework Core 설치

 

ASP.NET Core Identity support for Entity Framework Core를 포함하여 필요한 package를 설치하기 위해 Nuget Package System에서 Microsoft.AspNetCore.Identity.EntityFrameworkCore를 검색하고 설치합니다.

(2) Context Class 생성

 

database와 database로의 접근을 제공하는 Identity model객체 사이의 다리 역할을 수행하는 database context file을 생성하기 위해 AppIdentityDbContext.cs라는 이름의 file을 Models folder에 추가합니다.

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace MyWebApp.Models
{
    public class AppIdentityDbContext : IdentityDbContext<IdentityUser>
    {
        public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options) : base(options)
        {
        }
    }
}

IdentityDbContext로부터 상속된 AppIdentityDbContext class는 Entity Framework Core를 위한 Identity별 기능을 제공하기 위한 class이며 여기에서 사용된 IdentityUser class는 사용자를 표현하기 위해 사용되는 내장 class입니다.

 

(3) Connection String 설정

 

이제 database연결을 위한 연결 문자열(Connection String)을 설정합니다. 해당 설정은 Project의 appsettings.json file을 통해 이루어지며 Product Database를 설정한 경우와 동일하게 다음과 같이 설정을 추가해 줍니다.

"ConnectionStrings": {
  "MyDBConnection": "Server=(localdb)\\MSSQLLocalDB;Database=MyWebDB;Trusted_Connection=True;MultipleActiveResultSets=true",
  "IdentityConnection": "Server=(localdb)\\MSSQLLocalDB;Database=Identity;MultipleActiveResultSets=true"
}

추가된 설정은 IdentityConnection으로 정의되었으며 Identity라고 하는 LocalDB Database가 지정되었습니다.

 

(4) Application 구성

 

다른 ASP.NET Core 기능과 마찬가지로 Identity또한 Program.cs를 통해 구성될 수 있습니다. Program.cs를 다음과 같이 수정하여 위에서 만든 context class와 connection string을 통한 Identity를 설정합니다.

using Microsoft.EntityFrameworkCore;
using MyWebApp.Models;
using Microsoft.AspNetCore.Identity; //Identity

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<MyDbContext>(opts =>
{
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:MyDBConnection"]);
});

builder.Services.AddScoped<IMyDBRepository, MyDBRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddRazorPages();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession();
builder.Services.AddScoped<Cart>(sp => SessionCart.GetCart(sp));
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddServerSideBlazor();

builder.Services.AddDbContext<AppIdentityDbContext>(options => options.UseSqlServer(builder.Configuration["ConnectionStrings:IdentityConnection"])); //Identity
builder.Services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<AppIdentityDbContext>(); //Identity

var app = builder.Build();

app.UseStaticFiles();
app.UseSession();

app.UseAuthentication(); //Identity
app.UseAuthorization(); //Identity

app.MapControllerRoute("categoryPage", "{category}/Page{currentPage:int}", new { Controller = "Home", action = "Index" });
app.MapControllerRoute("page", "Page{currentPage:int}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapControllerRoute("category", "{category}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapControllerRoute("default", "Products/Page{currentPage}", new { Controller = "Home", action = "Index", currentPage = 1 });

app.MapDefaultControllerRoute();
app.MapRazorPages();
app.MapBlazorHub();
app.MapFallbackToPage("/admin/{*catchall}", "/Admin/Index");

MyData.InitData(app);

app.Run();

예제에서는 context class와 Identity services를 설정하는 데 사용된 AddIdentity() Method를 등록하기 위해 Entity Framework Core구성을 확장하였습니다. 특히 Identity services는 기본적으로 내장된 class를 사용하여 사용자와 사용자 역할을 표현하고 있습니다. 또한 UseAuthentication()와 UseAuthorization() Method를 호출하여 security정책을 구현하는 middleware component의 설정도 같이 추가하였습니다.

 

(5) Database Migration 생성및 적용

 

여기까지 기본적인 설정은 구현되었으며 이제 Entity Framework Core migration기능을 통해 schema를 정의하고 이를 database에 적용해야 합니다. 이를 위해 Project folder에서 아래 명령을 내려 Identity database를 위한 새로운 migration을 생성합니다.

dotnet ef migrations add Initial --context AppIdentityDbContext

이전에 사용한 명령과 다른 중요한 점은 database와 관련된 AppIdentityDbContext class의 이름을 특정하기 위해 -context 매개변수를 사용했다는 것입니다. 이와 같이 Application에 여러 database가 존재하는 경우에는 작업이 진행되어야 하는 정확한 context class가 지정되어야 합니다.

 

일단 위의 명령을 통해 초기화 migration file이 생성되고 나면 다시 아래 명령을 통해 database를 생성하고 migration이 적용될 수 있도록 해줍니다.

dotnet ef database update --context AppIdentityDbContext

명령을 내리고 나면 Identity라는 Database가 생성될 것입니다.

(6) 초기 data 설정

 

Project가 실행될때 명확한 관리 사용자를 설정하기 위해 Models folder에 IdentityInitialData.cs file을 아래와 같이 추가합니다.

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace MyWebApp.Models
{
    public static class IdentityInitialData
    {
        private const string adminUser = "Admin";
        private const string adminPassword = "!Admin123";

        public static async void InitData(IApplicationBuilder app)
        {
            AppIdentityDbContext context = app.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService<AppIdentityDbContext>();

            if (context.Database.GetPendingMigrations().Any())
                context.Database.Migrate();

            UserManager<IdentityUser> userManager = app.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
            IdentityUser user = await userManager.FindByNameAsync(adminUser);

            if (user == null)
            {
                user = new IdentityUser("Admin");
                user.Email = "admin@example.com";

                await userManager.CreateAsync(user, adminPassword);
            }
        }
    }
}

예제는 database를 생성하는 code인데 관련 내용 중 UserManager<T> class를 사용하고 있음을 확인할 수 있습니다. 이 class는 사용자 관리를 위한 ASP.NET Core Identity별 service를 제공하기 위한 것인데 관리자 계정(adminUser)을 검색하여 존재하지 않을 경우 Admin/1234를 통한 기본적인 관리자 계정을 생성하게 됩니다. 관리자의 비밀번호는 바꿀 수 있지만 숫자와 특수문자를 포함해야 한다는 정책이 적용되므로 해당 요건에 맞게 변경되어야 합니다.

 

이제 Project가 시작될 때 위의 예제가 동작하여야 하므로 아래와 같이 Program.cs를 수정하여 IdentityInitialData class의 InitData() Method가 호출될 수 있도록 합니다.

app.MapFallbackToPage("/admin/{*catchall}", "/Admin/Index");

MyData.InitData(app);
IdentityInitialData.InitData(app);

app.Run();

2. 관리자 Identity 적용

 

이전에는 Product의 관리나 주문관리를 위해 Blazor를 사용했었는데 관리자를 확인하기 위한 그 기능 자체는 Razor Page를 통해 구현하고자 합니다. 따라서 우선 Project의 Pages->Admin에 AdminUsers.cshtml file을 추가하여 현재 Identity Database에 존재하는 관리자를 표시할 수 있도록 합니다.

@page
@model AdminUsersModel

@using Microsoft.AspNetCore.Identity

<h3>관리자</h3>

<table>
    <tbody>
        <tr>
            <th>User : </th>
            <td>@Model.AdminUser.UserName</td>
        </tr>
        <tr>
            <th>Email : </th>
            <td>@Model.AdminUser.Email</td>
        </tr>
    </tbody>
</table>
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MyWebApp.Pages.Admin
{
    public class AdminUsersModel : PageModel
    {
        private UserManager<IdentityUser> _userManager;

        public AdminUsersModel(UserManager<IdentityUser> mgr)
        {
            _userManager = mgr;
        }

        public IdentityUser AdminUser { get; set; } = new();

        public async Task OnGetAsync()
        {
            AdminUser = await _userManager.FindByNameAsync("Admin");
        }
    }
}

Project를 실행하여 /admin/adminusers로 이동해 아래와 같이 Page가 정상적으로 표시되는지를 확인합니다.

3. 기본 인증정책 적용

 

현재 Project에서 /Admin영역은 별다른 인증과정 없이 주소만 입력하면 곧장 제품과 주문을 관리할 수 있는 Page로 접근이 가능합니다. 실제 환경이라면 이러한 동작은 위험할 수 있으므로 사용자를 정확히 구별할 수 있는 인증절차를 /Admin영역에 적용하고자 합니다. 물론 인증정책은 경우에 따라 여러 가지 형태로 이루어질 수 있으나 현재 Project에서 관리자는 하나에 불과하고 당장 다른 사용자를 위한 경우는 고려하지 않을 것으므로 현재 사용자가 단순히 인증된 사용자인지, 아니면 익명인지에 대해서만 구분하는 가장 단순한 형태의 인증정책을 적용해볼 것입니다.

 

우선 위에서 추가한 AdminUsers.cshtml의 Model의 class를 수정해 [Authorize] attribute를 적용합니다. 적용한 [Authroize]는 익명자로의 접근을 차단할 것입니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MyWebApp.Pages.Admin
{
    [Authorize]
    public class AdminUsersModel : PageModel
    {
        private UserManager<IdentityUser> _userManager;

        public AdminUsersModel(UserManager<IdentityUser> mgr)
        {
            _userManager = mgr;
        }

        public IdentityUser AdminUser { get; set; } = new();

        public async Task OnGetAsync()
        {
            AdminUser = await _userManager.FindByNameAsync("Admin");
        }
    }
}

이어서 /Admin으로 접근 시 표시되는 Pages -> Admin folder의 Index.cshtml관리자 기본 페이지 또한 Index.cshtml.cs에서 아래와 같이 [Authorize]를 적용하여 역시 익명 사용자는 접근할 수 없도록 합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MyWebApp.Pages.Admin
{
    [Authorize]
    public class IndexModel : PageModel
    {
        public void OnGet()
        {
        }
    }
}

만약 cshtml과 cs가 따로 분리된 형태가 아닌 page model class를 통해 하나의 cshtml file로 구현된 경우라면 다음과 같이 [Authorize] attribute를 cshtml에도 적용해 줄 수 있습니다.

@page "/admin"

@{
    Layout = null;
}

@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<!DOCTYPE html>
<html>
    <head>
        <title>관리자 영역</title>
        <base href="/" />
    </head>
    <body>
        <component type="typeof(BlazorRoute)" render-mode="Server" />
        <script src="/_framework/blazor.server.js"></script>
    </body>
</html>

4. Account관련 Controller와 View의 생성

 

만약 익명의 사용자가 인증이 필요한 영역으로의 접근을 시도하거나 특정 요청을 보내게 되면 해당 사용자는 익명을 요구하는 /Account/Login 쪽으로 redirect가 이루어지게 됩니다. 따라서 사용자로부터 인증정보를 받을 수 있는 전체적인 과정이 필요한데 이 절차를 완성하기 위한 가장 첫 번째로 인증정보를 담을 수 있는 Model class를 추가합니다.

 

Project의 Models->ViewModels folder에 UserLogin.cs이름의 file을 아래 내용으로 추가합니다.

using System.ComponentModel.DataAnnotations;

namespace MyWebApp.Models.ViewModels
{
    public class UserLogin
    {
        [Required]
        public string? Name { get; set; }
        [Required]
        public string? Password { get; set; }
        public string ReturnUrl { get; set; } = "/";
    }
}

Model에서 Name과 Password속성에는 [Required] Attribute를 decorate 하여 사용자가 반드시 Name과 Passsword를 입력할 수 있도록 하였습니다. 그다음으로 AccountController.cs라는 file을 Controllers folder에 추가합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models.ViewModels;

namespace MyWebApp.Controllers
{
    public class AccountController : Controller
    {
        private UserManager<IdentityUser> _userManager;
        private SignInManager<IdentityUser> _signInManager;

        public AccountController(UserManager<IdentityUser> userMgr, SignInManager<IdentityUser> signInMgr)
        {
            _userManager = userMgr;
            _signInManager = signInMgr;
        }

        public ViewResult Login(string returnUrl)
        {
            return View(new UserLogin
            {
                ReturnUrl = returnUrl
            });
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(UserLogin loginModel)
        {
            if (ModelState.IsValid)
            {
                IdentityUser user = await _userManager.FindByNameAsync(loginModel.Name);

                if (user != null)
                {
                    await _signInManager.SignOutAsync();

                    if ((await _signInManager.PasswordSignInAsync(user, loginModel.Password, false, false)).Succeeded)
                    {
                        return Redirect(loginModel?.ReturnUrl ?? "/Admin");
                    }
                }

                ModelState.AddModelError("", "이름 또는 비밀번호가 일치하지 않습니다.");
            }

            return View(loginModel);
        }

        [Authorize]
        public async Task<RedirectResult> Logout(string returnUrl = "/")
        {
            await _signInManager.SignOutAsync();

            return Redirect(returnUrl);
        }
    }
}

만약 사용자가 위에서 언급한 /Account/Login으로 Redirect가 된다면 우선 Get요청에 반응하는 Login Action Method가 먼저 요청을 받게 되고 인증이 성공적으로 수행된 경우 어떤 URL로 다시 Redirect 시킬지에 대한 returnUrl값을 가진 객체를 사용하여 기본 View Page의 Rendering을 시도할 것입니다.

 

사용자가 자신의 Name과 Password값과 같은 인증정보를 전달하게 되면 그때는 Post요청에 반응하는 Login Action Method가 요청을 받게 되고 해당 사용자의 Name을 통해 사용자 객체를 가져온 뒤 Model의 Password속성을 통해 사용자 인증을 수행하게 됩니다. 인증이 성공적으로 수행된다면 /Admin으로의 Redirect를 수행하지만 그렇지 않으면 Model에 Validation Error를 생성하고 기본 View Page의 Rendering을 시도합니다. 이때 UserManager<IdentityUser>는 사용자 확인을 SignInManager<IdentityUser>는 사용자의 인증처리를 담당합니다.

 

Controller를 위와 같이 구현했으므로 이제 해당 Action Method에 대한 기본 View Page를 추가해야 하므로 이를 위해 Views->Account folder에 Login.cshtml이름의 View File을 생성합니다.

@model UserLogin
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <title>로그인</title>
</head>
<body>
    <div asp-validation-summary="All"></div>
    <form asp-action="Login" asp-controller="Account" method="post">
        <input type="hidden" asp-for="ReturnUrl" />

        <label asp-for="Name"></label>
        <input asp-for="Name" /><div asp-validation-for="Name"></div>

        <label asp-for="Password"></label>
        <input asp-for="Password" type="password" /><div asp-validation-for="Password" class="text-danger"></div>
        
        <button type="submit">로그인</button>
    </form>
</body>
</html>

Login이 수행된 이후 다시 Logout을 처리하기 위한 Button도 필요한데 이 Button은 아래와 같이 Pages->Admin folder의 MainLayout.cshtml에 추가합니다.

@inherits LayoutComponentBase

<div>
    <p>관리자 페이지</p>
</div>
<div>
    <div>
        <a href="/account/logout">로그아웃</a>
        <NavLink href="/admin/products" Match="NavLinkMatch.Prefix">
            제품관리
        </NavLink>
        <NavLink href="/admin/orders" Match="NavLinkMatch.Prefix">
            주문관리
        </NavLink>
    </div>
</div>
<div>
    @Body
</div>

이제 Project를 시작하여 /Admin으로 이동해 다음과 같이 Login화면이 표시되는지 확인합니다.

Login화면이 정상적으로 표시되면 Name과 Password에 위에서 설정한 값인 'admin'과 '!Admin123'을 차례로 입력하고 '로그인'button을 눌러 /Admin으로의 접근이 정상적으로 수행되는지 확인합니다.

이제 '로그아웃'을 클릭하여 Project의 초기화 화면이 제대로 표시되는지를 확인합니다. 이는 Logout처리가 제대로 수행되었음을 의미합니다.

다시 /Admin으로 이동을 시도해 Login화면을 표시한 후 이번에는 고의적으로 잘못된 Name이나 Password값을 입력하여 인증처리가 거부되는지를 확인합니다.

5. 배포하기

 

이제 Project를 거의 완성하는 단계에 왔으므로 해당 Web Application을 실제 사용하기 위한 배포 단계에 들어가고자 합니다. ASP.NET Core Application에서는 배포를 위한 여러 방식이 존재하지만 여기서는 Docker Container를 활용하여 배포를 진행해 보고자 합니다. Docker container를 활용하면 원하는 Hosting Platform에서 Application을 동작시켜볼 수 있습니다.

 

(1) 오류제어 Page 설정하기

 

우선 Project를 배포하기 이전에 오류에 관한 처리를 먼저 진행하고자 합니다. 현재 Project에서는 Error가 발생하는 경우 개발자에게 도움이 될만한 정보를 제공해 주는 Error Page가 사용되도록 설정되어 있습니다. 그러나 Application이 배포되면 일반 사용자에게 같은 Error Page가 보여주기에는 무리가 있으므로 좀 더 단순한 형태의 Error Page를 만들어 볼 것입니다. 이를 위해 Pages Folder에 Error.cshtml file을 아래와 같이 추가합니다.

@page "/error"

@model MyWebApp.Pages.ErrorModel

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
    <head>
        <title>Error</title>
    </head>
    <body>
        <h2>오류가 발생하였습니다.</h2>
        <h3>문제가 지속되면 관리자에게 문의하시기 바랍니다.</h3>
    </body>
</html>

오류 페이지는 다른 View나 Component에 의존하지 않는 독립된 형태로 만들고 되도록이면 Programming적 요소를 최소한으로 줄이는 것이 좋습니다. 다른 외부적 요인을 최소한으로 줄여 Error Page를 사용자에게 표시하고자 하기 위함입니다.

 

그리고 Program.cs를 수정해 위에서 추가한 Error Page가 Application에서 예외(오류)가 발생한 경우 사용될 수 있도록 합니다.

var app = builder.Build();

if (app.Environment.IsProduction())
    app.UseExceptionHandler("/error");

app.UseStaticFiles();

 

예제에서 app.Environment로 사용된 IWebHostEnvironment Interface는 Application이 동작하는 환경을 알아내기 위한 것이며 위의 설정은 UseExceptionHandler Method가 Application이 실제 배포된 Live환경인 경우에 호출됨을 의미합니다.

 

또한 배포는 Docker container를 사용한다고 했는데 이를 위해 필요한 지역화 설정도 같이 진행합니다.

if (app.Environment.IsProduction())
    app.UseExceptionHandler("/error");

app.UseRequestLocalization(opts =>
{
    opts.AddSupportedCultures("ko-KR")
    .AddSupportedUICultures("ko-KR")
    .SetDefaultCulture("ko-KR");
});

(2) 배포를 위한 환경설정 만들기

 

환경설정을 위해 사용되는 json file은 connection string과 같은 설정을 정의하는 데 사용되며 동시에 Application이 동작하는 환경(development, staging, production 등)에서 적용되는 file을 각각 개별적으로 생성할 수 있습니다. 해당 Project에서는 이미 appsettings.json과 appsettings.Development.json이라는 이름의 file이 존재하지만 지금 Project를 실제 배포할 것이므로 그에 맞는 appsettings.Production.json이름의 json file을 추가하고 Production환경에 필요한 설정만을 정의하도록 합니다.

{
  "ConnectionStrings": {
    "MyDBConnection": "Server=sqlserver;Database=MyWebDB;MultipleActiveResultSets=true;User=sa;Password=!abc123123",
    "IdentityConnection": "Server=sqlserver;Database=Identity;MultipleActiveResultSets=true;User=sa;Password=!abc123123"
  }
}

예제에서 DB로의 연결 설정은 밑에서 생성하게 될 Docker Image에 맞추어 작성된 것이며 sa계정의 비밀번호는 임의로 지정할 수 있습니다. 다만 SQL DB Server가 따로 존재하거나 각각의 Docker container에서 동작하는 SQL Server에 따라 연결에 필요한 설정이 달라질 수 있으므로 주의해야 합니다.

 

(3) Docker Image 생성

 

계속해서 Application을 위한 Docker Image를 생성합니다. Docker Image는 향 후 Microsoft Azure나 Amazon Web Services와 같은 container환경에 배포될 수 있습니다.

 

● Docker Desktop 설치

 

Docker와의 작업을 위해 가장 먼저 필요한 것은 Docker를 설치하는 것입니다. 이를 위해 docker.com으로 이동해 Docker Desktop을 내려받아 설치합니다.

 

잠깐 Docker를 설치하기 전에 먼저 확인해야 할 사항이 있습니다. 먼저 Windows라면 Hyper-V가 설치되어 있어야 합니다. Docker는 가상화이며 이 가상화 기능을 현재 System에 의존하여 구현하기 때문입니다.

또한 CPU도 가상화 기능이 활성화되어 있어야 합니다.

위의 사항이 확인되었으면 docker.com으로 이동하여 Docker Desktop을 내려받습니다.

설치 과정은 굉장히 간단하므로 그대로 '다음'을 눌러 진행하기만 하면 됩니다.

다만 설치 option 중 WSL2를 사용할지에 대한 내용이 나오는데 WSL2를 사용할 거라면 당연히 WSL2도 역시 설치되어 있어야 합니다.

 

● Application 배포하기

 

우선 Project는 bin\Release\net6.0\publish\을 위치로 publish를 수행합니다.

● Docker Compose 지원 추가하기

 

Docker을 위해서는 2개의 파일이 필요한데 하나는 Dockerfile입니다. 확장자가 없으므로 Visual Studio에서 임의의 txt 파일을 추가한뒤 .txt확장자를 제거하고 이름을 Dockerfile로 변경합니다. 그리고 해당 파일의 내용을 아래와 같이 저장합니다.

FROM mcr.microsoft.com/dotnet/aspnet:6.0
COPY /bin/Release/net6.0/publish/ MyWebApp/
ENV ASPNETCORE_ENVIRONMENT Production
ENV Logging__Console__FormatterName=Simple
EXPOSE 5000
WORKDIR /MyWebApp
ENTRYPOINT ["dotnet", "MyWebApp.dll", "--urls=http://0.0.0.0:5000"]

그리고 두 번째 file은 docker-compose.yml 입니다. 해당 file도 마찬가지로 Visual Studio에서 직접 지정할 방법이 없으므로 임의의 txt 파일을 추가한 뒤 파일명을 docker-compose.yml로 바꿔줘야 합니다. 그리고 해당 file의 내용은 아래와 같이 저장합니다.

version: '3.4'

services:
  mywebapp:
    build: .
    ports:
     - "5000:5000"
    environment:
     - ASPNETCORE_ENVIRONMENT=Production
    depends_on:
     - sqlserver
  sqlserver:
   image: "mcr.microsoft.com/mssql/server"
   environment:
    SA_PASSWORD: "!abc123123"
    ACCEPT_EULA: "Y"

이제 cmd에서 Project folder로 이동해 아래 명령을 내립니다.

docker-compose build

그러면 ASP.NET Core Application을 위한 Docker Image를 내려받기 시작합니다. 이 과정은 시간이 걸릴 수 있으므로 잠시 기다려 줍니다.

 

내려받기가 완료되면 우선 아래 명령을 통해 Docker의 SQL Server Container를 먼저 실행합니다.

docker-compose up sqlserver

그런데 만약 실행 과정에서 아래와 같은 오류가 나온다면

image operating system "linux" cannot be used on this platform sqlserver

Docker Container의 실행환경을 'Switch to Windows Containers...'로 변경해 보시기 바랍니다. sqlserver는 시간이 꽤 걸리므로 기다려야 합니다.

 

위 명령으로 sqlserver가 정상적으로 작동한다고 판단되면(command가 끝나지 않으므로 output 되는 message로 판단해야 합니다.) 별도의 cmd창을 열어 이제 아래 명령으로 Application의 Docker Container를 실행합니다.

docker-compose up mywebapp

실행 도중 아래와 같은 방화벽 허용 화면이 나온다면 반드시 'Allow access'를 선택해야 합니다.

여기까지 완료한 뒤 Web browser를 통해 localhost:5000으로 접속을 시도하면 아래와 같이 Project가 실행될 것입니다.

 

728x90