요즘 Windows Azure에 많은 시간을 할애하고 있습니다. NeoDEEX 4.x를 Azure-aware 프레임워크로 만들기 위한 작업이지요. 이런 저런 이유로 Azure 상에서 작동하는 다양한 예제를 만들고 있는데요, 그러다 Worker Role에 WCF 서비스를 호스팅 하는 예제를 작성하게 되었습니다. 아주 간단한 서비스였음에도 불구하고 하루 동안 삽질하다가 다음날에서야 겨우 예제가 돌아가게 할 수 있었습니다. 저와 비슷한 삽질을 많은 분들이 할 수 있으리라 생각되어 키보드를 두드리게 되었네요. 이 글은 독자 여러분이 WCF Service에 대한 기본(?)적인 지식을 가지고 있으며 Window Azure에 대해서도 기본 지식을 가지고 있다고 가정할 것입니다. ServiceHost, BasicHttpBinding, Worker Role 등에 대해 주저리 주저리 이야기 하지 않겠다는 말입죠.

What's the Problem?

필자가 하고자 했던 일은 졸라 간단한 것이었다. 단순히 WCF 서비스를 Worker Role에 호스팅 하고 Web Role 혹은 클라우드 바깥에서 이 웹 서비스를 호출하는 것이 전부다. WCF 책을 쓴(그것도 책 이름에 '바이블'이 들어간!) 필자에게 이 정도 작업은 Azure에 대해 숏도 모른다는 점을 고려해도 1-2시간 이면 끝나야 하는 작업이어야 했다. C8,하지만 반나절을 끙끙대도 WCF 서비스는 Azure 상에서 작동하지 않았다. 그나마 위안으로 삼을 만한 건 로컬 컴퓨터에서는 겨우 서비스 호출만 할 수 있었다는 정도? (WSDL을 읽어 올 수 없었다.) 아놔!

필자가 반나절 동안 겪은 다양한 현상들은 다음과 같다.

  • WCF 서비스를 Azure에 배포 한 후 Worker Role 시작 시(OnStartup) 오류(권한 없음) 발생
  • 브라우저에서 서비스 Helper 웹 페이지 URL을 입력하면 405(Bad Request) 오류가 발생함.
  • 서비스 호출 시 Endpoint가 존재하지 않는다는 EndpointNotFoundException 예외 발생.
  • 서비스가 메타데이터(WSDL)을 제공하도록 구성했음에도 불구하고 WSDL을 받아 올 수 없음.


WCF 서비스 Helper 웹 페이지 (만지면 커져요)

Windows Azure 클라우드 환경에서 WCF 서비스를 구동하고, 이 서비스에 대한 WSDL을 제공하며, 위과 같은 서비스 구동을 확인할 수 있는 웹 페이지를 표시할 수 있는 것이 왜 그렇게 어려웠을까?

Self-Hosting WCF Service in Worker Role

Windows Azure Cloud Service의 Worker Role은 IIS를 사용하지 않는다. 대신 WaWorkerHost.exe라는 독립 호스트에 의해 worker role의 코드가 수행된다. 따라서 WCF 서비스를 Worker Role에 호스팅 하기 위해서는 독립 호스트에서 WCF 서비스를 구성하는 것처럼 ServiceHost 객체를 구성하고 서비스 종점(ServiceEndpoint)을 추가하는 등의 작업을 수행해 주어야 한다. 일반적으로 WCF 서비스를 Worker Role에서 호스팅 하는 코드는 다음과 같다.

   1: public override bool OnStart()
   2: {
   3:     ServicePointManager.DefaultConnectionLimit = 12;
   4:  
   5:     var workerEndpoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["ServiceEndpoint"];
   6:     var baseAddress = String.Format("http://{0}/HelloAzureService.svc", workerEndpoint.IPEndpoint);
   7:     var binding = new BasicHttpBinding();
   8:     _serviceHost = new ServiceHost(typeof(HelloAzureService), new Uri(baseAddress));
   9:     _serviceHost.AddServiceEndpoint(typeof(IHelloAzureService), binding, "");
  10:     _serviceHost.Open();
  11:  
  12:     return base.OnStart();
  13: }

위와 같은 코드 대신 app.config를 사용해도 되지만 서비스의 주소 및 포트를 RoleEnvironment 클래스로부터 읽어와야 하기 때문에 코드를 통한 서비스 구성이 좀더 선호된다. 어찌되었건 위와 같은 구성으로 서비스를 호스트를 작성하고 개발 환경에서 작동해 보면 서비스 메타데이터를 읽어오는 것(구성을 안 했으니까!) 외에는 서비스 호출에는 문제가 없다. 따라서 Woker Role을 Azure에 배포해서 테스트 하려고 하면 여지 없이 10번째 라인에서 AddressAccessDeniedException 예외를 뚜드려 맞게 된다.

