Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# - REST API를 이용해 Azure Kudu서비스 이용 - 웹 앱 확장 처리

지난 글에서 Kudu를 REST API로 접근해 Web App의 파일들을 다루는 방법을 설명했습니다.

C# - REST API를 이용해 Kudu서비스 이용 - 파일 처리
; https://www.sysnet.pe.kr/2/0/11730

그리고 이번에는 Web App(App Services)의 확장을,

Azure Web App 확장 예제 제작
; https://www.sysnet.pe.kr/2/0/11540

Kudu로 다루는 방법을 설명합니다.




확장 모듈을 다루는 Kudu 서비스는 다음의 2가지 정도로 나뉩니다.

  • api/extensionfeed
  • api/siteextensions

이 중에서 extensionfeed는 gallery를 기준으로 한 것입니다. 즉, 확장으로 등록될 수 있는 목록이나 그 확장의 정보를 원한다면 api/extensionfeed 경로를 이용할 수 있습니다. 유의할 것은, 이때 (Preview에 상관없이) 가장 마지막 버전을 기준으로 한 목록만을 가져온다는 점입니다. 따라서 특정 버전을 원하거나 stable 버전을 원한다면 Kudu가 아닌, gallery 그 자체(NuGet)의 REST API를 사용해 조회를 해야 합니다.

다른 하나인 api/siteextensions의 경우 해당 Web App에 설치된 확장 목록을 대상으로 합니다. 가령 Web App에 설치된 모든 확장 모듈의 목록을 가져오고 싶다면 다음과 같은 API를 사용할 수 있습니다.

public async Task<List<SiteExtensionItem>> GetInstalledSiteExtensionListAsync(string filter = null)
{
    string url = $"{_baseUrl}api/siteextensions";

    if (string.IsNullOrEmpty(filter) == false)
    {
        url = url + "?filter=" + filter;
    }

    string text = await _httpClient.GetStringAsync(url);

    List<SiteExtensionItem> list = new List<SiteExtensionItem>();
    list.AddRange(Newtonsoft.Json.JsonConvert.DeserializeObject<SiteExtensionItem[]>(text));

    return list;
}

또는 Web App에 설치된 특정 Package 정보만을 가져오고 싶다면 api/siteextensions 하위로 package id를 추가하면 됩니다.

public async Task<SiteExtensionItem> GetInstalledSiteExtensionAsync(string packageId)
{
    string url = $"{_baseUrl}api/siteextensions/{packageId}";
    string text = await _httpClient.GetStringAsync(url);

    return Newtonsoft.Json.JsonConvert.DeserializeObject<SiteExtensionItem>(text);
}

그리고 이미 설치된 웹 앱 확장의 삭제는 DELETE 쿼리를,

public async Task<MessageResponse> DeleteInstalledSiteExtensionAsync(string packageId)
{
    string url = $"{_baseUrl}api/siteextensions/{packageId}";
    HttpResponseMessage hrm = await _httpClient.DeleteAsync(url);

    string text = await hrm.Content.ReadAsStringAsync();

    MessageResponse mr = new MessageResponse();
    mr.Message = text;
    mr.HttpResultCode = hrm.StatusCode;

    return mr;
}

신규(또는 업그레이드) 설치는 PUT 쿼리를 이용하면 됩니다.

public async Task<MessageResponse> InstallSiteExtension(string packageId)
{
    string url = $"{_baseUrl}api/siteextensions/{packageId}";

    HttpResponseMessage hrm = await _httpClient.PutAsync(url, null);

    string text = await hrm.Content.ReadAsStringAsync();
    MessageResponse mr = Newtonsoft.Json.JsonConvert.DeserializeObject<MessageResponse>(text);
    mr.HttpResultCode = hrm.StatusCode;

    return mr;
}

사실 Kudu API를 이용해 웹 앱 확장을 제어한다면 대부분의 경우 이미 설치된 확장을 최신 버전으로 업데이트하는 스크립트를 만들고 싶을 것입니다. 그런데, 이때 한가지 유의할 점이 있습니다. "DELETE /api/siteextensions/{id}" 호출을 하면 "d:\home\SiteExtensions" 하위에 확장 모듈이 있는 폴더가 삭제되기는 하지만 당연히 특정 파일이 잠겨 있는 경우에는 남아 있게 됩니다. 만약 그 잠겨진 파일이 "웹 앱" 프로세스에서 사용 중이라면 해당 서비스를 재시작하지 않는 한 파일 잠김은 안 풀리게 됩니다. 여기서 문제는, SiteExtension을 새로 설치할 때 해당 패키지 ID의 폴더를 완전히 삭제하려고 든다는 점입니다. 따라서 기존의 파일이 잠겨 있는 경우 설치가 실패하게 됩니다.

