Sunday, February 13, 2011

Web Deploy : Customizing a deployment package

Microsoft’s Web Deployment Tool is one of those great improvements for managing ASP.NET applications that I’ve been wanting to give try. Ever since Scott Hanselman wrote about it.

Web Deploy was created to automate common deployment tasks by packaging ASP.NET applications into a .zip with a manifest. It supports both command line deployment and an interactive mode, through the IIS management console.

This post is the result of my first attempt to roll out package based deployment using Web Deploy at a customer.

It’s been sort of a bumpy ride getting everything implemented but the tool definitely delivers what it promises; seamless deployment for your web app.

Here’s what I set out to do:

  1. Build a deployment package for an existing application
  2. Verify the correct pipeline mode on deployment
  3. Include an empty folder (not something supported out of the box, oddly enough)
  4. Give write access to specific folders
  5. Prevent specific folders from being cleared on deployment
  6. Customize the deployment package name to include a version and/or build number

I managed to get all of these implemented, mostly by hand-authoring an MSBuild script. One thing that disappointed me though was that interactive installation does not allow me to prevent specific folders from being cleared. That’s for command line deployment only.

In this post I’ll first give some quick pointers on how to get started with Web Deploy and building packages. After that, I’ll work down the list of requirements posted above one by one.

Note that I’m building the application using Visual Studio 2010 and MSBuild 4. The application targets IIS7, I have not tried this with IIS6.
If you’re not familiar with authoring MSBuild scripts, get the book and read it. I did so a while back and that really helped me.

Build a deployment package

Building a deployment package in Visual Studio 2010 is easy; it’s supported out of the box.

image

Using the Package/Publish settings allows you to configure some basic settings like the application name.

Web Deploy 101

To understand what makes Web Deploy tick, open the xml files from the generated package (.zip) in a text editor. The archive.xml file describes what to deploy en how to do it. The parameters.xml file lists all customizable parameters. These can be used to customize deployment after the package has been built.

The build also creates a command script (.cmd) you can use to deploy the application from the command line. (This file is broken however, more on that later). The command file will use the SetParameters.xml file to override default values specified in the parameters.xml file.

There is plenty of good reference documentation for command line use of the Web Deploy tool on TechNet. Check out the section on providers to get a good idea of what Web Deploy can do. For practical how-to’s IIS.NET is the place to look.

For building deployment packages the most useful resource however is the main MSBuild targets file:

 [ProgramFiles]\MSBuild\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.targets

I learned a lot from digging into this file. Thankfully, this file has a lot of hooks you can use to customize your package.

Extending Web Deployment

You can extend the package building pipeline by adding a custom msbuild project file to your application. It should be named [your project].wpp.targets and should be placed in the same folder as the web application project file [see this article]. Set the file to build type None in the properties in Visual Studio to make sure it is not included in the deployment package.

In addition to the custom targets file, you can also add a parameters.xml file. Here you can add configurable parameters to your deployment. This is mostly usefull for interactive deployment. You could for example allow users to modify settings for specific AppSettings. That would look like this:

<?xml version="1.0" encoding="utf-8" ?>
<parameters >
   <parameter name="Admin Email" description="Please provide the e-mail address to notify when errors occur." defaultValue="admin@example.com" tags="">
      <parameterEntry kind="XmlFile" scope="\\web.config$" match="/configuration/appSettings/add[@key='adminEmail']/@value" />
   </parameter>
</parameters>

In this post I’ll focus on the *.wpp.targets file since there is the least documentation on it.

Important: I’ve found that the *.wpp.targets file is not automatically reloaded by Visual Studio. While making changes to the file you’ll probably want to build the package from the command line.

Building a deployment package from the command line

There is no convenient top level target to build a deployment package. I found this method on StackOverflow which works well for me. In a VS 2010 command prompt enter the following command:

msbuild YourSolution.sln /t:rebuild /p:Configuration=Release /p:DeployOnBuild=true;DeployTarget=Package

Create deployment package on a build server

Using the command line above you can get a build server to build a deployment package. The build server should either have Visual Studio 2010 installed or you can use this recipe.

imageOn TeamCity I managed to get the package build using the Visual Studio build runner and some additional command line parameters. The MSBuild build runner should also work.

On Hudson I was able to reuse the command line from the previous section, but I had to add the full path to the msbuild executable (i.e. c:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe ).

Verify the pipeline mode on deployment

One of those all important IIS setting is the application pool’s pipeline mode. Newer applications will usually require Integrated mode and will likely break when run in Classic mode. Here’s how to ensure the application pool is in integrated mode:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <DeployManagedPipelineMode>Integrated</DeployManagedPipelineMode>
  </PropertyGroup>
