Migrating a Native Windows Service to Windows Azure
The overall proof of concept I’m trying out is how you could migrate a Native Windows Service to Azure and communicate with this service from another role instance in the cloud. The goal is to reuse as much code as possible while still allowing the Azure Runtime to "monitor" my service. I borrowed most of the implementation of my service from a Windows SDK sample named "Service". The service itself is written in C and just creates a secured named pipe that receives data, processes it (reverses it), and sends it back to the client.
Let’s look at some of the dependencies this service has just from what we know by the above description:
- Security: The named pipe has an ACL on it so whatever machine is writing/reading the pipe will need to be authenticated.
- Distributing binaries: I’ll need to deploy the service executable and the VC++ Runtime library (which is not installed by default on Azure nodes).
- Since the bulk of my code is running in a Windows Service and not directly in the worker role how will Azure know if something went wrong?
- How will the two roles communicate? Named pipes? How is that configured?
To get started with this project I created a solution with the following projects:
- Cloud Project named "ServiceDemo"
- Worker Role named "Service Watcher"
- Web Role named "ASPXServerClient"
- Windows Service named "Service"
So let’s tackle the dependencies one at a time.
Security Dependency
Here is the security descriptor set by my service during the call to CreateNamedPipe(...):
TCHAR * szSD = TEXT("D:") // Discretionary ACL
TEXT("(D;OICI;GA;;;BG)") // Deny access to built-in guests
TEXT("(D;OICI;GA;;;AN)") // Deny access to anonymous logon
TEXT("(A;OICI;GRGWGX;;;AU)") // Allow read/write/execute to authenticated users
TEXT("(A;OICI;GA;;;BA)"); // Allow full control to administrators
If I want to authenticate from my web role to my Windows service how can I do that? There is no Active Directory in this scenario. A relatively simple solution to this problem is to create duplicate local accounts on both the web and worker role instances. Then impersonate the local account from the ASP.NET site to access the service.
This can be accomplished by using an elevated startup task running the following commands:
Startup.cmd
net user serviceUser [somePassword] /add
exit /b 0
The first line creates a local user named serviceUser with whatever password you set. The second line returns from the batch file. This particular user does not have to be part of the administrators groups due to my services service descriptor allowing the named pipe to be written to by users in the Users group. My ASP.NET code will have to authenticate as this user when it writes to the named pipe (I’ll show that a bit later).
This startup task needs to be run for the web role AND the worker role so you will need to add the following to both roles in the ServiceDefinition.csdef. Note: my startup tasks are in a folder I created in each project called “Startup” and put startup.cmd within.
<Task commandLine="Startup\Startup.cmd" executionContext="elevated" taskType="simple" />
Note the elevated requirement due to creating users.
Distributing Binaries
This one is pretty straightforward. To have additional files deploy with your worker role add them to your project (in a folder). Once added select each file in Visual Studio and change Build Action to Content and Copy to Output Directory to Copy if Newer.
For instance you will need to download the VC runtime that your service is compiled with. In my case it is the VC++ 2010 for x64 (vcredist_x64.exe). Once downloaded add the file to your Startup folder.
These steps will ensure the VC++ runtime is copied to the Azure servers when the project is published.
The next step is to add another step to the startup task for your worker role only:
"%~dp0vcredist_x64.exe" /q /norestart
This command will start the install quietly (and tell it not to reboot). The %~dp0 is a batch file constant that essentially means the current directory that the batch file is running in.
Service Installation
I also need to install my service silently. In my case the service supports a command line argument –install that performs this properly. Your service will need to perform similarly for it to install in a worker role.
The complete startup.cmd for the worker role is here:
net user serviceUser [somePassword] /add
"%~dp0vcredist_x64.exe" /q /norestart
"%~dp0Service.exe" -install
exit /b 0
Monitoring the Windows Service
As the name of my worker role implies (ServiceWatcher) I want to monitor my Windows Service so the Azure runtime is aware of any problems and can reimage/start the role as needed.
I have a simple class (probably not real robust either!) that checks if my service is running and if not tries to start it. If it is not started after these steps it returns false otherwise true.
class ServiceMonitor
{
public static bool CheckAndStartService(String ServiceName, int StartTimeOutSeconds)
{
ServiceController mySC = new ServiceController(ServiceName);
if (IsServiceStatusOK(mySC.Status) == false)
{
System.Diagnostics.Trace.WriteLine("Starting Service.....");
try
{
mySC.Start();
}
catch(Exception) {}
mySC.WaitForStatus(ServiceControllerStatus.Running, new TimeSpan(0, 0, StartTimeOutSeconds));
if (IsServiceStatusOK(mySC.Status))
return true;
else
return false;
}
return true;
}
static private bool IsServiceStatusOK(ServiceControllerStatus Status)
{
if (Status != ServiceControllerStatus.Running && Status != ServiceControllerStatus.StartPending && Status != ServiceControllerStatus.ContinuePending)
{
return false;
}
return true;
}
}
My worker role’s Run() method is as follows:
public override void Run()
{
String ServiceName = "SimpleService";
int ServiceFailCount = 0;
const int MAXFAILS = 5;
while (true)
{
// if the service failed more than 5 times return
if (ServiceFailCount >= MAXFAILS)
{
Trace.TraceError(String.Format(String.Format("{0} has failed to start {1} times", ServiceName, ServiceFailCount)));
return;
}
try
{
// Check if the service is running
if (ServiceMonitor.CheckAndStartService("SimpleService", 10) == true)
{
Trace.TraceInformation("Service is Running");
}
else
{
// if not increment the fail count
ServiceFailCount++;
Trace.TraceError("Service is no longer Running");
}
}
catch (Exception e)
{
Trace.TraceError("Exception occurred: " + e.Message);
}
Thread.Sleep(10000);
}
}
The worker role's Run method stays and checks the status of the Windows service every 10 seconds. If the service fails to start five times (MAXFAILS) then it returns which tells Azure that something went wrong.
Using the ServiceController class does require elevation. So in ServiceDefinition.csdef add the following line for the ServiceWatcher role instance:
<Runtime executionContext="elevated" />
How do the roles communicate?
The port for named pipes is 445. To allow communication between the web and worker roles I will need an internal endpoint opened up on the ServiceWatcher worker role.
Impersonating the Local User
My ASP.NET code will need to use impersonation to connect to the service to authenticate:
I’ve created a helper class for this:
public class LogonHelpers
{
// Declare signatures for Win32 LogonUser and CloseHandle APIs
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(
string principal,
string authority,
string password,
LogonSessionType logonType,
LogonProvider logonProvider,
out IntPtr token);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr handle);
public enum LogonSessionType : uint
{
Interactive = 2,
Network,
Batch,
Service,
NetworkCleartext = 8,
NewCredentials
}
public enum LogonProvider : uint
{
Default = 0, // default for platform (use this!)
WinNT35, // sends smoke signals to authority
WinNT40, // uses NTLM
WinNT50 // negotiates Kerb or NTLM
}
}
Load Balancing the Internal Endpoint
One other helper function I want to mention is the following:
// Internal endpoints are not load balanced so do it ourself
private String GetRandomServiceIP()
{
var endpoints = RoleEnvironment.Roles["ServiceWatcher"].Instances.Select(i => i.InstanceEndpoints["NamedPipes"]).ToArray();
Random r = new Random(DateTime.Now.Millisecond);
int ipIndex = r.Next(endpoints.Count());
return endpoints[ipIndex].IPEndpoint.Address.ToString();
}
This method is needed because internal endpoints are not load balanced by Azure. So if you configured multiple instances of your service to run you will want to load balance these calls.
Writing to and Reading from the Named Pipe
Now for the magic to actually write data to and read data from the service’s named pipe:
protected void cmdProcessText_Click(object sender, EventArgs e)
{
IntPtr token = IntPtr.Zero;
WindowsImpersonationContext impersonatedUser = null;
String ServiceUserName = "serviceUser";
String ServicePassword = "somePassword";
String ServiceInstanceIP = String.Empty;
try
{
ServiceInstanceIP = GetRandomServiceIP();
bool impResult = LogonHelpers.LogonUser(ServiceUserName, ".", ServicePassword, LogonHelpers.LogonSessionType.Interactive, LogonHelpers.LogonProvider.Default, out token);
if (impResult == false)
{
lblProcessedText.Text = "LogonUser failed";
return;
}
WindowsIdentity id = new WindowsIdentity(token);
// Begin impersonation
impersonatedUser = id.Impersonate();
// Resource access here uses the impersonated identity
NamedPipeClientStream pipe = new NamedPipeClientStream(ServiceInstanceIP, "simple", PipeDirection.InOut);
// connect to the pipe - give 10 seconds before timing out
pipe.Connect(10000);
String tmpInput = txtTextToProcess.Text;
// null terminate for our native C code
tmpInput += '\0';
// Write to the pipe service
StreamWriter sw = new StreamWriter(pipe);
sw.AutoFlush = true;
sw.Write(tmpInput);
// Wait for the result after processing
StreamReader sr = new StreamReader(pipe);
String result = sr.ReadToEnd();
lblProcessedText.Text = "Processed Text: " + result + " by instance: " + ServiceInstanceIP;
}
catch (Exception exc)
{
lblProcessedText.Text = "Exception occurred: " + exc.Message;
}
finally
{
if (impersonatedUser != null)
impersonatedUser.Undo();
if (token != null)
LogonHelpers.CloseHandle(token);
}
}
Simple Enough!
Comments
Anonymous
April 21, 2011
Hello when I run the startup in worker role for the following command: "%~dp0vcredist_x64.exe" /q /norestart The vcredist_x64.exe will pup-on a dialog (about "license agreement"), that need to click "next" or "install" to confirm the installation, then the installation will wait for there. So the worker role shows as "Waiting for role to start...", How do you solve this problem? Thank you MikeAnonymous
April 21, 2011
Interesting.. I didn't realize that the VC++ redist files that are downloaded require the EULA popup. I did found another blog that addresses it though: Take a look here and see if it helps: blogs.msdn.com/.../update-regarding-silent-install-of-the-vc-8-0-runtime-vcredist-packages.aspxAnonymous
February 21, 2014
Hi, can you share the source code? Thanks!