[Windows 컨테이너] 3: NT 서비스를 Windows 컨테이너로 마이그레이션하기

남정현

데브시스터즈는 Kubernetes 기반의 게임 개발 및 테스트 인프라를 성공적으로 구축하여 게임 출시에 이르는 모든 과정들을 효율적으로 처리하고 있습니다.

다년간의 성공적인 운영 경험을 바탕으로 Windows Server 기반의 Workload를 지원하기 위한 R&D 작업을 진행하였고, 그 경험담을 공유하는 아티클 시리즈를 몇 편에 걸쳐 DevTech 기술 블로그에 연재할 예정입니다.

Windows 컨테이너 시리즈 글 보기

  1. Windows 컨테이너에 대한 이해
  2. Windows 컨테이너 개발 환경 구축하고 테스트하기
  3. NT 서비스를 Windows 컨테이너로 마이그레이션하기
  4. 베이스 이미지, 격리 방식에 대한 이해

들어가기에 앞서

리눅스와는 달리 Windows 서버 애플리케이션의 경우 독립적인 서버에서 실행되던 애플리케이션을 컨테이너로 옮기는 작업은 매우 많은 고민을 필요로 합니다.

이 때의 고민들은 아키텍처 측면에서의 고민도 있겠지만, 기술적인 문제에 해당되는 경우도 많습니다.

이번에 소개하려는 내용은 컨테이너로 옮기기 까다로운 유형에 해당하는 NT Service 타입의 서버 애플리케이션을 옮기는 방법을 상세하게 알아보겠습니다. 또한, NT Service가 생성하는 다양한 종류의 로그나 기록을 어떻게 표준 출력으로 내보내는지에 대해서도 알아봅니다.

이 글의 내용을 따라하기 위해서는 Windows 컨테이너 개발 환경 구축하고 테스트하기편을 참고해 환경을 설정해야 합니다.

서비스를 컨테이너화하는 방법

서버 응용프로그램을 컨테이너화하는 것은 보통 단독으로 실행되는 프로그램을 기준으로 합니다. 리눅스 및 유닉스 환경에서는 거의 모든 응용프로그램들이 그 즉시 다른 도우미 프로그램을 경유하지 않고 실행되므로 컨테이너화를 하는데 큰 고민을 할 것이 없습니다.

반면 Windows의 경우 서비스 제어 관리자 (Service Control Manager, 이하 SCM)에 의하여 백그라운드 서비스를 다루게 됩니다. 그리고 SCM이 인식할 수 있는 서비스는 통상적인 응용프로그램들과는 동작 방식이 다릅니다. 컨테이너화를 하는데 있어서 허들이 되는 부분은 즉 SCM이라고 할 수 있습니다.

Windows 서비스를 컨테이너화한다는 것은, 컨테이너화할 서비스가 정상적으로 시작되어 SCM에 의하여 종료를 맞이할 때까지 컨테이너가 계속 실행되어야 함을 뜻합니다. 하지만 컨테이너 런타임은 SCM의 실행 상태를 고려하지 않고, 또한 SCM을 고려한다는 것은 플랫폼에 지나치게 종속적인 일입니다.

이 문제를 해결하기 위하여 Microsoft에서는 IIS용으로 ServiceMonitor 라는 오픈 소스 유틸리티를 공개하였습니다. 하지만 IIS만이 아니라 SCM에 의하여 제어할 수 있는 서비스에는 모두 적용할 수 있습니다.

https://github.com/Microsoft/IIS.ServiceMonitor

위의 리포지터리에서 제공하는 릴리스된 버전의 ServiceMonitor 유틸리티를 Docker로 이미지를 빌드하는 도중에 추가하여 ENTRYPOINT로 지정하면 손쉽게 활용할 수 있습니다.

실제 Dockerfile 예시 살펴보기

이번 글에서의 관심사는 순수하게 ServiceMonitor의 활용에 관한 것이므로 저는 Windows 운영 체제에 이미 포함되어있는 예제 TCP 서비스를 컨테이너화하는 것으로 시나리오를 잡았습니다.

예제 TCP 서비스를 활성화하기 위한 제반 사항을 포함하는 SKU는 Windows Server Core이므로, 해당 이미지를 기본 이미지로 사용하고, PowerShell 명령어를 자주 써야 하므로 기본 Shell을 PowerShell로 변경하기 위하여 Dockerfile의 첫 시작은 다음과 같이 만들었습니다.

FROM mcr.microsoft.com/windows/servercore:1809
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'Continue'; $verbosePreference='Continue';"]

