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

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12696정성태7/6/20217891VC++: 146. 운영체제의 스레드 문맥 교환(Context Switch)을 유사하게 구현하는 방법파일 다운로드2
12695정성태7/3/20217974VC++: 145. C 언어의 setjmp/longjmp 기능을 Thread Context를 이용해 유사하게 구현하는 방법파일 다운로드1
12694정성태7/2/20219879Java: 24. Azure - Spring Boot 앱을 Java SE(Embedded Web Server)로 호스팅 시 로그 파일 남기는 방법 [1]
12693정성태6/30/20217653오류 유형: 731. Azure Web App Site Extension - Failed to install web app extension [...]. {1}
12692정성태6/30/20217529디버깅 기술: 180. Azure - Web App의 비정상 종료 시 남겨지는 로그 확인
12691정성태6/30/20218349개발 환경 구성: 573. 테스트 용도이지만 테스트에 적합하지 않은 Azure D1 공유(shared) 요금제
12690정성태6/28/20219141Java: 23. Azure - 자바(Java)로 만드는 Web App Service - Tomcat 호스팅
12689정성태6/25/20219709오류 유형: 730. Windows Forms 디자이너 - The class Form1 can be designed, but is not the first class in the file. [1]
12688정성태6/24/20219415.NET Framework: 1073. C# - JSON 역/직렬화 시 리플렉션 손실을 없애는 JsonSrcGen [2]파일 다운로드1
12687정성태6/22/20217434오류 유형: 729. Invalid data: Invalid artifact, java se app service only supports .jar artifact
12686정성태6/21/20219825Java: 22. Azure - 자바(Java)로 만드는 Web App Service - Java SE (Embedded Web Server) 호스팅
12685정성태6/21/202110057Java: 21. Azure Web App Service에 배포된 Java 프로세스의 메모리 및 힙(Heap) 덤프 뜨는 방법
12684정성태6/19/20218499오류 유형: 728. Visual Studio 2022부터 DTE.get_Properties 속성 접근 시 System.MissingMethodException 예외 발생
12683정성태6/18/20219948VS.NET IDE: 166. Visual Studio 2022 - Windows Forms 프로젝트의 x86 DLL 컨트롤이 Designer에서 오류가 발생하는 문제 [1]파일 다운로드1
12682정성태6/18/20217724VS.NET IDE: 165. Visual Studio 2022를 위한 Extension 마이그레이션
12681정성태6/18/20217039오류 유형: 727. .NET 2.0 ~ 3.5 + x64 환경에서 System.EnterpriseServices 참조 시 CS8012 경고
12680정성태6/18/20218135오류 유형: 726. python2.7.exe 실행 시 0xc000007b 오류
12679정성태6/18/20218746COM 개체 관련: 23. CoInitializeSecurity의 전역 설정을 재정의하는 CoSetProxyBlanket 함수 사용법파일 다운로드1
12678정성태6/17/20218008.NET Framework: 1072. C# - CoCreateInstance 관련 Inteop 오류 정리파일 다운로드1
12677정성태6/17/20219438VC++: 144. 역공학을 통한 lxssmanager.dll의 ILxssSession 사용법 분석파일 다운로드1
12676정성태6/16/20219520VC++: 143. ionescu007/lxss github repo에 공개된 lxssmanager.dll의 CLSID_LxssUserSession/IID_ILxssSession 사용법파일 다운로드1
12675정성태6/16/20217556Java: 20. maven package 명령어 결과물로 (war가 아닌) jar 생성 방법
12674정성태6/15/20218318VC++: 142. DEFINE_GUID 사용법
12673정성태6/15/20219476Java: 19. IntelliJ - 자바(Java)로 만드는 Web App을 Tomcat에서 실행하는 방법
12672정성태6/15/202110596오류 유형: 725. IntelliJ에서 Java webapp 실행 시 "Address localhost:1099 is already in use" 오류
12671정성태6/15/202117268오류 유형: 724. Tomcat 실행 시 Failed to initialize connector [Connector[HTTP/1.1-8080]] 오류
... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...