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

비밀번호

댓글 작성자
 




1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13503정성태12/27/20232191Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232298닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232098개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232180디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20232857닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232296오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232311Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232322Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232502Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232564닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232266개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232234Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232350개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232138개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232070오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232384개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232199개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232088오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232162개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232302닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20232878닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232272개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232615개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232311개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232485닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/20232212닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...