앞에서 이야기한 ServiceMonitor를 미리 빌드한 버전을 다음과 같이 루트 디렉터리 (C:)에 추가합니다.

ADD https://dotnetbinaries.blob.core.windows.net/servicemonitor/2.0.1.3/ServiceMonitor.exe /

예제 TCP 서비스를 Windows 부가 기능에서 찾아 활성화하고, 서비스를 시작 상태로 변경하는 PowerShell 명령을 다음과 같이 추가합니다.

RUN Enable-WindowsOptionalFeature -Online -FeatureName SimpleTCP
RUN Start-Service -ServiceName SimpTcp

Echo (7/tcp), Discard (9/tcp), Daytime (13/tcp), QotD (17/tcp), chargen (19/tcp)가 SimpTcp 서비스로 인하여 한 번에 시작됩니다. 컨테이너 외부에서 이들 서비스에 접속할 수 있도록 설정합니다.

# Echo
EXPOSE 7

# Discard
EXPOSE 9

# Daytime
EXPOSE 13

# Quote of the Day (QOTD)
EXPOSE 17

# Character Generator (chargen)
EXPOSE 19

그리고 SimpTcp 서비스가 실행되는 동안 컨테이너가 종료되지 않도록 ServiceMonitor가 SimpTcp 서비스를 바라보도록 ENTRYPOINT 설정을 다음과 같이 추가합니다.

ENTRYPOINT C:\ServiceMonitor SimpTcp

지금까지 만든 Dockerfile은 다음과 같습니다.

FROM mcr.microsoft.com/windows/servercore:1809
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'Continue'; $verbosePreference='Continue';"]

ADD https://dotnetbinaries.blob.core.windows.net/servicemonitor/2.0.1.3/ServiceMonitor.exe /

RUN Enable-WindowsOptionalFeature -Online -FeatureName SimpleTCP
RUN Start-Service -ServiceName SimpTcp

# Echo
EXPOSE 7

# Discard
EXPOSE 9

# Daytime
EXPOSE 13

# Quote of the Day (QOTD)
EXPOSE 17

# Character Generator (chargen)
EXPOSE 19

ENTRYPOINT C:\ServiceMonitor SimpTcp

컨테이너 이미지 빌드 및 테스트

컨테이너 이미지를 빌드하기 위하여 Dockerfile이 있는 디렉터리에서 다음과 같이 명령어를 실행합니다.

docker build -t simpletcp:latest .

그 후, 다음과 같이 컨테이너를 실행합니다.

docker run --rm -d -p 7:7 -p 9:9 -p 13:13 -p 17:17 -p 19:19 --name=simpletcp simpletcp:latest .

컨테이너가 정상적으로 계속 실행되는지 확인한 후에, 13, 17, 19번 포트 중 하나를 골라서 telnet 명령으로 접속해봅니다.

telnet localhost 13
telnet localhost 17
telnet localhost 19

각각에 해당되는 서비스가 정상 작동하는 모습을 볼 수 있습니다.

참고로 telnet 유틸리티는 기본으로 설치되지 않으므로 호스트 PC에서 관리자 권한으로 아래 명령을 실행하여 해당 기능을 설치해야 합니다.

# Windows 10의 경우
Enable-WindowsOptionalFeature -Online -FeatureName 'TelnetClient'

# Windows Server의 경우
Install-WindowsFeature Telnet-Client

로그 수집을 위한 해결책

일단 컨테이너화된 Windows 서비스가 잘 작동하는 것을 볼 수 있었습니다. 하지만 stdout으로 로그를 내보내는 기능이 없어 동작 상태를 즉시 파악할 수 없는 문제가 있습니다.

이 문제를 해결하기 위하여 최근에 Microsoft에서는 LogMonitor라는 보조 유틸리티를 추가로 릴리스하였습니다.

https://github.com/microsoft/windows-container-tools/tree/master/LogMonitor

LogMonitor를 사용하는 방법은 단순합니다. 실행하려는 NT 서비스나 Win32 프로세스를 실행하기 앞서 LogMonitor.exe 유틸리티가 대신 실행할 수 있도록 Docker의 SHELL 지시자로 연결하거나, LogMonitor.exe의 명령줄 인자로 실행할 프로그램을 지정하는 것입니다.

적용 및 테스트

앞서 만든 Dockerfile에 다음의 내용을 추가하면 ServiceMonitor.exe와 PowerShell 인터프리터를 LogMonitor.exe가 대신 실행해주면서 로그 파일, ETW 로그, 이벤트 로그 수집에 필요한 전처리 과정을 가로채어 stdout으로 내보낼 수 있게 준비합니다.