</Project>

There are a couple of basic settings you can check / enforce through properties:

Setting Property Values
Pipeline mode DeployManagedPipelineMode Integrated or Classic
Enable 32-bit applications DeployEnable32bitAppOnWin64 True or False
Set AppPool .NET runtime DeployManagedRuntimeVersion 2.0 or 4.0
If empty, it defaults to application’s .NET runtime version.

Take a look at the targets file that is used to build the package for more properties:

[ProgramFiles]\MSBuild\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.targets

Include an empty folder

By default, the web deployment build scripts will filter out empty folders. In most cases that’s a good thing, for example for folders with code like /controllers in an MVC application. This folder will be empty if the application is pre-compiled and can safely be removed before deployment.

In some cases however you may want to include a folder to hold temporary data, uploaded files, cached files or whatever. There’s two ways to go about this:

  1. Include a placeholder file to make the folder non-empty
  2. Create the folder in the package location

I find the second option more elegant. Here’s what the msbuild script looks like:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <AfterAddIisSettingAndFileContentsToSourceManifest>MakeEmptyFolders</AfterAddIisSettingAndFileContentsToSourceManifest>
  </PropertyGroup>
  <Target Name="MakeEmptyFolders">
    <Message Text="Adding empty folder to hold downloads" />
    <MakeDir Directories="$(_MSDeployDirPath_FullPath)\Downloads"/>
  </Target>
</Project>

The propery AfterAddIisSettingAndFileContentsToSourceManifest holds the names of targets that will be invoked after the contents for the deployment package has been prepared. This is the perfect time to add additional files or folders to the package.

Give write access to specific folders

The empty folder created above should also be writeable so that the administrative users can upload files into it. There are a couple of resources describing this, but none that capture the complete picture.

The Web Deployment tool has a provider specifically for this purpose; setAcl. This provider will conveniently apply the rights you specify for the application pool identity. However, the setAcl rule in the deployment manifest needs a scope to be able to determine the application pool identity for the target folder or file. It took some digging into Microsoft.Web.Publishing.targets and lots of trial-and-error to figure this one out. In msbuild it looks like this:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <AfterAddIisSettingAndFileContentsToSourceManifest>SetCustomACLs</AfterAddIisSettingAndFileContentsToSourceManifest>
    <AfterAddDeclareParametersItemsForContentPath>SetCustomAclParameters</AfterAddDeclareParametersItemsForContentPath>
  </PropertyGroup>


  <Target Name="SetCustomACLs">
    <Message Text="Adding Custom ACls" />
    <!-- Create empty folders -->
    <MakeDir Directories="$(_MSDeployDirPath_FullPath)\Downloads"/>
    <ItemGroup>
     <!--Make sure the application pool identity has write permission to the download folder -->
     <MsDeploySourceManifest Include="setAcl"
       Condition="$(IncludeSetAclProviderOnDestination) And Exists('$(_MSDeployDirPath_FullPath)\Downloads')">
      <Path>$(_MSDeployDirPath_FullPath)\Downloads</Path>
      <setAclAccess>Write</setAclAccess>
      <setAclResourceType>Directory</setAclResourceType>
      <AdditionalProviderSettings>setAclResourceType;setAclAccess</AdditionalProviderSettings>
     </MsDeploySourceManifest>
    </ItemGroup>
  </Target>
  <Target Name="SetCustomAclParameters">
    <EscapeTextForRegularExpressions Text="$(_MSDeployDirPath_FullPath)">
      <Output TaskParameter="Result" PropertyName="_EscapeRegEx_MSDeployDirPath" />
    </EscapeTextForRegularExpressions>
    <ItemGroup>
     <MsDeployDeclareParameters Include="Add write permission to Downloads folder"
      Condition="$(IncludeSetAclProviderOnDestination) and Exists('$(_MSDeployDirPath_FullPath)\Downloads')">
      <Kind>ProviderPath</Kind>
      <Scope>setAcl</Scope>
      <Match>^$(_EscapeRegEx_MSDeployDirPath)\\Downloads$</Match>
      <Description>Add write permission to Downloads folder</Description>
      <DefaultValue>{IIS Web Application Name}/Downloads</DefaultValue>
      <Value>$(_DestinationContentPath)/Downloads</Value>
      <Tags>Hidden</Tags>
      <Priority>$(VsSetAclPriority)</Priority>
      <ExcludeFromSetParameter>True</ExcludeFromSetParameter>
     </MsDeployDeclareParameters>
    </ItemGroup>
  </Target>
</Project>