HTTP.SYS와 URL 예약

WCF의 HTTP 트랜스포트 기반 바인딩들은 HTTP.SYS를 이용하여 HTTP 요청들을 수신하게 된다. 그런데 HTTP.SYS를 통해 HTTP 요청을 수신하기 위해서는 URL 예약(reservation)을 미리 수행하거나 관리자 권한이 있어야만 한다. 기본적으로 WaWokerHost.exe가 관리자 권한으로 구동되지 않기 때문에 Access Denied 예외가 발생하는 것이다. Worker Role이 배포될 때 기본적으로 다음과 같이 URL 예약을 수행해 준다.

netsh http add urlacl http://10.62.58.151:8080/ user=… listen=yes delegate=no

이 URL 예약은 HTTP 요청이 10.62.58.151라는 IP를 통해 요청될 때에만 HTTP.SYS가 요청을 라우팅하여 서비스가 호출될 수 있도록 해준다. 한편 WCF 서비스가 HTTP 트랜스포트를 사용하면 다음과 같이 임의의 IP, 호스트이름을 사용해도 되도록 다음 URL을 사용하여 HTTP 리스닝 등록(registration)을 수행한다.

http://+:8080/HelloAureService.svc

위와 같은 등록 명령이 성공되려면, 이 URL 접두어(+기호 및 8080 포트)가 미리 예약되어 있거나 관리자 권한이 있어야 한다. 하지만 Worker Role이 예약해 놓은 URL 접두어는 특정 IP만을 허용하도록 되어 있을 뿐이고 Worker Role 프로세스는 관리자 권한이 없다. 이 때문에 다음과 같은 예외가 발생하는 것이다.

HTTP could not register URL http://+:8080/HelloAzureService.svc/. Your process does not have access rights to this namespace (see http://go.microsoft.com/fwlink/?LinkId=70353 for details).

(예외를 확인하는 방법은 IntelliTrace를 사용하거나 Remote Desktop으로 Worker Role에 접속하여 윈도우 이벤트 로그를 살펴보면 된다)

문제 해결 방법

앞서도 이야기 하였지만 WCF 서비스를 IIS가 아닌 독립 호스트에서 호스팅 하기 위해서는 관리자 권한으로 호스트 프로세스를 구동하던가 아니면 미리 적절한 URL에 대해 예약을 수행해야 한다.

Azure 내부에서만 사용가능 하도록 수정

WCF 서비스가 임의의 IP, 호스트 이름이 아닌 특정 IP를 통해 서비스를 리스닝 하도록 구성이 가능하다. HTTP 트랜스포트를 사용하는 바인딩은 HostNameComparisonMode 속성을 제공하는데 이 속성에 HostNameComparisonMode.Exact 값을 설정하면 된다.

var binding = new BasicHttpBinding();
binding.HostNameComparisonMode = HostNameComparisonMode.Exact;
_serviceHost = new ServiceHost(typeof(HelloAzureService), new Uri(baseAddress));
_serviceHost.AddServiceEndpoint(typeof(IHelloAzureService), binding, "");

 

이제 WCF 서비스는 다음과 같이 로컬 컴퓨터(VM;Virtual Machine)의 IP를 지정하여 URL에 대해 등록을 수행할 것이다.

http://10.62.58.151:8080/HelloAzureService.svc

WCF의 이러한 행동은 Worker Role의 배포가 이루어질 때 수행되는 URL 예약과 호환이 되기 때문에 Access Denied 예외 없이 서비스 구동이 가능하다.

하지만 이 방법은 Worker Role 인스턴스, 즉 VM이 구성될 때 지정되는 전용 IP(private IP)를 사용해야만 WCF 서비스에 접근이 가능하게 된다. Azure 클라우드 환경의 전용 IP는 클라우드 외부에 알려지지 않으며 새로운 배포가 발생하거나 장애에 의해 VM이 새로이 구성되는 경우 전용 IP가 바뀔 수도 있다. 다시 말해서 이 방법으로는 클라우드 외부에서 WCF 서비스에 접근할 방법이 없다는 것이다.

클라우드 외부에서 Windows Azure Cloud Service의 Worker Role에 접근하기 위해서는 클라우드 서비스가 외부에 공개한 IP 혹은 호스트 이름(이 예제에서는 helloazurecloudservice.cloudapp.net)으로만 접근이 가능하지만 WCF 서비스가 인스턴스의 전용 IP만을 리스닝 하기 때문에 발생하는 문제로 보면 되겠다.

관리자 권한으로 수행시키기

먼저 관리자 권한으로 WaWorkerHost.exe 프로세스를 구동시키기 위해서는 서비스 정의 파일(.csdef)에서 Woker Role 정의에 다음과 같이 <Runtime> 요소를 추가하고 executionContext 속성 값을 elevated로 지정해 주면 된다.

