Develop an Adapter for a VM Role in Windows Azure
Updated: March 8, 2011
[The VM Role feature of Windows Azure is being retired on May 15th, 2013. After the retirement date, VM role deployments will be deleted. To move forward with your existing applications, you can use Windows Azure Virtual Machines. For more information about using Virtual Machines for your application, see Moving from VM Role to Windows Azure Virtual Machines.
You can write an adapter as a Windows service that starts automatically when the operating system starts and that uses the Microsoft.WindowsAzure.ServiceRuntime API to use runtime information from Windows Azure. You may need to write a Windows service if you need network address information for the current VM Role instance or other role instances running in your service, if you need to write to a local storage resource, or if you need to read service configuration settings at runtime or respond when they change.
This section shows how to build an adapter that demonstrates services that support a VM Role. The example that is used in this section mounts a Windows Azure drive when the operating system starts, and then configures Internet Information Services (IIS) to write HTTP log files to the drive. For details on getting started with Windows Azure drives, see the Windows Azure Drives white paper.
To create an adapter, you must complete the following tasks:
-
Create a storage container
-
Create the adapter project
-
Add the ability to track role instance configuration changes
-
Define what happens when the adapter starts
-
Define what happens when the adapter stops
-
Create the installers for the adapter
-
Create a setup project for the adapter
-
Add a post build event to correct assembly inconsistencies
-
Add configuration settings to the cloud service model
-
Install the adapter
The adapter uses blob storage to facilitate the storing of log data in the Windows Azure drive. You must have access to a storage account in Windows Azure, and you must create a container that is used by the adapter. You can use your favorite tool for creating a container in Windows Azure storage with any name that you choose. You will use the name of the container when you configure the cloud service model.
Visual Studio 2010 provides a template for creating a Windows service. You can use this template to create the adapter for your VM Role instances.
-
Open Visual Studio 2010, click File, click New, and then click Project.
-
In the Installed Templates pane under Visual C#, click Windows, and then in the center pane, click Windows Service.
-
Enter a name for the solution and the adapter project, and then click OK.
-
In Solution Explorer, right-click the solution, click Configuration Manager, ensure that Any CPU is selected for the project, and then click Close.
-
In Solution Explorer, rename the Service1.cs file to the name that you are using for the adapter. The examples in this section use the AdapterName placeholder, which represents the name that you choose.
-
Right-click the project, click Properties, on the Application page, ensure that .NET Framework 3.5 is selected for the Target Framework.
-
On the Publish page of the project properties, click Prerequisites, and then ensure that .NET Framework 4 (Client Profile) is not selected.
-
In Solution Explorer, add references for the following assemblies:
-
Microsoft.WindowsAzure.ServiceRuntime.dll
Note You must ensure that that the Copy Local property for this assembly is set to False. -
Microsoft.WindowsAzure.CloudDrive.dll
-
Microsoft.WindowsAzure.StorageClient.dll
-
Microsoft.Web.Administration.dll
-
Microsoft.WindowsAzure.ServiceRuntime.dll
-
In Solution Explorer, right-click AdapterName.cs, and then click View Code.
-
Add the following using statements:
using Microsoft.WindowsAzure.ServiceRuntime; using Microsoft.WindowsAzure.StorageClient; using Microsoft.WindowsAzure; using Microsoft.Web.Administration; using System.IO; using System.Threading;
-
In Solution Explorer, right-click AdapterName.cs, and then click View Designer.
-
In the Properties pane, set the following properties:
-
ServiceName - You can specify the name by which the adapter is identified to the system.
-
(Name) - You can specify the name that is used in code to represent the adapter object.
-
CanShutdown – You must specify the value for this property as True for the adapter to be notified when the system is shutting down.
-
ServiceName - You can specify the name by which the adapter is identified to the system.
The Program.cs file contains the following code:
static void Main()
{
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new ServiceName()
};
ServiceBase.Run(ServicesToRun);
}
You can define operations that are performed when the configuration of a role instance changes. For example, you could change the location of a drive where logging data is being stored by changing the configuration of the role instance. To do this, you use the RoleEnvironment events.
-
In Solution Explorer, right-click AdapterName.cs, and then click View Code.
-
Edit the constructor to define the configuration changing events. The following code example shows the event definitions added to the constructor:
public AdapterName() { InitializeComponent(); RoleEnvironment.Changed += RoleEnvironmentChanged; RoleEnvironment.StatusCheck += RoleEnvironmentStatusCheck; } -
Add the RoleEnvironmentChanged method that is called when the Changed event occurs. The following code example shows the RoleEnvironmentChanged method, which mounts a new Windows Azure drive and configures IIS to write log files to the new drive:
private CloudDrive currentDrive; private void RoleEnvironmentChanged(object sender, RoleEnvironmentChangedEventArgs e) { if(!e.Changes.OfType<RoleEnvironmentConfigurationSettingChange>().Any( c => c.ConfigurationSettingName == "AdapterName.BlobPath")) return; try { // perform a rolling drive change var oldDrive = this.currentDrive; var newDrive = MountDrive(); try { ConfigureWebServer(newDrive.LocalPath); } catch (Exception) { UnmountDrive(newDrive); throw; } this.currentDrive = newDrive; UnmountDrive(oldDrive); } catch (Exception ex) { this.EventLog.WriteEntry(ex.ToString(), EventLogEntryType.Error); throw; } }Where AdapterName is the name that you provided for the adapter project.
-
Add the methods that mount and unmount the Windows Azure drive, and add the method to configure the log file location for IIS. The following code example shows the MountDrive method:
private CloudDrive MountDrive() { // create or mount an instance-specific drive var credentials = GetStorageCredentials(); var driveUri = GetDriveUri(); var drive = new CloudDrive(driveUri, credentials); try { drive.Create(1024); } catch (Exception ex) { if (ex.Message != "ERROR_BLOB_ALREADY_EXISTS") throw; } // mount the drive string mountPoint = drive.Mount(1024, DriveMountOptions.FixFileSystemErrors | DriveMountOptions.Force); this.EventLog.WriteEntry(string.Format("{0} mounted at {1}", drive.Uri, mountPoint)); return drive; } private Uri GetDriveUri() { return new Uri(string.Format( RoleEnvironment.GetConfigurationSettingValue("AdapterName.BlobPath"), RoleEnvironment.CurrentRoleInstance.Id)); } private StorageCredentials GetStorageCredentials() { return new StorageCredentialsAccountAndKey( RoleEnvironment.GetConfigurationSettingValue("AdapterName.AccountName"), RoleEnvironment.GetConfigurationSettingValue("AdapterName.AccountKey")); }For more information about using the CloudDrive API, see CloudDrive. The AdapterName.BlobPath, AdapterName.AccountName, and AdapterName.AccountKey settings are defined in the cloud service definition file and are configured in the service configuration file. For more information about defining these settings, see Add configuration settings to the cloud service model.
The following code example shows the UnmountDrive method:
private void UnmountDrive(CloudDrive drive) { drive.Unmount(); this.EventLog.WriteEntry(string.Format("{0} unmounted", drive.Uri)); }The following code example shows the ConfigureWebServer method:
private void ConfigureWebServer(string drivePath) { using (var config = new ServerManager()) { var logdir = Path.Combine(drivePath, @"inetpub\logs\LogFiles"); config.SiteDefaults.LogFile.Directory = logdir; config.CommitChanges(); this.EventLog.WriteEntry(string.Format("IIS log location set to '{0}'", logdir)); } } -
Add the RoleEnvironmentStatusCheck method that is used to determine the state of the role instance. The statusCheckWaitHandle object is used as a signal to inform the load balancer that the role instance is busy or ready. The call to the Set method of the object is the signal that the configuration change is finished. The following code example shows the RoleEnvironmentStatusCheck method:
private readonly EventWaitHandle statusCheckWaitHandle = new ManualResetEvent(false); private volatile bool busy = true; private void RoleEnvironmentStatusCheck(object sender, RoleInstanceStatusCheckEventArgs e) { if (this.busy) { e.SetBusy(); } statusCheckWaitHandle.Set(); }
In this example, the adapter performs the following operations when it starts:
-
Initialize the drive cache
-
Mount the drive
-
Configure IIS to use the mounted drive
-
In Solution Explorer, right-click AdapterName.cs, and then click View Code. Locate the OnStart method that was automatically overridden when you created the project, and add the code that the checks the role instance to ensure it us running, performs the operations of the adapter on a separate thread, and then signals that the operations are complete by using the statusCheckWaitHandle object.
protected override void OnStart(string[] args) { /// Windows Azure waits for auto-start services to be fully started /// before sending any traffic. Service Control Manager (SCM) does /// not impose a time limit on startup; the service need only /// request additional time. if (!RoleEnvironment.IsAvailable) return; var startThread = new Thread(OnStartInternal); startThread.Start(); // wait until a status check has occurred, so that Windows Azure // knows we are working on something. WaitForHandle(statusCheckWaitHandle); }The following code example shows the OnStartInternal method where the Windows Azure drive is mounted and the IIS configuration is changed to use the new drive for writing log data:
private void OnStartInternal() { try { // initialize the drive cache string cachePath = RoleEnvironment.GetLocalResource("Data").RootPath; CloudDrive.InitializeCache(cachePath, 4096); this.EventLog.WriteEntry("initialization succeeded"); // mount the current drive this.currentDrive = MountDrive(); // configure IIS ConfigureWebServer(this.currentDrive.LocalPath); this.busy = false; } catch (Exception ex) { this.EventLog.WriteEntry(ex.ToString(), EventLogEntryType.Error); throw; } }The following code example shows the WaitForHandle method that waits until a status check occurs:
private const int ThreadPollTimeInMilliseconds = 1000; private void WaitForHandle(WaitHandle handle) { while (!handle.WaitOne(ThreadPollTimeInMilliseconds)) { this.RequestAdditionalTime(ThreadPollTimeInMilliseconds * 2); } }
In this example, the adapter reconfigures the log file location and unmounts the drive when it stops.
-
In Solution Explorer, right-click AdapterName.cs, and then click View Code. Locate the OnStop method that was automatically overridden when you created the project, and add the code that reconfigures the log file location and unmounts the drive.
protected override void OnStop() { OnStopInternal(); } -
The following code example shows the OnShutdown method:
protected override void OnShutdown() { /// Windows Azure stops sending traffic before shutting down. /// Note that some requests may still be executing. OnStopInternal(); } -
Add the OnStopInternal method where the operations are performed.
private void OnStopInternal() { try { ConfigureWebServer(@"%SystemDrive%"); if (this.currentDrive != null) { UnmountDrive(this.currentDrive); this.currentDrive = null; } } catch (Exception ex) { this.EventLog.WriteEntry(ex.ToString(), EventLogEntryType.Error); throw; } }
Some custom actions need to occur when installing a Windows service, which can be done by the Installer class. Visual Studio can create these installers specifically for a Windows service and add them to your project.
-
In Solution Explorer, right-click AdapterName.cs, and then select View Designer.
-
Click the background of the designer to select the adapter itself, rather than any of its contents.
-
With the designer in focus, right-click, and then click Add Installer.
By default, a component class containing two installers is added to your project. The component is named ProjectInstaller, and the installers it contains are the installer for your adapter and the installer for the associated process of the adapter.
-
In Design view for ProjectInstaller, click serviceInstaller1.
-
In the Properties window, set the ServiceName property to the name of your adapter.
-
Set the StartType property to Automatic.
-
In the designer, click serviceProcessInstaller1. Set the Account property to LocalService. This will cause the adapter to be installed and to run on a local service account.
-
Build the project.
A setup project installs the compiled project files and runs the installers needed to run the adapter. To create a complete setup project you must add the project output to the setup project, and then add a custom action to install the program.
-
In Solution Explorer, right-click to select your solution, point to Add, and then click New Project.
-
In the Installed Templates pane, expand Other Project Types, expand Setup and Deployment Projects, and then click Visual Studio Installer.
-
In the center pane, click Setup Project.
-
Enter a name for the setup project, and then click OK.
-
Expand Detected Dependencies, double-click Microsoft .NET Framework, and then in the Properties pane, ensure that the value for Version is .NET Framework 3.5.
-
For each of the following assemblies under Detected Dependencies, right-click the assembly, and then click Exclude:
-
Microsoft.Web.Administration.dll
-
Microsoft.WindowsAzure.ServiceRuntime.dll
-
msshrtmi.dll
-
mswacdmi.dll
-
Microsoft.Web.Administration.dll
Now add a custom action to install the program file for your adapter.
-
In Solution Explorer, right-click the setup project, point to View, and then click Custom Actions.
-
In the Custom Actions editor, right-click the Custom Actions node, and then select Add Custom Action.
-
Double-click the Application Folder in the list box to open it, and then select Add Ouput.
-
In the Add Project Output Group window, select Primary Output, select (Active) for the Configuration, and then click OK.
Because Visual Studio injects 32-bit assemblies into the .msi file, but Windows Azure requires 64-bit assemblies, a script must be run to correct the inconsistent assemblies. You can use a JavaScript file as a post build event to correct the assemblies.
-
Save the following code to a file named FixMSI.js in the root folder of the setup project:
// workaround for "BadImageFormatException" issue - see http://msdn.microsoft.com/en-us/library/kz0ke5xt.aspx var msiOpenDatabaseModeTransact = 1; var msiViewModifyInsert = 1 var msiViewModifyUpdate = 2 var msiViewModifyAssign = 3 var msiViewModifyReplace = 4 var msiViewModifyDelete = 6 var filespec = WScript.Arguments(0); var frameworkpath = WScript.Arguments(1); var installer = WScript.CreateObject("WindowsInstaller.Installer"); var database = installer.OpenDatabase(filespec, msiOpenDatabaseModeTransact); WScript.Echo("Updating file '" + filespec + "' to use a 64-bit custom action..."); Update64Bit(); database.Commit(); database = null; installer = null; function Update64Bit() { var sql; var view; var record; sql = "SELECT * FROM Binary WHERE `Name`='InstallUtil'"; view = database.OpenView(sql); view.Execute(); record = view.Fetch(); if (record != null) { var dataCol = 2; record.SetStream(dataCol, frameworkpath + "\\InstallUtilLib.dll"); view.Modify(msiViewModifyUpdate, record); } record = null; view.close(); view = null; } -
In Solution Explorer, click the setup project that you previously created, and then in the Properties pane, add the following command to the PostBuildEvent property:
cd $(ProjectDir) CScript //NoLogo FixMSI.js "$(BuiltOutputPath)" "%SystemRoot%\Microsoft.NET\Framework64\v2.0.50727"
-
In Solution Explorer, right-click the setup project, and then click Build.
For the adapter to communicate with Windows Azure, settings must be defined in the cloud service model.
-
Open the ServiceDefinition.csdef file for the VM Role.
-
Add the following settings to the ConfigurationSettings element:
<Setting name="AdapterName.BlobPath" /> <Setting name="AdapterName.AccountName" /> <Setting name="AdapterName.AccountKey" />
Where AdapterName is the name of the adapter project.
-
Add the following setting to the LocalResources element:
<LocalStorage name="Data" />
For more information about the elements that can be used in the service model, see VirtualMachineRole Schema.
-
Save the file.
-
Open the ServiceConfiguration.cscfg file for the VM Role.
-
Add the following configuration settings to the file:
<Setting name="AdapterName.BlobPath" value="http://StorageAccountName.blob.core.windows.net/ContainerName/{0}.vhd" /> <Setting name="AdapterName.AccountName" value="StorageAccountName" /> <Setting name="AdapterName.AccountKey" value="StorageAccountKey" />Where AdapterName is the name of the adapter project that you created. StorageAccountName is the name of your storage account. StorageAccountKey is the primary key of your storage account. ContainerName is the storage container that you previously created. For more information about the configuration of the service model, see Windows Azure Service Configuration Schema. For more information about defining and configuring settings, see Create and Deploy the VM Role Service Model.
-
Save the file.
Your adapter is now ready to be installed on the VHD that you are uploading to Windows Azure.
After the code is created and the settings are defined and configured in the service model, you can install the adapter and deploy the service model package. For a VM Role, the adapter is installed on the virtual machine after the Windows Azure Integration components are installed. For more information about when the adapter is installed, see Getting Started with Developing a Server Image for a VM Role.
For the adapter to function correctly, several operating system features must be enabled. On the image that you will upload to Windows Azure, run the following command:
DISM /Online /Enable-Feature /FeatureName:NetFx3 /FeatureName:IIS-WebServerRole /FeatureName:IIS-WebServer /FeatureName:IIS-CommonHttpFeatures /FeatureName:IIS-HttpErrors /FeatureName:IIS-ApplicationDevelopment /FeatureName:IIS-HealthAndDiagnostics /FeatureName:IIS-HttpLogging /FeatureName:IIS-RequestMonitor /FeatureName:IIS-Security /FeatureName:IIS-RequestFiltering /FeatureName:IIS-Performance /FeatureName:IIS-WebServerManagementTools /FeatureName:IIS-StaticContent /FeatureName:IIS-DefaultDocument /FeatureName:IIS-DirectoryBrowsing /FeatureName:IIS-HttpCompressionStatic /FeatureName:IIS-ManagementConsole
To install the adapter, browse to the folder that contains the .msi file, and then double-click the file. This file is located in the Debug folder of the setup project that you previously created.
See Also