Tag Archives: ServicedComponent

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:SoftwareClassesAppID{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

[sourcecode language="csharp"]
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 + "WindowsMicrosoft.NETFramework64v4.0.30319regsvcs.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:SoftwareClassesAppID$AppID)
{
remove-item HKLM:SoftwareClassesAppID$AppID -recurse
}

#make the AppID key for the app
new-item -path HKLM:SoftwareClassesAppID$AppID
#Configure the endpoint port for the app
new-itemproperty -path HKLM:SoftwareClassesAppID$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):

[sourcecode language="text"]
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.

[sourcecode language="xml"]
<Startup>
<Task commandLine="StartupStartup.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:

[sourcecode language="text"]
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:

[sourcecode language="xml"]
<Startup>
<Task commandLine="StartupStartup.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.

[sourcecode language="csharp"]
// 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.

[sourcecode language="csharp"]
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

[sourcecode language="csharp"]
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.