<WorkerRole name="HelloAzure.WorkerRole" vmsize="Small">
  <!-- 다른 설정 부분 생략 -->
  <Runtime executionContext="elevated"/>
</WorkerRole>

 

이렇게 함으로써 WaWorkerHost.exe 프로세스는 관리자 권한으로 구동되게 되며, URL 예약과 무관하게 모든 IP, 호스트 이름을 사용한 서비스 호출을 리스닝 할 수 있게 된다. 다시 말해 클라우드 외부에서도 서비스 호출이 가능하다는 것이다.

Startup 작업을 통한 설정

관리자 권한으로 Worker Role 호스트 프로세스(WaWorkerHost.exe)를 구동하는 방법은 간편하지만 보안적으로 권장되지 않는다. 일반적으로 최대한 어플리케이션의 권한을 축소시키는 것이 보안적으로 안전하기 때문이다. 배포 단계에서 URL 예약을 적절히 수행한다면 호스트 프로세스가 관리자 권한으로 작동될 필요가 없을 것이다.

배포 단계에서 수행할 어플리케이션은 콘솔 어플리케이션(ReserveUrlPrefix.exe)으로써 코드는 다음과 같다.

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         if (RoleEnvironment.IsEmulated == false)
   6:         {
   7:             int port = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["ServiceEndpoint"].IPEndpoint.Port;
   8:             string userName = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
   9:             string command = String.Format("http add urlacl url=http://+:{0}/ user=everyone", port, userName);
  10:  
  11:             Process.Start("netsh.exe", command);
  12:         }
  13:     }
  14: }

위 코드는 다음과 같은 netsh.exe 명령을 수행하여 URL 예약을 수행할 것이다. 쉘 스크립트를 사용하여 다음 명령을 수행해도 되지만 서비스 정의 파일(.csdef)에 정의된 공개 종점(public endpoint)의 포트 번호를 읽어오기 위해 콘솔 어플리케이션을 작성한 것이다. 포트 번호가 고정되어 변화되지 않을 것이라면 .cmd 파일을 작성하여도 무방하다.

netsh http add urlacl url=http://+:8080/ user=everyone

작성한 콘솔 어플리케이션은 닷넷 프레임워크 3.5 버전을 사용하도록 컴파일 해야만 한다. 정확한 이유는 찾아보아야 하겠지만 4.0을 사용하도록 하면 수행 오류를 일으킨다.

이제 작성한 프로그램이 Worker Role의 배포 패키지에 포함되도록 해야 한다. 이 작업은 Worker Role 프로젝트에 exe를 링크(link)로써 추가(Add Existing Item)하고 속성 창에서 Copy to Output Directory 속성 값을 Copy Always로 지정해 주면 된다.

마지막으로 수행할 작업은 .csdef 파일을 수정하여 배포 직후에 작성한 콘솔 프로그램이 수행 되도록 해야만 한다. Worker Role 정의에 <Startup> 요소를 다음과 같이 추가해 주자. 물론, <Runtime> 태그는 이제 없어도 된다.

<WorkerRole name="HelloAzure.WorkerRole" vmsize="Small">
  <!-- 다른 설정 부분 생략 -->
  <Startup>
    <Task executionContext="elevated" commandLine="ReserveUrlPrefix.exe" 
          taskType="simple" />
  </Startup>
</WorkerRole>

 

혹시나 쉘 스크립트 파일(.cmd)을 사용하고자 하는 독자라면 Worker Role 프로젝트에 .cmd 파일을 만들어 넣고 위에서 보여준 netsh 명령어를 입력하면 된다. 당연히 .csdef 파일의 <Task> 요소의 commandLine 속성도 적절히 수정해 주어야 할 것이다. 이 쉘 스크립트 파일 역시 Copy to Output Directory 속성 값을 Copy Always로 지정해 주어야 함을 잊지 말자.

WSDL 관련 문제

WaWorkerHost.exe를 관리자 권한으로 구동시키거나 URL 예약 명령을 수행한 후 WCF 서비스에 대한 URL을 브라우저를 통해 살펴보면 다음과 같은 결과를 얻을 수 있다.

image
(만지면 커져요)

그런데 이 페이지는 WSDL을 얻을 수 있는 HTTP GET 주소가 존재하지 않음을 알려주고 있다. 애초에 우리(아니 필자)가 원했던 것은 HTTP GET을 이용해 손쉽게 서비스의 메타데이터(WSDL)를 얻는 것이었으니 이것을 지원하도록 서비스를 약간 수정해 보자.

HTTP GET 메타데이터 지원

HTTP GET을 통해 메타데이터를 제공하고자 한다면 ServiceMetadataBehaivor를 서비스 종점에 구성해 주어야 한다.

var behavior = new ServiceMetadataBehavior();
behavior.HttpGetEnabled = true;
_serviceHost.Description.Behaviors.Add(behavior);

 