There are two targets declared and hooked into the packaging pipeline. The first target, SetCustomACLs, is an extension of the empty folder solution discussed earlier. The empty folder is created and then the task MsDeploySourceManifest is invoked to make sure the required parameters are emitted to add the setAcl rules to the package manifest.

The second target, SetCustomAclParameters, is invoked to add the required parameters to setup the scope for the setAcl provider rules. The parameters are regular expression based so the base path ( in the _MSDeployDirPath_FullPath parameter) is escaped for use in a regular expression. Most of this is copied straight from Microsoft.Web.Publishing.targets where by default write access is assigned to the App_Data folder.

Prevent specific folders from being cleared on deployment

Assume that the downloads folder we’ve been working on up till now is in a production site. Files have been uploaded into that folder so they can be downloaded. Now you need to deploy a new version of the application. The default behavior for Web Deploy is to clear out any and all content that’s not in the deployment package. That would mean all the uploaded files are deleted. Not a good thing.

You could change the default behavior and disable clearing of the site. But that could mean stale files (like .aspx files) are left behind. Not a good thing either.

Fortunately, Web Deploy supports so called skip rules. A skip rule is used to exclude files or folders from a particular operation (Update, Delete, AddChild). In this case we want to prevent the Delete operation from being applied to the Downloads directory. That would look like this:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <OnBeforePackageUsingManifest>AddCustomSkipRules</OnBeforePackageUsingManifest>
  </PropertyGroup>
  <Target Name="AddCustomSkipRules">
    <ItemGroup>
      <MsDeploySkipRules Include="SkipDownloadsFiles">
        <SkipAction>Delete</SkipAction>
        <ObjectName>filePath</ObjectName>
        <AbsolutePath>.*\\Downloads\\.*$</AbsolutePath>
        <XPath></XPath>
      </MsDeploySkipRules>
      <MsDeploySkipRules Include="SkipDownloadsFolder">
        <SkipAction>Delete</SkipAction>
        <ObjectName>dirPath</ObjectName>
        <AbsolutePath>.*\\Downloads\\.*$</AbsolutePath>
        <XPath></XPath>
      </MsDeploySkipRules>
    </ItemGroup>
  </Target>
</Project>

The ObjectName should be dirPath for directories and filePath for files. The AbsolutePath element contains a regular expression that will be matched against the name or file or folder. Note that a directory name should end with a back-slash. In this example I’ve added a second skip rule make sure any files in subfolders are left untouched as well.

If a folder is skipped it automatically means that any file in that folder is skipped as well.

Warning : skip rules work only when deploying from the command line, using the generated command file. They are not included in the package manifest and will not be applied when deploying the package in intractive mode (ie. through IIS Manager).

Customize the deployment package name to include a version and/or build number

Finally, when building releases on a build server it’s nice to have the build artifacts named after the version or build number so that you know from the file name what version it is.

Visual Studio uses the PackageFileName property for this. We can override that in the  *.wpp.targets file:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <__ProductVersion Condition="'$(__ProductVersion)'=='' AND '$(VERSION)'!='' AND '$(REVISION)'!=''">$(VERSION).$(REVISION)</__ProductVersion>
    <__ProductVersion Condition="'$(__ProductVersion)'=='' AND '$(BUILD_NUMBER)'!=''">$(BUILD_NUMBER)</__ProductVersion>
    <__ProductVersion Condition="'$(__ProductVersion)'==''">dev</__ProductVersion>
    <PackageFileName Condition="'$(BuildingInsideVisualStudio)' != 'true'">..\..\Build\$(MSBuildProjectName)-$(__ProductVersion).zip</PackageFileName>
  </PropertyGroup>
</Project>

This MSBuild snippet uses a couple of variables to determine the product version, captured in the __ProductVersion property. Exactly how these properties are set is outside the scope of this post. They could be environment variables or they could come from the main project file for example. In this particular case the Hudson build will set the VERSION and REVISION properties. If those are not set, the BUILD_NUMBER will be used. This property is set by the TeamCity build server to increment on each build. If that is not available either the build is assumed to be from the command line on a development system, hence __ProductVersion is set to dev.

Finally, the PackageFileName property is set. This is the property used by the web packaging build to determine the package file name and path. In this sample, the package will be created in the Build folder and will be named after the main project, suffixed with the product version. Also note that the BuildingInsideVisualStudio property will be set to true if we’re building in Visual Studio. In that case we’ll use whatever was defined in the Publish & Package settings for the project.

