COM+ and Serviced Components within Windows Azure

This post is the first in a series that discuss running COM+/Serviced Components within Windows Azure.

The primary reason for even bringing up this topic is COM+/MTS has been around for a VERY long time and we are well aware that there is a lot of existing code investments that companies do not want to just “throw away”. Having said that it is important to point out that Windows Azure is very different from the typical environment that most COM+ applications were originally designed for.

Data Access

The number one thing to consider when considering moving COM+ to the cloud is your data. Where will it live? Moving your data to the cloud with SQL Azure will likely be your first choice. Consider though that SQL Azure does not currently support distributed transactions (For Reference). Many people used COM+ specifically for handling distributed transactions so this will require some careful thought as to whether you should change your transaction model from [AutoComplete] or SetComplete/SetAbort to using SqlTransactions and some manual transaction handling for other resources. Other options are building hybrid models where the data access is handled on premises and accessed through Windows Azure Connect or Windows Azure AppFabric Service Bus.

Authentication

In Windows Azure by default there is no Active Directory. So if you are currently performing role based authorization in COM+ then you will want to look into using Windows Azure Connect to domain join the roles that are hosting your COM+ package.

Deployment

Deploying COM+ objects to the cloud can be tricky. At a minimum you will be deploying an msi file if you are deploying a native COM+ object. If you are deploying managed code you will likely be scripting out regsvcs.exe on your assembly. If you are deploying remotely to a worker role and calling from a web tier you will be deploying the application proxy AND the COM+ object. Not to mention there will be some firewall configuration needed. I’ve written a PowerShell script that makes this easier that I will post shortly. I’m 100% positive it will not cover all scenarios but it might be a good start.

Deploying a ServicedComponent in Windows Azure

Topology

In an enterprise environment it is pretty easy to code against a remote object because you know ahead of time what the server name or the IP address will be. In the cloud this is not the case. Instances come and go so changing your client code to dynamically discover where your objects live will be important.

Queued Components

MSMQ is not currently supported in Windows Azure due to the lack of durability of disks. So queued components is another scenario that currently is not suitable to run up in the cloud. Alternatives for queued components are Windows Azure Queues and AppFabric Service Bus queues. Depending on the level of functionality needed one should suit your purpose.

Deploying a COM+ ServicedComponent to Windows Azure

In this post I’m going to cover how you can deploy a serviced component to the cloud. I will point out the differences between deploying on a web role only and also distributing the serviced component back to a worker role and calling it from a web role.

Deployment Details

From an architecture perspective I am going to run the COM+ object on its own port (7001).
See the following article: http://support.microsoft.com/kb/217351 for details on how to do this.

The key is adding the registry key

REG KEY Details:

Value Name: Endpoints

Data Type: REG_MULTI_SZ

Value: ncacn_ip_tcp,0,7001

Beneath your COM+/DCOM objects AppID HKLM:\Software\Classes\AppID\{YourAppID}

Note this approach would likely work for regular DCOM servers as well. You would of course have to modify the PowerShell script below.

To make this work in the cloud though it needs to be scriptable. You don’t want to have to login and update registry key’s all the time!

I have written a PowerShell script called COMPlusInstaller.ps1 that helps with the deployment.

Description of Arguments

$AppPath: Path to the assembly for managed or exported msi for native.

$ApplicationName: The COM+ application name.

$EndPointPort The endpoint port (if deploying remotely).

$IsLibrary: Whether the component will be deployed as a library or server package, is

$IsServicedComponent: ServicedComponent or native

$Is32Bit: true = 32 bit false = 64 bit

$ServerUserName = User Name to run server package as

$ServerPassword = Password for User

Start COMPlusInstaller.ps1

param(
  $AppPath = $(throw "AppPath is required (.dll for serviced components and .MSI for native components)."), #required parameter
  $ApplicationName = $(throw "COM+ ApplicationName is required."), #required parameter
  $EndPointPort = "",
  $IsLibrary = $False,
  $IsServicedComponent = $True,
  $Is32Bit = $False,
  $ServerUserName = "",
  $ServerPassword = ""
)

# put quotes around the path to pass to regsvcs.exe
$AppPath = """$AppPath"""
$EndPointPort = "ncacn_ip_tcp,0," + $EndPointPort
$RegsvcsPath = $env:SystemDrive + "\Windows\Microsoft.NET\Framework64\v4.0.30319\regsvcs.exe"
$ArgumentsList = "";
$AppID = ""

if($IsServicedComponent -eq $True)
{
    if($Is32Bit -eq $True)
    {
       $FrameworkPath = $FrameworkPath.Replace("Framework64", "Framework")
    }

    $ArgumentsList = $AppPath + " /quiet"

    # use regsvcs to register the COM+ application
    start-process $RegsvcsPath -argumentlist $ArgumentsList -wait -NoNewWindow

}
else
{
    # kick off msi to install COM+ native package
    start-process $AppPath -wait -argumentlist "-qn"
}