설치 실패는, 당연하겠지만 Web App의 Main 사이트를 호스팅하는 w3wp.exe가 내려갈 때까지 계속됩니다. 이런 문제를 방지하려면 확장을 제거할 때 잠김 파일이 없도록 여러분들의 확장을 만드는 것입니다. 사실 이게 가장 좋은 방법인데 만약 그것이 불가능하다면 차선책으로 Main 사이트를 종료하도록 /api/processes API를 사용하는 것을 고려할 수 있습니다.

Process Threads list and minidump gcdump diagsession
; https://github.com/projectkudu/kudu/wiki/Process-Threads-list-and-minidump-gcdump-diagsession

위의 설명에도 나오지만,

GET /api/processes/0 => detail process information of scm site's w3wp.exe. Or use -1 for main site.
DELETE /api/processes/0 => terminate scm site's w3wp.exe process and its children. Or use -1 for main site.

pid가 0인 경우에는 scm 관리 웹 애플리케이션을 의미하고, -1인 경우에는 Web App 애플리케이션을 의미합니다. 따라서 -1을 인자로 해서 DELETE를 하면 AppPool을 Recycle 하는 것과 같은 효과를 냅니다.

public async Task<MessageResponse> RestartMainSiteAsync()
{
    string url = $"{_baseUrl}api/processes/-1";
    HttpResponseMessage hrm = await _httpClient.DeleteAsync(url);

    string text = await hrm.Content.ReadAsStringAsync();
    MessageResponse mr = Newtonsoft.Json.JsonConvert.DeserializeObject<MessageResponse>(text);
    mr.HttpResultCode = hrm.StatusCode;

    return mr;
}

또 하나 유의할 점은 PUT /api/siteextensions/{id} 명령어로 (새로 설치가 아닌) 수행되는 업데이트의 절차는 "삭제 후 설치"와 같습니다. 그리고 위에서 파일이 잠겨 있다거나 하는 등의 오류가 발생하는 시점은 이미 "삭제"가 된 이후 설치하는 단계에서 발생하는 것입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




참고로, Web App이 실행되는 가상 머신 환경에서 shell 명령어를 실행하고 결과를 받는 것도 가능합니다.

internal async Task<MessageResponse> RunCommandAsync(string command, string dir)
{
    string url = $"{_baseUrl}api/command";

    CommandInfo ci = new CommandInfo();
    ci.command = command;
    ci.dir = dir;

    string jsonText = Newtonsoft.Json.JsonConvert.SerializeObject(ci);

    StringContent sc = new StringContent(jsonText, Encoding.UTF8, "application/json");
    HttpResponseMessage hrm = await _httpClient.PostAsync(url, sc);

    string text = await hrm.Content.ReadAsStringAsync();
    MessageResponse mr = Newtonsoft.Json.JsonConvert.DeserializeObject<MessageResponse>(text);
    mr.Result = hrm.StatusCode == HttpStatusCode.OK;

    return mr;
}

물론 Kudu 서비스 자체가 그다지 높은 권한을 가지고 실행되는 것은 아니므로, 가령 iisreset과 같은 관리자 권한이 요구되는 명령을 실행하는 경우에는 다음과 같이 권한 오류가 발생하는 것을 볼 수 있습니다.

await client.RunCommandAsync("iisreset", "");

/* 반환 값

Access denied, you must be an administrator of the remote computer to use this

command. Either have your account added to the administrator local group of

the remote computer or to the domain administrator global group.
*/




참고로, nuget.org에 패키지를 올리면 최초에는 "Validating" 단계에 머물다가 일정 시간이 지난 후에야 "Listed" 상태로 바뀝니다. 문제는 NuGet에서 "Listed" 상태로 바뀌어도 한동안 Azure Web App의 Kudu에서는 "Listed" 상태의 그 버전을 api/extensionfeed API로는 가져오지 못합니다. 따라서 NuGet API로 패키지가 "Listed"로 바뀌었다고 해도 곧바로 Kudu API를 이용해 웹 앱 확장을 설치(또는 업그레이드)할 수 없습니다.

일정 시간이 지난 후 그 버전을 api/extensionfeed API로 가져오게 되었어도 이번에는 다운로드 가능하지 않을 수도 있음을 알아야 합니다. 만약 그 순간에 설치를 시도하려는 경우 다음과 같은 "FileNotFoundException"이 발생할 수 있습니다.

{"Message":"System.IO.FileNotFoundException: Package TestWebAppExtension - 1.0.0.1-alpha not found when try to download.\r\n at Kudu.Core.SiteExtensions.FeedExtensions.<GetPackageStream>d__8.MoveNext() in C:\\Kudu Files\\Private\\src\\master\\Kudu.Core\\SiteExtensions\\FeedExtensions.cs:line 397\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n at Kudu.Core.SiteExtensions.FeedExtensions.<UpdateLocalPackage>d__5.MoveNext() in C:\\Kudu Files\\Private\\src\\master\\Kudu.Core\\SiteExtensions\\FeedExtensions.cs:line 182\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\r\n at Kudu.Core.SiteExtensions.SiteExtensionManager.<InstallExtension>d__27.MoveNext() in C:\\Kudu Files\\Private\\src\\master\\Kudu.Core\\SiteExtensions\\SiteExtensionManager.cs:line 550\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n at Kudu.Core.SiteExtensions.SiteExtensionManager.<TryInstallExtension>d__26.MoveNext() in C:\\Kudu Files\\Private\\src\\master\\Kudu.Core\\SiteExtensions\\SiteExtensionManager.cs:line 335"}