If we’re modifying the package file name to change with every build we also need to add some additional logic to clean up older versions. Here’s how to do that:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <CleanDependsOn>
      $(CleanDependsOn);
      CleanAllPackagesFromBuildFolder;
    </CleanDependsOn>
    <Target Name="CleanAllPackagesFromBuildFolder">
    <ItemGroup>
      <__BuildFolderCleanup Include="..\..\Build\$(MSBuildProjectName)*"/>
    </ItemGroup>
    <Delete Files="@(__BuildFolderCleanup)"
          Condition="Exists('..\..\Build')"
          TreatErrorsAsWarnings="True"  />
  </Target>
</Project>

First the Clean target is extended to run our custom cleanup target by modifying the CleanDepensOn property. The CleanAllPackagesFromBuildFolder target then proceeds to delete all files from the Build folder. That’s the folder we indicated our package should be created.

Fixing the MSDeploy version

The steps above customize the generated package and the build process so that all the requirements I listed at the beginning of this post are met. Unfortunately, the Microsoft.Web.Publishing.targets file that shipped with Visual Studio 2010 is not quite up to date. Web Deployment Tool v2 has shipped recently but targets file is hard coded to generate a command file for version 1.

The command script uses a registry key to determine where msdeploy.exe is installed:

HKLM\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\1

Unfortunately, if you install only the latest release this key does not exist and the script will fail. The fix is simple; replace the 1 at the end with a 2. Here’s how that looks in msbuild:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <OnAfterPackageUsingManifest>
		$(OnAfterPackageUsingManifest);
		FixMsDeployVersion;
    </OnAfterPackageUsingManifest>
  </PropertyGroup>
  <Target Name="FixMsDeployVersion" DependsOnTargets="GenerateSampleDeployScript">
	<PropertyGroup>
		<__DeployScriptLocation>$(GenerateSampleDeployScriptLocation)</__DeployScriptLocation>
		<__DeployScriptContents>$([System.IO.File]::ReadAllText('$(__DeployScriptLocation)'))</__DeployScriptContents>
	</PropertyGroup>	
	<WriteLinesToFile File="$(GenerateSampleDeployScriptLocation)"
               Overwrite="True"
               Encoding="us-ascii"
               Lines="$(__DeployScriptContents.Replace('HKLM\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\1', 'HKLM\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\2'))"/>
  </Target>
</Project>

PackageUsingManifest is the main target performing the package generation. When it’s done we want to do our final fix of the command script. We hook in our target by modifying the OnAfterPackageUsingManifest property. To make sure our target is executed after the deploymeny command script is generated a dependency on the GenerateSampleDeployScript is added to our target.

The FixMsDeployVersion target itself pulls in the entire command script from disk and does a String.Replace on it to fix the registry key before writing the whole file back to disk.

Conclusion

The Web Deploy tool offers a lot to ease the deployment of ASP.NET applications. Tailoring the build to match every day deployment requirements is possible but by no means easy. I’ve had to do a lot of digging into the bowels of the build targets file to find the right extension points. As far as I know, there is no documentation on the MSBuild tasks used by the packaging pipeline.

The fact that interactive deployment does not support skip rules is really a huge mistake. That and the fix needed for the command script are things that would prevent me from rolling out the web deployment tool at a customer.

However, once the deployment package is customized it’s just so easy to deploy a new version. No more diffing directories when deploying an update. No more last minute phone calls when deploying to production because somebody forgot to create a folder or set the appropriate access rights.
This is a huge win for developers and QA people needing to deploy new versions often for testing as well as for IT people that need to deploy to production.

I’ll post a complete sample solution shortly.

[Update 2012-11-29] The code sample for preventing folders and their contents from being deleted was not quite perfect. Turns out it’s needed to exclude both the folder (and sub folders) and the files.

21 comments:

Paulo said...

The most complete post about Web Deploy pipeline I´ve seen!

Paulo said...

The most complete post about Web Deploy pipeline I´ve seen!

Vishal R Joshi said...

Very cool post Marnix. Thanks for putting it together.

Pradeep Y said...

Nice post on web deployment package.

Is it possible to exclude some files from the package?

I want include only updated files ,from previous build, in the package.

koistya said...

Great blog post. Thanks! That's exactly what I was looking for..

Brake said...

