컴퓨팅/프로그래밍

ASP.NET MVC, IIS 호스팅 디스크 쿼터 관리

epician 2015. 1. 12. 04:08

1. 서론

IIS(Internet Information Server) 웹서버의 경우, 자체적인 디스크 쿼터(Disk Quota) 기능이 없습니다. 따라서, 호스팅 중인 사이트의 디스크 사용량을 제한하려면 윈도 서버에서 제공하는 디스크 쿼터나 디렉토리 쿼터 기능 중에 하나를 사용해야 합니다.

디스크 쿼터를 설정하는 것 자체는 마우스 클릭 몇 번으로 끝날 정도로 간단한데, 문제는 설정된 쿼터 내용(쿼터 크기, 사용량 등)을 웹 어플리케이션 쪽에서 받아오기가 어렵습니다.

2. 쿼터 설정

2-1. 디스크 쿼터 설정

디스크 등록정보에서 각 디스크 별로 각 계정에 할당된 디스크 쿼터를 제한할 수 있습니다. 아주 오래된 방법이죠.

디스크 쿼터 사용 설정디스크 쿼터 사용 설정

두 번째 옵션 'Deny disk space to a user exceeds quota limit' 를 켜두지 않으면, 쿼터를 초과해도 디스크 I/O가 차단되지 않습니다.

디스크 쿼터 내역디스크 쿼터 내역

위 화면처럼 디스크 쿼터 내역이 관리됩니다.

이 방법의 단점은 디스크 쿼터가 전체 디스크에 적용된다는 점인데, 예를 들어, 웹 어플리케이션이 저장되는 호스팅 공간 따로, 데이터 파일이 저장되는 공간 따로 각각의 쿼터를 설정할 수 없습니다. 그리고 진짜 귀찮은 점은 계정을 기준으로 쿼터를 잡는다는 데 있습니다. 예를 들어, 10개의 웹 어플리케이션을 호스팅한다면 각각의 어플리케이션 풀(Application Pool)을 다 따로 설정해야 각각의 사이트에 쿼터를 지정할 수 있습니다.

호스팅되는 사이트가 몇 개 없다면 이 방법도 나쁘지 않습니다만, 사이트 개수가 늘어날 수록 귀찮아질 가능성 200%.