따라서 설치(또는 업그레이드)하는 경우 FileNotFoundException이 발생하면 다시 얼마간 기다렸다 재시도를 해야 합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 10/9/2018]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 61  62  63  64  65  [66]  67  68  69  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
11998정성태7/30/201911914오류 유형: 562. .NET Remoting에서 서비스 호출 시 SYN_SENT로 남는 현상파일 다운로드1
11997정성태7/30/201913471.NET Framework: 850. C# - Excel(을 비롯해 Office 제품군) COM 객체를 제어 후 Excel.exe 프로세스가 남아 있는 문제 [2]파일 다운로드1
11996정성태7/25/201915897.NET Framework: 849. C# - Socket의 TIME_WAIT 상태를 없애는 방법파일 다운로드1
11995정성태7/23/201918956.NET Framework: 848. C# - smtp.daum.net 서비스(Implicit SSL)를 이용해 메일 보내는 방법 [2]
11994정성태7/22/201914450개발 환경 구성: 454. Azure 가상 머신(VM)에서 SMTP 메일 전송하는 방법파일 다운로드1
11993정성태7/22/20199883오류 유형: 561. Dism.exe 수행 시 "Error: 2 - The system cannot find the file specified." 오류 발생
11992정성태7/22/201911674오류 유형: 560. 서비스 관리자 실행 시 "Windows was unable to open service control manager database on [...]. Error 5: Access is denied." 오류 발생
11991정성태7/18/20199180디버깅 기술: 128. windbg - x64 환경에서 닷넷 예외가 발생한 경우 인자를 확인할 수 없었던 사례
11990정성태7/18/201911385오류 유형: 559. Settings / Update & Security 화면 진입 시 프로그램 종료
11989정성태7/18/201910302Windows: 162. Windows Server 2019 빌드 17763부터 Alt + F4 입력시 곧바로 로그아웃하는 현상
11988정성태7/18/201911771개발 환경 구성: 453. 마이크로소프트가 지정한 모든 Root 인증서를 설치하는 방법
11987정성태7/17/201916726오류 유형: 558. 윈도우 - KMODE_EXCEPTION_NOT_HANDLED 블루스크린(BSOD) 문제 [1]
11986정성태7/17/20199513오류 유형: 557. 드라이브 문자를 할당하지 않은 파티션을 탐색기에서 드라이브 문자와 함께 보여주는 문제
11985정성태7/17/20199642개발 환경 구성: 452. msbuild - csproj에 환경 변수 조건 사용 [1]
11984정성태7/9/201917846개발 환경 구성: 451. Microsoft Edge (Chromium)을 대상으로 한 Selenium WebDriver 사용법 [1]
11983정성태7/8/20198896오류 유형: 556. nodemon - 'mocha' is not recognized as an internal or external command, operable program or batch file.
11982정성태7/8/20198895오류 유형: 555. Visual Studio 빌드 오류 - result: unexpected exception occured (-1002 - 0xfffffc16)
11981정성태7/7/201911082Math: 64. C# - 3층 구조의 신경망(분류)파일 다운로드1
11980정성태7/7/201921525개발 환경 구성: 450. Visual Studio Code의 Java 확장을 이용한 간단한 프로젝트 구축파일 다운로드1
11979정성태7/7/201911056개발 환경 구성: 449. TFS에서 gitlab/github등의 git 서버로 마이그레이션하는 방법
11978정성태7/6/201910402Windows: 161. 계정 정보가 동일하지 않은 PC 간의 인증을 수행하는 방법 [1]
11977정성태7/6/201914971오류 유형: 554. git push - error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413 Request Entity Too Large
11976정성태7/4/20199325오류 유형: 553. (잘못 인증 한 후) 원격 git repo 재인증 시 "remote: HTTP Basic: Access denied" 오류 발생
11975정성태7/4/201917844개발 환경 구성: 448. Visual Studio Code에서 콘솔 응용 프로그램 개발 시 "입력"받는 방법
11974정성태7/4/201913199Linux: 22. "Visual Studio Code + Remote Development"로 윈도우 환경에서 리눅스(CentOS 7) C/C++ 개발
11973정성태7/4/201912412Linux: 21. 리눅스에서 공유 라이브러리가 로드되지 않는다면?
... 61  62  63  64  65  [66]  67  68  69  70  71  72  73  74  75  ...