Freigeben über


Using MSBuild to deploy to multiple Windows Azure environments

Update 6 December 2011: Windows Azure SDK 1.6 includes some significant changes to how credentials and publishing settings are managed, so I've written a new post Automated Build and Deployment with Windows Azure SDK 1.6 which shows how to leverage these in a build and deployment process.

Update 26 August 2011: The following post was written for Azure SDK 1.4 with the Visual Studio Tools 1.3. While most of the concepts remain valid for more recent versions, Joel Forman has put together a post that describes how he approached this problem using Azure SDK 1.4 and the Visual Studio Tools 1.4.  

Update 20 October 2011: Updated samples to show changes required for Azure SDK 1.5 and Azure Management cmdlets v2.

Introduction

If you’ve used the Windows Azure Tools for Visual Studio, you’ll know how easy it is to publish your applications to Windows Azure straight from Visual Studio with just a couple of clicks. But while this process is great for individual projects, most teams use automated build and deployment processes to get more predictability and control of what gets built and where it gets deployed. In most real-world scenarios, you’ll probably want to deploy to multiple Windows Azure managed services or subscriptions—for example for test, UAT and production—and use different configuration settings for each.

This post shows one way to extend Visual Studio 2010 and Team Foundation Server 2010 to automatically build and deploy your Windows Azure applications to multiple accounts and use configuration transforms to modify your Service Definition and Service Configuration files. The goal is to make it easy to set up as many TFS Build definitions as required, enabling you to build and deploy to a chosen environment on-demand or as part of a regular process such as a Daily Build. The following screenshot shows how easy it is to launch an automated build and deployment using this approach. A sample project containing all of the build targets and scripts used in this solution is available for download here.

clip_image001

Packaging and Deploying to Windows Azure

The following steps were followed to create a build definition that packages and deploys to Windows Azure:

  1. Modify the Windows Azure .ccproj project file to include a new default build target called AzureDeploy which calls the built-in CorePublish target to package the solution
  2. Create a PowerShell script to deploy the solution, and have the AzureDeploy target launch that script
  3. Create a build definition that passes MSBuild the configuration parameters for the target environment
  4. Configure the build server with the Windows Azure Management Certificate for the target environment.

Let’s look at these in more detail.

Modify the Windows Azure Project File

The Windows Azure Tools for Visual Studio includes MSBuild targets that know how to package your solution files for deployment to Windows Azure. However by default these targets will not fire as a part of an automated build. Our new AzureDeploy target is configured to depend on the existing CorePublish target, so we can be sure the solution will be fully packaged before we attempt to deploy.

In this solution, the custom build targets are specified within the Windows Azure Project file (.ccproj). To edit this in Visual Studio 2010, you must first right-click the project node in Solution Explorer and choose Unload Project. You can then right-click the project node and choose Edit <ProjectName>.ccproj to edit the project file in a text editor.

At the bottom of the project file, I added some properties and then defined the AzureDeploy target to run after the Build target with a dependency on the CorePublish target and call a PowerShell script on the build server. Note the use of the Condition attribute that specifies that the target should only execute if the AzureDeployEnvironment property is set. We’ll set this when we create our build definition later.

Azure SDK 1.5:

<PropertyGroup>

    <PackageName>$(AssemblyName).cspkg</PackageName>

    <ServiceConfigName>ServiceConfiguration.Cloud.cscfg</ServiceConfigName>

    <PackageForComputeEmulator>true</PackageForComputeEmulator>

  </PropertyGroup>

 

  <Target Name="AzureDeploy" AfterTargets="Build" DependsOnTargets="CorePublish" Condition="$(AzureDeployEnvironment)!=''">

    <Exec WorkingDirectory="$(MSBuildProjectDirectory)" Command=" $(windir)\system32\WindowsPowerShell\v1.0\powershell.exe -f \build\AzureDeploy.ps1 $(AzureSubscriptionID) $(AzureCertificateThumbprint) $(PublishDir) $(PackageName) $(ServiceConfigName) $(AzureHostedServiceName) $(AzureStorageAccountName)" />

  </Target>

 

Azure SDK 1.4:

<PropertyGroup>

    <PackageName>$(AssemblyName).cspkg</PackageName>

    <ServiceConfigName>ServiceConfiguration.cscfg</ServiceConfigName>

    <PackageLocation>$(OutDir)Publish</PackageLocation>

  </PropertyGroup>

 

  <Target Name="AzureDeploy" AfterTargets="Build" DependsOnTargets="CorePublish" Condition="$(AzureDeployEnvironment)!=''">

    <Exec WorkingDirectory="$(MSBuildProjectDirectory)" Command=" $(windir)\system32\WindowsPowerShell\v1.0\powershell.exe -f \build\AzureDeploy.ps1 $(AzureSubscriptionID) $(AzureCertificateThumbprint) $(PackageLocation) $(PackageName) $(ServiceConfigName) $(AzureHostedServiceName) $(AzureStorageAccountName)" />

  </Target>

Create the PowerShell Deployment Script

A few months ago, Scott Densmore posted on how he managed automated deployments to Windows Azure with TFS 2008. I used his deployment PowerShell script as the basis of mine. Like his, it requires that you install the Windows Azure Service Management cmdlets on your build server. My version of the script differs from his in that it takes all of the deployment settings (Hosted Service Name, Storage Account Name, Management Certificate Thumbprint, etc.) as parameters, and the script also returns an error code to ensure the build fails if the deployment could not succeed. Like Scott’s script, mine deploys to the Staging slot within the Windows Azure managed service to allow for build verification testing before the Virtual IP addresses are manually flipped to use the Production slot.

This is what my AzureDeploy.ps1 script looks like. This file should be saved onto your build server in the location specified in your build targets (I used \Build).

$error.clear()

$sub = $args[0]

$certThumbprint = $args[1].ToUpper()

$certPath = "cert:\LocalMachine\MY\" + $certThumbprint

$cert = get-item $certPath

$buildPath = $args[2]

$packagename = $args[3]

$serviceconfig = $args[4]

$servicename = $args[5]

$storageAccount = $args[6]

$package = join-path $buildPath $packageName

$config = join-path $buildPath $serviceconfig

$a = Get-Date

$buildLabel = $a.ToShortDateString() + "-" + $a.ToShortTimeString()

 

if ((Get-PSSnapin | ?{$_.Name -eq "WAPPSCmdlets"}) -eq $null) # Change WAPPSCmdlets to AzureManagementToolsSnapIn for cmdlets v1

{

  Add-PSSnapin WAPPSCmdlets # Change WAPPSCmdlets to AzureManagementToolsSnapIn for cmdlets v1

}

 

$hostedService = Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub | Get-Deployment -Slot Staging

 

if ($hostedService.Status -ne $null)

{

    $hostedService |

      Set-DeploymentStatus 'Suspended' |

      Get-OperationStatus -WaitToComplete

    $hostedService |

      Remove-Deployment |

      Get-OperationStatus -WaitToComplete

}

 

Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub |

    New-Deployment Staging -package $package -configuration $config -label $buildLabel -serviceName $servicename -StorageServiceName $storageAccount |

    Get-OperationStatus -WaitToComplete

 

Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub |

    Get-Deployment -Slot Staging |

    Set-DeploymentStatus 'Running' |

    Get-OperationStatus -WaitToComplete

 

$Deployment = Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub | Get-Deployment -Slot Staging

Write-host Deployed to staging slot: $Deployment.Url

 

if ($error) { exit 888 }

Creating a Build Definition

With the build targets and PowerShell scripts in place, you’re now ready to create a Build Definition for each environment you want to deploy to. You’re free to choose whatever build template you want and customise the settings as required for your build process. In order to trigger the build and deployment, you pass certain properties to the build process by selecting the Process tab and entering the properties under MSBuild Arguments. Here’s an example of what this could look like for a specific Azure subscription and service:

/p:AzureDeployEnvironment="Test" /p:AzureSubscriptionID="847d5f81-1111-2222-9984-af8c008ba1b7" /p:AzureCertificateThumbprint="A38242009E0A1A4DDC0000111122220D943C98E8"
/p:AzureHostedServiceName="myservice" /p:AzureStorageAccountName="mystorage"

Configure the build server with the Windows Azure Management Certificate