이 방법으로 디스크 쿼터를 관리할 경우, 쿼터값(제한 크기, 사용량 등)은 WMI 기능을 통해 받아와서 확인할 수 있는데, 자세한 내용는 이 포스트(http://blogs.msdn.com/b/mpoulson/archive/2005/05/24/disk-quota-and-net-wmi-and-microsoft-diskquota-1.aspx)를 참고하시면 됩니다.

2-2. 디렉토리 쿼터 설정

File Server Resource Manager (이하 FSRM)을 통해 디렉토리 별로 쿼터를 지정할 수 있습니다. 저는 최근에야 이 기능을 알았는데, 관련 내용을 찾다보니 이 기능이 Windows Server 2003 R2부터 탑재됐다고 합니다.

최근 서버 OS에는 이 기능이 기본적으로 설치되어 있지 않으니, 사용하고자 한다면 서버 매니저의 'Add Roles and Features'를 통해 FSRM을 설치해줘야 합니다.

그런 후에 서버 매니저의 Tools 메뉴를 통해 'File Server Resource Manager'를 실행하시면 됩니다. (또는 명령창에서 fsrm.msc)

FSRM 설정FSRM 설정

위 화면은 샘플로 C:\Uploads 디렉토리에 100MB 제한을 설정한 모습입니다. 다른 내용은 읽어보시면 바로 이해되실 건데, Soft 그리고 Hard Quota는 조금 헷갈릴 수 있습니다. Soft Quota는 쿼터를 설정하고 추적, 관리하긴 하지만 제한 크기가 넘어가도 디스크 I/O를 차단하지 않습니다. 그 외의 모든 내용은 Hard Quota와 동일합니다. (Hard Quota는 제한량을 넘어서면 I/O가 차단됩니다.)

FSRM 설정까지는 어려울 게 전혀 없습니다.
어려울 게 없을 뿐 아니라, 윈도 계정과는 상관 없이 특정 디렉토리에만 쿼터를 걸 수 있으므로 웹 호스팅 용도의 쿼터 설정에 딱 들어맞아 보입니다. 웹 어플리케이션의 계정을 다 따로 설정할 필요가 없어집니다.

3. 웹 어플리케이션에서 쿼터 설정 내용 받아오기

호스팅 중인 사이트에 쿼터가 걸려 있다면 해당 사이트의 관리자 페이지 등에서 쿼터 설정이나 사용량에 대한 정보를 제공해줄 수 있어야 합니다. 하지만, 아무 것도 아닌 이게 생각보다 까다롭습니다.

WMI 기능을 통해서 원하는 데이터를 얻을 수 있습니다만, 몇 가지 복병이 저희를 귀찮게 합니다.

먼저, 사용권한 문제!
WMI Query를 사용하려면 최소한 Network Service 계정을 요구합니다. (기억이 가물가물해서 정확하지 않습니다. ^^;;)
하지만, 일반적으로 IIS에서 호스팅되는 웹 어플리케이션은 가상 계정인 IIS APPPOOL 그룹이죠. 이 그룹에 지정된 권한 정도는 Users 그룹하고 비슷한데, WMI Query를 사용할 수 없을 겁니다.

[참고] Application Pool Identities(IIS APPPOOL 계정)
http://www.iis.net/learn/manage/configuring-security/application-pool-identities

그렇다면 IIS APPPOOL 그룹이 아니라 Network Service 계정으로 지정하면 간단히 해결되지 않으냐라고 생각하실 겁니다.

그러나, 다시 사용권한 문제!
Network Service 계정으로 웹 어플리케이션을 실행시켜서 WMI에 접근했다쳐도 FSRM의 Quota 데이터를 받아오질 못합니다. FSRM에 접근하려면 로컬 관리자 계정이나 그에 준하는 레벨의 계정이 필요하거든요. 망했어요! 쉽게 해결 안되요 ㅋㅋ

WMI의 FSRM 데이터에 접근할 수 있도록 DCOM, WMI 권한 설정을 이리저리 바꿔봐도 해결되질 않습니다. 그렇다고 무식하게 웹 어플리케이션에 관리자 권한을 줄 순 없잖아요! 다 포기하고 다른 방법을 찾아봐야 겠습니다.

3-1. 별도의 프로그램 만들기

처음엔 별도의 프로그램이나 스크립트를 만들어서 Task Scheduler에 등록하는 건 어떨까 싶었습니다. 매 10~30분 정도마다 한번씩 Quota 내역을 받아서 웹 어플리케이션이 접근할 수 있는 별도의 디렉토리에 XML이나 JSON파일로 저장해두면 될거 같습니다.

헌데, 생각을 좀 하다보니 안전한 방법이긴 해도 그다지 깔끔해 보이진 않습니다. 쿼터 정보를 요청하는 클라이언트가 없어도 정해진 시간에 프로그램이 계속 돌아가야 하니 비효율적이기도 하고, 유지보수 면에서도 결코 아름다워 보이지 않습니다.

3-2. 별도의 API 사이트 만들기

그래서, 별도의 프로그램이나 스크립트를 만드는 방법은 버리고, 내부 WEB API 사이트를 만들기로 합니다. 불필요하게 외부 프로그램이 안돌아가도 된다는 장점이 있고, 유지보수 면에서도 훨씬 깔끔해 보입니다.

일단 API 사이트를 만들어보죠~

Visual Studio 2013 프로젝트 생성Visual Studio 2013 프로젝트 생성

Visual Studio 2013 프로젝트 속성Visual Studio 2013 프로젝트 속성

적당한 이름으로 ASP.NET Web Application 프로젝트를 생성합니다. 프로젝트 속성은 .NET Framework 4.5 형식의 Web API를 선택하시면 되고, 인증 옵션은 'No Authentication' 으로 설정하시면 됩니다.

프로젝트가 생성됐으면 가장 먼저 WMI를 사용하기 위해 System.Management 에 대한 Reference를 걸어줍니다.

Reference 설정System.Management에 대한 Reference 설정

프로젝트 템플릿을 통해 자동으로 생성된 파일 가운데 필요 없는 건 다 지우고, Controllers 폴더에 QuotaContoller라고 이름 붙인 새 컨트롤러를 추가합니다. 해당 파일의 내용은 아래와 같습니다.

public class QuotaController : ApiController { // GET: api/Quota/Path public HttpResponseMessage Get(string path) { HttpResponseMessage response = Request.CreateErrorResponse(HttpStatusCode.NotFound, "Specified path was not found."); ; DirectoryQuota quota = null; string directoryPath = HttpUtility.UrlDecode(path); try { ManagementObjectSearcher searcher = new ManagementObjectSearcher("root\\Microsoft\\Windows\\FSRM", "SELECT * FROM MSFT_FSRMQuota"); foreach (ManagementObject queryObj in searcher.Get()) {         // These properties can be used:         // Description, Disabled, Matches Template, Peak Usage, Size, SoftLimit, Template, Usage if (string.Compare(queryObj["Path"].ToString(), directoryPath, true== 0) { quota = new DirectoryQuota() { Path = queryObj["Path"].ToString(), Description = queryObj["Description"].ToString(), Disabled = bool.Parse(queryObj["Disabled"].ToString()), Size = decimal.Parse(queryObj["Size"].ToString()), Usage = decimal.Parse(queryObj["Usage"].ToString()), PeakUsage = decimal.Parse(queryObj["PeakUsage"].ToString()), Template = queryObj["Template"].ToString(), SoftLimit = bool.Parse(queryObj["SoftLimit"].ToString()), MatchesTemplate = bool.Parse(queryObj["MatchesTemplate"].ToString()) }; response = Request.CreateResponse<DirectoryQuota>(HttpStatusCode.OK, quota); break; } } } catch (ManagementException ex) { response = Request.CreateErrorResponse(HttpStatusCode.Forbidden, ex.Message); quota = null; } catch (Exception ex) { response = Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.Message); quota = null; } return response; } }

그리고 위 컨트롤러에서 사용하고 Quota에 대한 정보를 담을 DirectoryQuota 클래스를 Models 폴더 아래에 만듭니다. 클래스 내용은 아래와 같습니다.

public class DirectoryQuota { public string Path { getset; } public string Description { getset; } public decimal Size { getset; } public bool SoftLimit { getset; } public bool Disabled { getset; } public string Template { getset; } public bool MatchesTemplate { getset; } public decimal Usage { getset; } public decimal PeakUsage { getset; } }

이제 뭘 해야 되느냐? 끝났습니다. 너무 간단하죠 ㅋ

파일럿 형태로 만들어본거라  심각하게 간단한데, Production 레벨에서 작업하는 경우라면 WEB API 쪽에서 WMI Query 결과를 캐쉬할 수 있는 뭔가를 넣어주는 것이 좋다고 생각됩니다. WMI Query를 마구잡이로 날려대면 시스템 성능이 떨어지는 경험을 한 적이 있습니다.

이제 이 프로젝트 빌드하여 서버 올려서 테스트를 해봐야겠죠? FSRM이 Windows Server 계열에만 탑재되어 있기 때문에 Windows 8.x 혹은 그 이하의 OS에선 사용할 수 없습니다. 따라서 개발용으로 쓰는 PC에선 테스트가 어렵습니다.

3-3. 테스트 사이트에 올려서 테스트 해보기

일단 저는 InfraHammerBoard라는 테스트 사이트를 하나 만들었습니다.

테스트 사이트 설정테스트 사이트 설정

WEB API 테스트 사이트의 Application Pool은 독립적으로 사용하도록 별도로 지정합니다. 그리고, Bindings 설정에서 포트 83번으로 쓰도록 설정했습니다.

Application Pools에서 테스트 사이트를 선택하고 Identity를 LocalSystem으로 설정합니다. 관리자 계정에 준하는 계정으로 FSRM WMI에 접근하려면 이 계정의 권한이 필요합니다.

테스트 사이트에 올리고 설정을 마쳤으면 테스트를 해봅니다.

http://localhost:83/api/Quota?path=C:\Uploads

웹 브라우저에서 API 테스트 사이트로 접근해보면 Json 파일이 전달되는데, 내용은 아래와 같습니다.

{"Path":"C:\\Uploads","Description":"","Size":104857600.0,"SoftLimit":false,"Disabled":false,"Template":"100 MB Limit","MatchesTemplate":false,"Usage":51558400.0,"PeakUsage":51558400.0}

4. 실제로 해당 API 사용해보기

먼저 위 API를 사용할 사이트가 .NET Framework 4.0을 사용하는 사이트라면 NuGet 매니저를 통해 Microsoft Async (Microsoft.Bcl.Async)를 패키지를 설치합니다. 4.5의 경우엔 설치할 필요 없습니다.

그리고, System.Net.Http.Formatting에 대한 Reference를 걸어줍니다.

Reference 설정System.Net.Http.Formatting에 대한 Reference 설정

Index Action에서 Quota API를 사용하도록 아래처럼 코딩을 해봤습니다.

public async Task<ActionResult> Index() { ViewBag.QuotaUsed = "Unknown"; ViewBag.QuotaSize = "Unknown"; ViewBag.Message = null; if (LastCheck.Minutes >= INTERVAL_MINS) { try { using (var client = new System.Net.Http.HttpClient()) { client.BaseAddress = new Uri("http://vm2012:83/"); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); HttpResponseMessage response = await client.GetAsync("api/Quota?Path=" + HttpUtility.UrlPathEncode(Broker.Config.DataDirectory)); if (response.IsSuccessStatusCode) { DirectoryQuota quota = await response.Content.ReadAsAsync<Models.DirectoryQuota>(); ViewBag.QuotaUsed = ((long)quota.Usage).ToBytesString(); ViewBag.QuotaSize = ((long)quota.Size).ToBytesString(); } } } catch (Exception ex) { Logger.Write("Quota Check", ex.Message, false, ex.ToLogString()); } finally { SetCheck(); } } return View(new SiteConfigModel(Broker.Config)); }

if (LastCheck... 부분과 SetCheck(); 부분은 지속적인 접근으로 성능이 떨어지는 것을 막으려고 넣어둔 코드이니 빼셔도 됩니다.

response.Content.ReadAsAsync<Models.DirectoryQuota>(); 부분에서 API 사이트에서 만들었던 DirectoryQuota클래스를 사용하는 것을 볼 수 있습니다. 아까 만들었던 클래스를 복사해와서 프로젝트의 Models 폴더에 넣고 Namespace만 조정해주시면 됩니다.

5. 보안, 접근 제한 설정

위 WEB API사이트는 LocalSystem 계정(로컬 관리자에 준하는 권한)으로 돌아가는 사이트라 아무나 막 접근해서 쑤셔대면 위험할 수 있습니다. 그래서, 호스팅 중인 내부 사이트에서만 접근할 수 있도록 접근제한을 설정해주는 게 좋습니다.

다시 API 테스트 사이트의 설정으로 가보죠.
IIS 관리자에서 API 테스트 사이트를 선택하고 IIS 그룹 아래에 있는 'IP Address and Domain Restrictions'를 선택합니다.

IP Restrictions 설정 #1IP Restrictions 설정 #1

먼저 Edit Feature Settings를 실행하여 기본적으로 모든 IP에 대해 차단을 설정합니다.
그런 후에 아래처럼 루프백 IP(127.0.0.1 또는 ::1)나 웹서버 IP만 'Add Allow Entry...'를 통해 추가해주면 됩니다.

IP Restrictions 설정 #2IP Restrictions 설정 #2

끝~

며칠 전에 했던 작업을 리마인드해서 쓴 글이라 뭔가 빼먹은 핵심적인 것이 있지 않나 하는 의구심이 마구마구 솟구치고 있습니다. 년초부터 겪고 있는 빌어먹을 금단현상 중에 하나이기도 합니다. 조세저항 차원에서 금연 중이거든요 ㅠ.ㅠ

잘 안풀리는 부분은 즐거운 마음으로 잘 해결하시기 바랍니다. ㅎㅎ