Thank you for a great article! I am working on a Silverlight WCF RIA application. I have build and deployment (TeamCity) up and running! What I need is to obfuscate the Silverlight assemblies prior to deployment. Is it (at all) possible to insert a step between packaging and deployment that would obfuscate the SL assemblies? I have been trying this with no success :-( So, as far as you know: is this possible at all? And if it's possible could you give me a hint as to how to solve this issue? Thank you very much in advance!

Marnix said...

Yes, that's entirely possible. I know of at least one obfuscator (DeepSea) that can obfuscate SilverLight assemblies. You could invoke the obfuscator during the AfterAddIisSettingAndFileContentsToSourceManifest target. At that point all the files are copied and ready to be zipped into the package. Check the section on adding an empty folder above for an example of how you can modify the deployment package contents. I would however recommend you do obfuscation a bit earlier. To be on the safe side, you should also run your tests against obfuscated assemblies, so you may want to integrate obfuscation into the build of your SL assemblies.

Marnix said...

@Pradeep Y - You could remove all unchanged files before the package is zipped. However that would defeat the purpose of a web deployment package however. It's intended to be a complete web application. If you need something that updates your deployed sites quickly (with rollback) you may want to look into using git as a deployment tool.

koistya said...

The target added by using OnBeforePackageUsingManifest is never get executed when project is published (right click on the project > Publish). Do you know why this could be the case?

Marnix said...

@koistya I never deploy directly from Visual Studio, so I haven't run into this issue yet. If you can figure out what target is used to publish from Visual Studio, you should be able to track down what is happening. From what I've read though, the Publish feature in Visual Studio is not entirely based on MSBuild, so it may be difficult or even impossible to figure out what's happening [article].
As always, StackOverflow is a good place to look for more info. Please let me know if you figure this out.

koistya said...

For now I am adding skip rules by using AfterAddIisSettingAndFileContentsToSourceManifest. It seems like it works with both MSBuild and Visual Studio > Publish.

Stif said...

Very interesting post! It's hard to find decent and concise documentation on this topic.

I'm trying to a bit further using this tutorial and the links you provided but I'm having a few troubles.

I want to do 3 things using a *.wpp.targets files:
1. Define the app pool (and create it if it's non-existing) that should be used by the web application

2. Specify the physical path to where the package should be deployed

3. modify the Web Application name so that it contains a version number ($(VersionNumber) == MSBuild variable)

Any suggestions on how to do this or where to start?

Marnix said...

I think all of these things are possible with web deploy. Take a look at the iisApp provider for web deploy. This provider is all about deploying applications to folders.
I'm not sure how to get that working from a pre-built and packaged application like described in this article.

It is possible to inject additional commands for providers into the manifest. The setAcl provider, for example, is used to set access rights from the deployment package. I'm not sure whether it is possible to create the application in IIS that way; I haven't tried that.

An alternative approach could be to create (or generate) a batch file or power shell script. From the script create the web application in IIS using the web deploy command line and then deploy your application from the package.

Ranco said...

Hi Marnix,

Thanks for the great post! I worked half-way through before I realized it was yours. Keep up the good work.

Best regards, Ranco

Andrei said...

When you defined the folder and files skip rules to prevent deletion, was it necessary to specify both:
        .*\\Downloads
\\$

And
        .*\\Downloads\\.*$


Doesn't the 2nd regular expression include the first one?

Isn't the zero or more characters .* after path separator character matches the folder path too?

Thanks!

Marnix said...

@Andrei Good point. I thought I had fixed that already. It should be two rules, but 1 for dirPath and 1 for filePath. I've updated the code.

Stephan Peters said...

We are implementing a pluggable architecture at a client.
We have one ASP.NET MVC web core with N plugins created by separate teams (which go into the Areas folder of the core).
Each plugin is a separate ASP.NET MVC project and we should be able to deploy it.
We want the ability for a plugin team to deploy the plugin by creating a package.
The core should not be redeployed each time.
How would this have to be achieved (custom targets file)?

All content (views, scripts, css) must go into the Areas sub folder of the core.
The dll files must go into the bin folder of the core.

The goals is that an administrative person runs the package (eventually with some customizations).

Marnix said...

I don't think WebDeploy was designed for this scenario. It's more geared towards deploying and updating from a single deployment package.

You could probably deploy your application in parts if IIS considers each part to be a separate application. So you'd have to configure a folder in IIS where each plugin lives. But that will also have implications for your applications architecture.

Some other solutions come to mind like using filters to ignoring parts of the application you don't want to touch. But that's fiddly to say the least.

Aside from WebDeploy, you could look at using portable areas. That will allow you to package all the code, views and other files into a single assembly for each plugin. That could make deploying individual plugins really easy.

You could use WebDeploy to deploy the core and then you would copy in plugins as needed.


JohnC said...

Thanks for a great black-belt article.

Do you mean that settings such as DeployManagedRuntimeVersion must go in a custom msbuild project file aka "[your project].wpp.targets" and can not go in the main proj file "[your project].csproj"?

Marnix said...

No, you can put it in the .csproj if you want. I find it easier though to keep all the deployment related settings together.

Anonymous said...

Did you ever post a sample project or solution for this?