There’s one final step required before we’re ready to kick off the build. Before you can deploy anything to Windows Azure, you need a Service Management Certificate installed on your build server and uploaded to your Windows Azure subscription. If you haven’t yet got a Management Certificate for your Windows Azure subscription, I find that the easiest way to create this is to use Visual Studio 2010 to manually publish your solution to Windows Azure. The Manage Credentials dialog boxes will walk you through the process of creating a certificate and uploading it to your Windows Azure subscription via the Windows Azure Platform portal. Once you have your certificate installed on your development machine, you need to also get it installed on your build server. Here’s how to do that:

  1. On your development machine that already has the Management Certificate installed, export your certificate to a .pfx file:
    1. Launch Microsoft Management Console (mmc.exe)
    2. Choose File | Add/Remove Snap-In, add the Certificates snap-in and choose the My User Account option.
    3. In the tree view, expand Certificates – Current User\Personal\Certificates
    4. Find the Management Certificate in the details pane
    5. Right-click the certificate, choose All Tasks | Export
    6. In the Certificate Export Wizard, choose to export the private key, set a password and export to a .pfx file
  2. On your build server, import the .pfx file and set the private key permissions:
    1. Launch Microsoft Management Console (mmc.exe)
    2. Choose File | Add/Remove Snap-In, add the Certificates snap-in and choose the My Computer option.
    3. In the tree view, expand Certificates – Current User\Personal\Certificates
    4. Right-click the Certificates folder and choose All Tasks | Import
    5. Browse to the .pfx file you exported earlier (note, you’ll need to change the file type filter to see .pfx files) and enter the password to import the certificate.
    6. Find the newly-imported certificate, right-click it and choose Manage Private Keys
    7. Ensure that the user account that the Build process runs under has read access to the certificate’s private key.

After following these steps, you should be able to automatically deploy your solution to multiple Windows Azure environments as a part of a TFS automated build. Note however that this build process does not automate the process of uploading certificates. Since this process is normally only performed once per environment, you may decide it’s not worth automating. However if you want to do this, you can look to Scott Densmore’s second post for a working sample.

Next, we will extend this solution to support transformation of configuration files, and also show what changes are required to support publishing and deploying solutions that include more than one web site within a single web role.

Transforming Configuration Files

Configuration files are the most common solution to storing application information that has the potential to change more often than the code does. Quite often, the information you put into configuration files is environment-specific—that is, you need different settings for environments such as Test, UAT and Production. The most common example is database connection strings, but in a large application there will likely be many others.

For years, build masters and release managers have been devising clever ways to automatically update configuration files for each environment. However this became a lot simpler with the release of Visual Studio 2010 which has built-in support for web.config transforms. For those unfamiliar with this feature, it lets you specify a default web.config file, plus a “transform” file per build configuration (e.g. web.Debug.config, web.Release.config) that specifies deltas from the original file. These deltas may include adding or removing elements, or changing attribute values for existing elements.

The web.config transforms feature can be used happily on Windows Azure projects too. However things are complicated by the fact that Windows Azure uses additional config files beyond web.config: namely ServiceDefinition.csdef and ServiceConfiguration.cscfg which specify your application’s static and dynamic service characteristics respectively. If you want to specify different configuration in these files per environment, there’s no way to do this out of the box. Luckily, it’s relatively easy to reuse the same transformation engine to apply to these files too.

Before I get on to the solution, it’s worth briefly discussing why you may need different configuration per deployment environment. In the case of ServiceConfiguration.cscfg the answer is relatively obvious. It’s extremely likely that you may want to use different connection strings, application settings or certificates in different environments. You may also want to run different numbers of instances of each role. However ServiceDefinition.csdef contains the static definition of your application’s service model, which shouldn’t need to change, right? Actually I found a couple of reasons why this is useful. First, you may want to use smaller VM sizes in your test environments to production to save money. Second, I found it was necessary to change the relative paths of secondary web sites when hosting more than one site in a web role – this is discussed in more detail later in this post.

To implement transformations of both ServiceDefinition.csdef and ServiceConfiguration.cscfg, I followed the approach developed by Alex Lambert but extended it to transform both file and fit within the build process described in my previous post.

Once again, I chose to define my targets directly within my .ccproj file. Note that you need to manually declare each transformation file within the <ItemGroup> ; the rest of the markup should not need to change as you add more environments.