코드는 간단하다. 위와 같이 코드를 수정하고 멋지게 Azure에 배포한 후에 브라우저에서 WCF 서비스를 다시 살펴보자.

image
(만지면 커져요)

서비스 주소 교정

이제 우리(?)의 클라우드 WCF 서비스는 원하는 대로 서비스에 대한 상세 정보(서비스 이름)와 더불어 WSDL을 얻기 위한 HTTP GET 주소 역시 제공되고 있음을 알 수 있을 것이다. 그런데 WSDL을 제공하는 HTTP GET URL의 IP가 조금 이상하다. 헬퍼 웹 페이지가 표시하는 WSDL에 대한 HTTP GET URL은 다음과 같다.

http://10.62.58.151:8080/HelloAzureService.svc?wsdl

위 주소가 제공하는 WSDL을 통해 서비스 참조를 수행하면 WCF 서비스의 주소가 http://10.62.58.151:8080/HelloAzureService.svc가 되어 버린다. 10.62.58.151은 Windows Azure 내부의 Worker Role 인스턴스, 즉 VM의 전용 IP로써 클라우드 바깥 세상에는 아무런 의미도 없는 IP이다. 따라서 이 WSDL을 기반으로 서비스 참조를 만들어 서비스를 호출하면 오류가 발생할 것이다. 물론 서비스 프록시를 만든 후에 코드를 통해 주소만을 수정할 수도 있겠지만 대략 응가하고 밑 안 닦은 것처럼 기분이 좆지 못하다.

기본적인 원인은 WCF 서비스가 WSDL에 사용하는 서비스의 주소로써 서비스가 리스닝 하는 주소를 사용하기 때문이다. 앞서부터 반복적으로 언급하지만 Worker Role은 전용 IP를 리스닝 하는 반면 서비스 제공은 공용 IP 혹은 공용 DNS 이름을 사용함을 잊지 말자.

Windows Azure 팀에서도 이런 문제를 인지하고 닷넷 프레임워크 3.5에 대해서는 핫픽스를 내놓았고 이 핫픽스는 닷넷 프레임워크 4.0에 포함되어 있다. WSDL 메타 데이터를 생성할 때 사용하는 주소를 리스닝 주소가 아닌 클라이언트의 HTTP 헤더에서 얻어오도록 하는 UseRequestHeadersForMetadataAddressBehavior를 사용하면 된다.

var behavior2 = new UseRequestHeadersForMetadataAddressBehavior();
behavior2.DefaultPortsByScheme.Add("http", publicEndpoint.IPEndpoint.Port);
_serviceHost.Description.Behaviors.Add(behavior2);

 

호스팅 코드를 수정하고 다시 한번 브라우저를 통해 서비스 헬퍼 페이지를 보면 다음과 같이 원하는 형태의 WSDL HTTP GET URL이 구성되어 있음을 알 수 있을 것이다.

image
(만지면 커져요)

정리

Windows Azure Cloud Service의 Worker Role에 WCF 서비스를 호스팅 하는 작업은 어렵지 않다. 이 WCF 서비스가 Worker Role에서만 호출된다면 NetTcpBinding과 Internal 종점을 사용하여 손쉽게 호출이 가능하다. 앞서 설명했던 URL 예약 따위의 작업은 해줄 필요가 없다는 것이다.

그러나 HTTP 기반의 바인딩(BasicHttpBinding, WSHttpBinding 등)을 사용하면서 Worker Role에 호스팅 된 WCF 서비스를 클라우드 바깥에서도 호출하고자 한다면 몇 가지 추가 설정(?)이 필요하게 된다. 첫째로는 WCF 서비스가 외부에서 호출될 수 있도록 URL 예약 작업을 수행해 주어야 한다. HTTP GET을 통해 WSDL을 획득하도록 하고자 한다면 UseRequestMetadataAddressBehavior 역시 구성해 주어야만 한다.

다소 산만한 내용이지만 Windows Azure 상에서는 관내(on-premise) 시스템을 구성할 때와 달리 고려할 사항들이 추가적으로 발생할 수 있다는 점을 기억해 두자. 나중에 시간이 나면 Windows Azure Cloud Service에서 public endpoint와 internal endpoint가 다를 수 밖에 없는 이유와 load balancer가 이들 public endpoint와 internal endpoint 상에서 어떤 역할을 수행하는지 별도의 포스트를 올리도록 하겠다. 또한, Web 혹은 Worker Role에서 Startup 커맨드로 쉘 스크립트를 구사하는 방법도 설명하기로 하겠다. 항상 그렇듯이 약속은 못하겠다. 쩝……


경고 : 이 글을 무단으로 복제/스크랩하여 타 게시판, 블로그에 게시하는 것은 허용하지 않습니다.