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

(시리즈 글이 8개 있습니다.)
개발 환경 구성: 371. Azure Web App 확장 예제 - Simple WebSite Extension
; https://www.sysnet.pe.kr/2/0/11505

개발 환경 구성: 379. Azure Web App 확장 예제 제작
; https://www.sysnet.pe.kr/2/0/11540

개발 환경 구성: 380. Azure Web App 확장 배포 방법
; https://www.sysnet.pe.kr/2/0/11541

개발 환경 구성: 408. C# - REST API를 이용해 Azure Kudu 서비스 이용 - 파일 처리
; https://www.sysnet.pe.kr/2/0/11730

개발 환경 구성: 409. C# - REST API를 이용해 Azure Kudu 서비스 이용 - 웹 앱 확장 처리
; https://www.sysnet.pe.kr/2/0/11731

개발 환경 구성: 578. Azure - Java Web App Service를 위한 Site Extension 제작 방법
; https://www.sysnet.pe.kr/2/0/12711

개발 환경 구성: 579. Azure - 리눅스 호스팅의 Site Extension 제작 방법
; https://www.sysnet.pe.kr/2/0/12712

개발 환경 구성: 605. Azure App Service - Kudu SSH 환경에서 FTP를 이용한 파일 전송
; https://www.sysnet.pe.kr/2/0/12855




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이 발생하면 다시 얼마간 기다렸다 재시도를 해야 합니다.




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







[최초 등록일: ]
[최종 수정일: 11/27/2024]

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

비밀번호

댓글 작성자
 




... 151  152  153  154  155  156  157  158  159  [160]  161  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1048정성태5/27/201132259개발 환경 구성: 123. Apache 소스를 윈도우 환경에서 빌드하기
1047정성태5/27/201126120.NET Framework: 217. Firebird ALinq Provider - 날짜 필드에 대한 낙관적 동시성 쿼리 오류
1046정성태5/26/201130787.NET Framework: 216. 라이선스까지도 뛰어넘는 .NET Profiler [5]
1045정성태5/24/201131877.NET Framework: 215. 닷넷 System.ComponentModel.LicenseManager를 이용한 라이선스 적용 [1]파일 다운로드1
1044정성태5/24/201132442오류 유형: 122. zlib 빌드 오류 - inflate.obj : error LNK2001: unresolved external symbol _inflate_fast
1043정성태5/24/201131419.NET Framework: 214. 무료 Linq Provider - DbLinq를 이용한 Firebird 접근파일 다운로드1
1042정성태5/23/201137740개발 환경 구성: 122. PHP 소스를 윈도우 환경에서 빌드하기
1041정성태5/22/201128634.NET Framework: 213. Linq To SQL - ALinq Provider를 이용하여 Firebird 사용파일 다운로드1
1040정성태5/21/201138962개발 환경 구성: 121. .NET 개발자가 처음 설치해 본 Apache + PHP [2]
1039정성태5/17/201131656.NET Framework: 212. Firebird 데이터베이스와 ADO.NET [2]파일 다운로드1
1038정성태5/16/201133632개발 환경 구성: 120. .NET 프로그래머에게도 유용한 Firebird 무료 데이터베이스 [2]
1037정성태5/11/201128470개발 환경 구성: 119. Visual Studio Professional 이하 버전에서도 TFS의 정적 코드 분석 정책 연동이 가능할까? [3]
1036정성태5/7/201194266오류 유형: 121. Access DB에 대한 32bit/64bit OLE DB Provider 관련 오류 [11]
1035정성태5/7/201129026오류 유형: 120. File cannot be opened. Ensure it is a valid Data Link file.
1034정성태5/2/201126060.NET Framework: 211. 파일 잠금 없이 .NET 어셈블리의 버전을 구하는 방법 [2]파일 다운로드1
1033정성태5/1/201131775웹: 19. IIS Express - appcmd.exe를 이용한 applicationHost.config 변경 [2]
1032정성태5/1/201128425웹: 18. IIS Express를 NT 서비스로 변경
1031정성태4/30/201129580웹: 17. IIS Express - "IIS Installed Versions Manager Interface"의 IIISExpressProcessUtility 구하는 방법 [1]파일 다운로드1
1030정성태4/30/201151850개발 환경 구성: 118. IIS Express - localhost 이외의 호스트 이름으로 접근하는 방법 [4]파일 다운로드1
1029정성태4/28/201140980개발 환경 구성: 117. XCopy에서 파일/디렉터리 확인 질문 없애기 [2]
1028정성태4/27/201138372오류 유형: 119. Visual Studio 2010 SP1 설치 후 Windows Phone 개발자 도구로 인한 재설치 문제 [3]
1027정성태4/25/201127550디버깅 기술: 40. 상황별 GetFunctionPointer 반환값 정리 - x86파일 다운로드1
1026정성태4/25/201145837디버깅 기술: 39. DebugDiag 1.1을 사용한 덤프 분석 [7]
1025정성태4/24/201127895개발 환경 구성: 116. IIS 7 관리자 - Active Directory Certification Authority로부터 SSL 사이트 인증서 받는 방법 [2]
1024정성태4/22/201129198오류 유형: 118. Windows 2008 서버에서 Event Viewer / PowerShell 실행 시 비정상 종료되는 문제 [1]
1023정성태4/20/201130085.NET Framework: 210. Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 [1]
... 151  152  153  154  155  156  157  158  159  [160]  161  162  163  164  165  ...