<ItemGroup>

    <EnvironmentConfiguration Include="ServiceConfiguration.Test.cscfg">

      <BaseConfiguration>ServiceConfiguration.cscfg</BaseConfiguration>

    </EnvironmentConfiguration>

    <EnvironmentConfiguration Include="ServiceConfiguration.Prod.cscfg">

      <BaseConfiguration>ServiceConfiguration.cscfg</BaseConfiguration>

    </EnvironmentConfiguration>

    <EnvironmentDefinition Include="ServiceDefinition.Test.csdef">

      <BaseConfiguration>ServiceDefinition.csdef</BaseConfiguration>

    </EnvironmentDefinition>

    <EnvironmentDefinition Include="ServiceDefinition.Prod.csdef">

      <BaseConfiguration>ServiceDefinition.csdef</BaseConfiguration>

    </EnvironmentDefinition>

 

    <!-- make our environment configurations appear in VS -->

    <None Include="@(EnvironmentConfiguration)" />

    <None Include="@(EnvironmentDefinition)" />

  </ItemGroup>

 

  <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.targets" />

 

  <Target Name="ValidateServiceFiles" Inputs="@(EnvironmentConfiguration);@(EnvironmentConfiguration->'%(BaseConfiguration)');@(EnvironmentDefinition);@(EnvironmentDefinition->'%(BaseConfiguration)')"

   Outputs="@(EnvironmentConfiguration->'%(Identity).transformed.cscfg');@(EnvironmentDefinition->'%(Identity).transformed.csdef')">

    <Message Text="APL: ValidateServiceFiles: Transforming %(EnvironmentConfiguration.BaseConfiguration) to %(EnvironmentConfiguration.Identity).tmp via %(EnvironmentConfiguration.Identity)" />

    <TransformXml Source="%(EnvironmentConfiguration.BaseConfiguration)" Transform="%(EnvironmentConfiguration.Identity)"

     Destination="%(EnvironmentConfiguration.Identity).tmp" />

 

    <Message Text="APL: ValidateServiceFiles: Transforming %(EnvironmentDefinition.BaseConfiguration) to %(EnvironmentDefinition.Identity).tmp via %(EnvironmentDefinition.Identity)" />

    <TransformXml Source="%(EnvironmentDefinition.BaseConfiguration)" Transform="%(EnvironmentDefinition.Identity)"

     Destination="%(EnvironmentDefinition.Identity).tmp" />

   

    <Message Text="APL: ValidateServiceFiles: Transformation complete; starting validation" />

    <ValidateServiceFiles ServiceDefinitionFile="@(ServiceDefinition)" ServiceConfigurationFile="%(EnvironmentConfiguration.Identity).tmp" />

    <ValidateServiceFiles ServiceDefinitionFile="%(EnvironmentDefinition.Identity).tmp" ServiceConfigurationFile="@(ServiceConfiguration)" />

 

    <Message Text="APL: ValidateServiceFiles: Validation complete; renaming temporary file" />

    <Move SourceFiles="%(EnvironmentConfiguration.Identity).tmp" DestinationFiles="%(EnvironmentConfiguration.Identity).transformed.cscfg" />

    <Move SourceFiles="%(EnvironmentDefinition.Identity).tmp" DestinationFiles="%(EnvironmentDefinition.Identity).transformed.csdef" />

  </Target>

 

  <Target Name="CopyTransformedEnvironmentConfigurationXmlBuildServer" AfterTargets="AfterPackageComputeService" Condition="$(AzureDeployEnvironment)!=''">

    <Copy SourceFiles="ServiceConfiguration.$(AzureDeployEnvironment).cscfg.transformed.cscfg" DestinationFiles="$(OutDir)ServiceConfiguration.cscfg" />

    <Copy SourceFiles="ServiceDefinition.$(AzureDeployEnvironment).csdef.transformed.csdef" DestinationFiles="ServiceDefinition.build.csdef" />

  </Target>

With this in place, you can now create files such as ServiceConfiguration.Test.cscfg and ServiceDefinition.Test.csdef that use the same web.config transform syntax to override or modify configuration properties from the base file. Note that including XML namespaces in the transform files is required, as per the following example.

<?xml version="1.0" encoding="utf-8"?>

<sc:ServiceConfiguration

 xmlns:sc="https://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" xmlns:xdt="https://schemas.microsoft.com/XML-Document-Transform">

  <sc:Role>

    <sc:ConfigurationSettings>

      <sc:Setting xdt:Transform="Replace" xdt:Locator="Match(name)" name="Environment" value="Test"/>

    </sc:ConfigurationSettings>

  </sc:Role>

</sc:ServiceConfiguration>

Supporting Multiple Sites per Role

With the release of Windows Azure SDK 1.3, it’s now possible to host several web sites, web applications or virtual directories within a single web role. This can be accomplished by specifying additional elements within the <Sites> element in your ServiceDefinition.csdef file. The following example shows how to deploy two sites within a web role. Note that the location of the second web site is defined using the physicalDirectory attribute, which can be an absolute or relative (to ServiceDefinition.csdef) path:

<ServiceDefinition name="CloudService" xmlns="https://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">

  <WebRole name="Site1">

    <Sites>

      <Site name="Web">

        <Bindings>

          <Binding name="Endpoint1" endpointName="Endpoint1" />

        </Bindings>

      </Site>

      <Site name="Site2" physicalDirectory="..\Site2">

        <Bindings>

          <Binding name="Endpoint2" endpointName="Endpoint2" />

        </Bindings>

      </Site>

    </Sites>

    <Endpoints>

      <InputEndpoint name="Endpoint1" protocol="http" port="80" />

      <InputEndpoint name="Endpoint2" protocol="http" port="8080" />

    </Endpoints>

  </WebRole>

</ServiceDefinition>

While configuring multiple web sites per role in Visual Studio is easy, I ran into a few problems when attempting to package and deploy my service using the automated build process. The problem stems from the fact that the files are laid out differently on the build server to the development environment. Specifically, while “..\Site2” was correct on my development machine, on the build server “..\Site2” points to the uncompiled sources folder, so the deployed web site has no bin folder or DLLs and simply won’t run.

Fortunately, the configuration transformation mechanism described earlier in this post makes it easy to use a different relative path when running on the build server. Here’s how I used my ServiceDefinition.Test.csdef file to redirect to a compiled version of the web site:

<sd:ServiceDefinition sd:name="CloudService" xmlns:sd="https://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition" xmlns:xdt="https://schemas.microsoft.com/XML-Document-Transform">

  <sd:WebRole xdt:Transform="SetAttributes" xdt:Locator="Match(name)" name="Site1" vmsize="Small">

    <sd:Sites>

      <sd:Site xdt:Transform="SetAttributes" xdt:Locator="Match(name)" name="Site2" physicalDirectory="..\..\..\Binaries\_PublishedWebsites\Site2">

      </sd:Site>

    </sd:Sites>

  </sd:WebRole>

</sd:ServiceDefinition>

We’re getting close now, but we’re not quite there. When I ran the build with these configuration transforms in place, I found that Site2 had not yet been compiled at the time when the Windows Azure service was being packaged. This is because, by default, the Windows Azure project has no dependency on secondary web sites so the build process can choose to compile these projects after the packaging is complete. You can change this by right-clicking the Windows Azure project node in Visual Studio’s Solution Explorer, choose Project Dependencies, and tell it to depend on all projects that need to be included in the deployment package:

clip_image001[4]

With this dependency defined and checked in, you should now be good to go with automated build and deployment of multiple web sites within a single web role.

Summary

I hope this information is helpful for anyone looking to set up automated build and deployment of Windows Azure applications with MSBuild and Team Foundation Server 2010. While this approach has been used successfully in real customer projects, I do want to stress that this doesn’t constitute a complete build process. There are many other things you should consider, including:

  • Deploying any required data to Azure queues, tables or blobs
  • Deploying SQL Azure schemas and migrating data
  • Deploying certificates as a part of your build process and managing differences across environments
  • Protecting credentials and keys for production environments
  • Integrating automated testing into the build
  • Managing which sources and binaries can get deployed to production

Still, most of these have been solved before and should be easy enough to integrate with the process described in this post. Happy building!

Comments

  • Anonymous
    February 28, 2011
    Wow thanks Tom! Your detail on the transformations just saved me from a kludgy solution for my development vs. test vs. deployment setup. I owe you one.

  • Anonymous
    March 08, 2011
    Tom, Correct me if I'm wrong but you appear to be suggesting I rebuild for each environment I target? Cheers, Steve.

  • Anonymous
    March 08, 2011
    Hi Steve - I'm not suggesting you do anything :-) However you are correct that the build process as described above involves rebuilding for each environment. In a real-world situation you'd likely include different build steps for each environment - at a minimum to pull down a specific version of the source, or possibly to pull down binaries produced from a different build. But if you want to use the config transforms, you'll still need to repackage for each environment. Overall, this post was designed to show how to accomplish key tasks around transfroming config, packaging and deploying to Azure, but you will still want to take some time to determine how this all fits within your own (likely more complex) build process. Tom

  • Anonymous
    March 17, 2011
    Hi Tom, Really useful post. To Steve's point, I think it's better to package and transform all the files for the different environments and have a separate step that executes the Powershell script to actually publish one of them. Maybe using something like TFS Deployer. Cheers, Miguel