# if it is a server app and a port is specified configure it in the registry and enable COM+ network access
if($IsLibrary -eq $False)
{
    # use COMAdminCatalog to configure other settings
    $comAdmin = New-Object -comobject COMAdmin.COMAdminCatalog
    $apps = $comAdmin.GetCollection("Applications")
    $apps.Populate();
    $app = $apps | Where-Object {$_.Name -eq $ApplicationName}

    if($IsLibrary -eq $False)
    {
        $app.Value("Identity") = $ServerUserName
        $app.Value("Password") = $ServerPassword
        $app.Value("Activation") = 1 # dedicate local server process
        # save this for configuring the endpoint port below
        $AppID = $app.Value("ID")
    }
    # other values that might be interesting
    #$app.Value("ApplicationDirectory") = $appRootDir
    #$app.Value("ConcurrentApps") = 1 # set to default
    #$app.Value("RecycleCallLimit") = 0 # set to default
    #$app.Value("ApplicationAccessChecksEnabled") = 0
    $apps.SaveChanges()

    if($EndPointPort -ne "ncacn_ip_tcp,0," -and $AppID -ne "")
    {
        if(Test-Path HKLM:\Software\Classes\AppID\$AppID)
        {
            remove-item HKLM:\Software\Classes\AppID\$AppID -recurse
        }

        #make the AppID key for the app
        new-item -path HKLM:\Software\Classes\AppID\$AppID
        #Configure the endpoint port for the app
        new-itemproperty -path HKLM:\Software\Classes\AppID\$AppID -name Endpoints -value $EndPointPort -propertyType MultiString

        #finally configure COM+ Network access on the server
        import-module servermanager
        add-windowsfeature AS-Ent-Services
   }
   else
   {
        write-host "Missing EndPointPort"
   }
}

End COMPlusInstaller.ps1

Deployment on the Worker Role

I’ve added the PowerShell script (COMPlusInstaller.ps1), the ServicedComponent binary (ServicedComponentTest.dll) and a batch file named startup.cmd to the worker role project in a folder named startup.

Startup.cmd consists of the following (note there should not be any line breaks):

powershell -ExecutionPolicy Unrestricted -File "%~dp0COMPlusInstaller.ps1" -AppPath "%~dp0ServicedComponentTest.dll" -ApplicationName "ServicedComponentTestApp" -ServerUserName ".\myuser" -ServerPassword "myuserspassword" -EndPointPort 7001

REM  open up the port for COM+ access on the worker role's firewall
netsh advfirewall firewall add rule name="COMPLUSRPC" dir=in action=allow protocol=TCP localport=135
netsh advfirewall firewall add rule name="COMPLUSENDPOINT1" dir=in action=allow protocol=TCP localport=7001

exit /b 0

For the User myuser – this is actually the user I’m specifying in remote desktop so it is created for me. If you want to create your own account you can use the net user command to script this. Remember, you will have to create the user/pass on both the web and the worker role.

You will also need the following internal endpoints added to the worker role.

Finally, you will need to add a startup task to your worker role in the servicedefinition.csdef file.

    <Startup>
      <Task commandLine="Startup\Startup.cmd" executionContext="elevated" taskType="simple" />
    </Startup>

Deployment on the Web Role

You will need to export the application proxy from COM+ and then deploy it with your web role via a startup task.

The startup.cmd for this startup task is much simpler:

REM Install the proxy on the web tier
%~dp0ServicedComponentTestProxy.msi /quiet
exit /b 0

Again, you will need to open up servicedefinition.csdef and add the startup task to the web role’s configuration:

    <Startup>
      <Task commandLine="Startup\Startup.cmd" executionContext="elevated" taskType="simple" />
    </Startup>

Update the Calling Code

Since the COM+ object is remote and could potentially be running on multiple worker roles we need a method of calling this object in a “load balanced” manner.

GetRandomServiceIP takes a role name and an endpoint name and returns a random instance’s IP address.

 // Internal endpoints are not load balanced so we do it ourself
private String GetRandomServiceIP(String roleName, String endPointName)
{
    var endpoints = Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment.Roles[roleName].Instances.Select(i =&gt; i.InstanceEndpoints[endPointName]).ToArray();
    Random r = new Random(DateTime.Now.Millisecond);
    int ipIndex = r.Next(endpoints.Count());
    return endpoints[ipIndex].IPEndpoint.Address.ToString();
}

Then to actually call into the object I am impersonating the user created earlier. Calling GetRandomServiceIP passing the worker role name and the endpoint name and I’m using .NET to create the object remotely using the random IP address.

if (LogonHelpers.ImpersonateUser("myuser", "myuserspassword", ".") == false)
{
    Response.Write("Impersonate user failed.");
    return;
}
String COMPlusIP = GetRandomServiceIP("COMPlusRole", "COMPlusEndpoint");
Type RemoteComPlusLib = Type.GetTypeFromProgID("ServicedComponentLib.ServicedComponentTest", COMPlusIP);
ServicedComponentLib.ServicedComponentTest component = (ServicedComponentLib.ServicedComponentTest)Activator.CreateInstance(RemoteComPlusLib);
lblResults.Text = component.GetComputerName();

LogonHelpers.cs contents

public class LogonHelpers
{
    public static bool ImpersonateUser(String User, String Password, String Domain)
    {
        IntPtr token = IntPtr.Zero;
        WindowsImpersonationContext impersonatedUser = null;

        bool impResult = LogonHelpers.LogonUser(User, Domain, Password, LogonHelpers.LogonSessionType.Interactive, LogonHelpers.LogonProvider.Default, out token);
        if (impResult == false)
        {
            return false;
        }

        WindowsIdentity id = new WindowsIdentity(token);
        // Begin impersonation
        impersonatedUser = id.Impersonate();
        return true;
    }

    // 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
    }
}

Running on the Web Role

Running this COM+ object on the web role would be much simpler. All that is needed is install the COM+ object on the web role instead of the worker role. Don’t specify an endpoint in the COMPlusInstaller.ps1 script. You do not need to open up firewall rules, configure the COM+ object to run on a specific port or do impersonation either. In other words a much cleaner and simpler architecture.

Summary

I’ve taken an existing COM+ ServicedComponent and installed it onto a worker role and called it from a web role. To accomplish this I deployed the serviced component using a custom PowerShell script and ran the PowerShell script using an elevated startup task. I also configured the COM+ object to run on port 7001. I also deployed the application proxy onto the web role and updated the client code to use impersonation and to dynamically discover the COM+ role’s IP.