# Add LogMonitor and wrap command with LogMonitor.exe
ADD https://github.com/microsoft/windows-container-tools/releases/download/v1.0/LogMonitor.exe /LogMonitor/
ADD LogMonitorConfig.json /LogMonitor/
WORKDIR /LogMonitor
SHELL ["C:\\LogMonitor\\LogMonitor.exe", "powershell.exe"]

ENTRYPOINT Start-Service SimpTcp; C:\ServiceMonitor.exe SimpTcp

LogMonitorConfig.json 파일은 LogMonitor.exe를 위한 설정 파일이며, Simple TCP 서비스의 로그를 수집하기 위해 이벤트 로그와 특정 ETW 로그를 선택적으로 수집하도록 구성했습니다.

{
  "LogConfig": {
    "sources": [
      {
        "type": "EventLog",
        "startAtOldestRecord": true,
        "eventFormatMultiLine": false,
        "channels": [
          {
            "name": "system",
            "level": "Information"
          },
          {
            "name": "application",
            "level": "Error"
          }
        ]
      },
      {
        "type": "ETW",
        "eventFormatMultiLine": false,
        "providers": [
          {
            "providerName": "Microsoft-Windows-Services",
            "providerGuid": "0063715B-EEDA-4007-9429-AD526F62696E",
            "level": "Information"
          }
        ]
      }
    ]
  }
}

위와 같이 수정한 컨테이너 이미지를 시작한 후 로그 출력을 보면 한 줄씩 XML 형태로 상세 로그가 나타나는 것을 볼 수 있습니다.

...
<Source>EtwEvent</Source><Time>2020-01-10T12:10:26.000Z</Time><Provider idGuid="{0063715B-EEDA-4007-9429-AD526F62696E}"/><DecodingSource>DecodingSourceXMLFile</DecodingSource><Execution ProcessID="624" ThreadID="3704" /><Level>Information</Level><Keyword>0x8000000000010000</Keyword><EventID Qualifiers="105">105</EventID><EventData><ExecutionPhase>1</ExecutionPhase><CurrentState>2</CurrentState><StartType>2</StartType><PID>0</PID><ServiceName>simptcp</ServiceName><ImageName></ImageName></EventData>
...
<Source>EventLog</Source><Time>2020-01-10T11:50:31.000Z</Time><LogEntry><Channel>System</Channel><Level>Information</Level><EventId>7036</EventId><Message>The simptcp service entered the running state.</Message></LogEntry>
...

만약 다른 종류의 이벤트 로그를 추가 수집하려면 https://gist.github.com/guitarrapc/35a94b908bad677a7310 에 나와있는 ETW 공급자 목록에 해당하는 이름과 GUID 값을 LogMonitorConfig.json 파일에 추가할 수 있습니다.

만약 서비스가 로그 파일 기록을 지원한다면, 로그 파일의 내용을 직접 읽어 출력하도록 만드는 것도 가능하며, LogMonitorConfig.json 파일의 레퍼런스는 https://github.com/microsoft/windows-container-tools/blob/master/LogMonitor/src/LogMonitor/sample-config-files/IIS/LogMonitorConfig.json 에서 볼 수 있습니다.

정리

지금 소개한 두 가지 유틸리티를 조합하여 NT 서비스가 만들어낼 수 있는 다양한 기록 (이벤트 로그, 로그 파일, ETW 로그)를 표준 출력으로 내보내면서도, Docker 컨테이너가 서비스 제어 관리자의 흐름과 컨테이너의 생명 주기에 맞출 수 있도록 만들 수 있습니다.

그럼에도 불구하고, 이 방법은 IIS, ASP, ASP.NET 처럼 NT 서비스를 직접 콘솔 애플리케이션으로 변환하기 어려운 상황에서만 고려해야 하는 대안입니다.

만약 애플리케이션을 컨테이너화 하는 것에 투자할 시간적 여유가 충분하다면 콘솔 애플리케이션 형태로 작동하는 독립적인 서버 애플리케이션으로 리팩토링하는 것이 더 효과적인 방법일 수 있습니다.

다음 글

다음 글 보기: 베이스 이미지, 격리 방식에 대한 이해

데브시스터즈는 최고의 인재를 찾고 있습니다.

자세한 내용은 채용 사이트를 확인해주세요!

© 2024 Devsisters Corp. All Rights Reserved.