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

비밀번호

댓글 작성자
 




... 91  92  93  94  95  96  97  98  99  100  [101]  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11404정성태12/16/201729824.NET Framework: 709. C# - OpenCvSharp을 이용한 동영상(avi, mp4, ...) 처리 + Direct2D [7]파일 다운로드1
11403정성태12/16/201732452.NET Framework: 708. C# - OpenCvSharp을 이용한 동영상(avi, mp4, ...) 처리 [3]파일 다운로드1
11402정성태12/15/201737148.NET Framework: 707. OpenCV 응용 프로그램을 C#으로 구현 - OpenCvSharp [2]파일 다운로드1
11401정성태12/15/201726095.NET Framework: 706. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 + Direct2D 출력 [2]파일 다운로드1
11400정성태12/14/201728961.NET Framework: 705. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 [9]파일 다운로드1
11399정성태12/13/201717496.NET Framework: 704. Win32 API의 UnionRect를 닷넷 BCL의 Rectangle.Union으로 바꿀 때 주의 사항
11398정성태12/13/201717698오류 유형: 442. ASP.NET Core Web Application (on .NET Framework) 프로젝트에서 외부 라이브러리 동적 로드 시 런타임 버전 문제파일 다운로드1
11397정성태12/12/201720267.NET Framework: 703. 양자 컴퓨팅을 위한 마이크로소프트의 Q# 언어
11396정성태12/8/201742629개발 환경 구성: 343. Visual Studio - 리눅스 용 프로젝트의 인텔리센스를 위한 헤더 파일 처리 방법 [3]
11395정성태12/8/201718539오류 유형: 441. 이벤트 로그 - Time Provider NtpClient: No valid response has been received from domain controller
11394정성태12/8/201718170개발 환경 구성: 342. 비주얼 스튜디오에서 실행하던 ASP.NET Core (.NET Framework) 응용 프로그램을 명령행에서 실행하는 방법
11393정성태12/7/201722694Windows: 145. 윈도우 10 빌드 17046부터 WSL에서 백그라운드 작업 지원 [5]
11392정성태12/7/201717977개발 환경 구성: 341. openSUSE에 닷넷 코어 설치
11391정성태12/7/201720839개발 환경 구성: 340. WSL을 이용해 윈도우 PC 1대에서 openSUSE 응용 프로그램을 Visual Studio로 개발하는 방법 [1]
11390정성태12/7/201729476개발 환경 구성: 339. WSL을 이용해 윈도우 PC 1대에서 Linux 응용 프로그램을 Visual Studio로 개발하는 방법 [6]
11389정성태12/7/201718145오류 유형: 440. .NET Core 오류 - 0x80131620 Unable to load DLL 'libuv'
11388정성태12/6/201721790개발 환경 구성: 338. WSL 또는 Ubuntu에 닷넷 코어 설치 [3]
11387정성태12/6/201722108오류 유형: 439. 이벤트 로그 - Data Sharing Service 서비스의 %%3239247874 오류 메시지
11386정성태12/5/201717636오류 유형: 438. Hyper-V - '...' failed to add device 'Virtual CD/DVD Disk'
11385정성태12/5/201730759VC++: 121. DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++) [16]파일 다운로드1
11384정성태12/5/201720057오류 유형: 437. Visual C++ - Cannot open include file: 'SDKDDKVer.h'
11383정성태12/4/201723208디버깅 기술: 110. 비동기 코드 실행 중 예외로 인한 ASP.NET 프로세스 비정상 종료 현상 [1]
11382정성태12/4/201721797오류 유형: 436. System.Data.SqlClient.SqlException (0x80131904): Connection Timeout Expired 예외 발생 시 "[Pre-Login] initialization=48; handshake=1944;" 값의 의미
11381정성태11/30/201718190.NET Framework: 702. 한글이 포함된 바이트 배열을 나눈 경우 한글이 깨지지 않도록 다시 조합하는 방법(두 번째 이야기)파일 다운로드1
11380정성태11/30/201718232디버깅 기술: 109. windbg - (x64에서의 인자 값 추적을 이용한) Thread.Abort 시 대상이 되는 스레드를 식별하는 방법
11379정성태11/30/201718953오류 유형: 435. System.Web.HttpException - Session state has created a session id, but cannot save it because the response was already flushed by the application.
... 91  92  93  94  95  96  97  98  99  100  [101]  102  103  104  105  ...