Employee photo

About

Tom Revans is a specialist C#/ASP.NET Developer in Reading, Berkshire.

He has been programming using C#/ASP.NET for 6 years and also Objective C for the past year.

Skills

  • C#/ASP.NET
  • SQL Server
  • XHTML
  • CSS
  • jQuery


TeamCity, MSBuild & MSDeploy… how hard can it be? Part 2

September 24th, 2011

In part 1 I spoke about what I was trying to achieve when setting up my continuous integration server.

  1. Continue using my current source control (Subversion)

  2. The use of TeamCity as my build server (free for basic use)

  3. Automate builds to push to my test and live server with the use of a single set of build files and also take care of all configuration files

  4. A maintenance page to be shown during a deployment for external users but also allow an admin to view site during and after deployment (using IIS)

With my solution I used 3 TeamCity configurations, as described in Part 1. My first configuration does the web.config transforms, building of solution and copy to the test server (with offline page during deployment). The 2nd configuration pushes to live (again with offline page during deployment) and the 3rd configuration enables live for all traffic.

Each configuration is a single MSBuild file, in the screenshot below for my first configuration its called “builddev.target”. I used TeamCity to pass in a single parameter which is a environment variable of the working directory for the build.

env.Path = %teamcity.build.workingDir%/Client Name Here/Code/iPhone/

There are some other files which required for this solution to work:

  • A web.config to stop appPool recycling
  • A maintenance/offline page which in my case is a simple html file
  • A set of http redirect rules to config the redirection
  • An empty set of http redirect rules to enable traffic as before

The folder structure I used looks like this:

My MSbuild file (which for my was contained in my source control but could be anywhere you specify) starts off with a root element that contains some default targets and also includes an additional MSbuild task that allows the web.config transform to be used.

<Project ToolsVersion="4.0"
	 DefaultTargets="Build;AfterBuild"
	 xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <UsingTask TaskName="TransformXml"
               AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll"/>

The first job is to make sure the solution gets built.

<Target Name="Build">
     <MSBuild Projects="$(SolutionLocation)" Targets="Clean;ReBuild" />
</Target>

These are the properties that I use throughout (with some things like username and password I have removed for my blog ;) ).

<PropertyGroup>
     <DeployPath>F:\Deploy</DeployPath>
     <OfflineWebFile>Maintenance.htm</OfflineWebFile>
     <OfflineConfigWithRulesFile>Maintenance.config</OfflineConfigWithRulesFile>
     <OfflineConfigFile>Deploy.config</OfflineConfigFile>
     <OfflineWebFullPath>$(DeployPath)\$(OfflineWebFile)</OfflineWebFullPath>
     <OfflineConfigFullPath>$(DeployPath)\$(OfflineConfigFile)</OfflineConfigFullPath>
     <OfflineConfigWithRulesFullPath>$(DeployPath)\MaintenanceRules.config</OfflineConfigWithRulesFullPath>
     <OfflineConfigNoRulesFullPath>$(DeployPath)\MaintenanceEmpty.config</OfflineConfigNoRulesFullPath>
     <DeployConfigPath>$(DeployPath)\Configs</DeployConfigPath>
     <PackagePath>$(DeployPath)\Package</PackagePath>
     <SolutionLocation>$(Path)iPhone.sln</SolutionLocation>
     <ProjectPath>$(Path)iPhone</ProjectPath>
     <TestConfiguration>TestDeploy</TestConfiguration>
     <LiveConfiguration>LiveDeploy</LiveConfiguration>
     <TransformInputFile>$(ProjectPath)\Web.config</TransformInputFile>
     <TransformTestVersionInputFile>$(ProjectPath)\Web.$(TestConfiguration).config</TransformTestVersionInputFile>
     <TransformLiveVersionInputFile>$(ProjectPath)\Web.$(LiveConfiguration).config</TransformLiveVersionInputFile>
     <TransformTestVersionOutputFile>$(DeployConfigPath)\Web.$(TestConfiguration).config</TransformTestVersionOutputFile>
     <TransformLiveVersionOutputFile>$(DeployConfigPath)\Web.$(LiveConfiguration).config</TransformLiveVersionOutputFile>
     <DeployTestAppName>www.devdomain.co.uk</DeployTestAppName>
     <DeployLiveAppName>www.livedomain.co.uk</DeployLiveAppName>
     <DeployServerName>https://Your IIS Server IP:8172/MsDeploy.axd?Site=$(DeployTestAppName)</DeployServerName>
     <MSDeployPath>C:\Program Files\IIS\Microsoft Web Deploy V2\msdeploy.exe</MSDeployPath>
     <ServerUsername>Your IIS 7 Username</ServerUsername>
     <ServerPassword>Your IIS 7 Password</ServerPassword>
     <ServerApplicationRoot>f:\data\web\www.devdomain.co.uk</ServerApplicationRoot>
     <PackageName>Package</PackageName>
</PropertyGroup>

Below performs the transform of each web.config for each environment, in my case TestDeploy and LiveDeploy.

    <Target Name="Transform">
		<MakeDir Directories="$(DeployConfigPath)"
                         Condition="!Exists('$(DeployConfigPath)')"/>

        <TransformXml Source="$(TransformInputFile)"
                      Transform="$(TransformTestVersionInputFile)"
                      Destination="$(TransformTestVersionOutputFile)"
                      StackTrace="false" />

        <TransformXml Source="$(TransformInputFile)"
                      Transform="$(TransformLiveVersionInputFile)"
                      Destination="$(TransformLiveVersionOutputFile)"
                      StackTrace="false" />
    </Target>

The next target is to build the packages as zip files (test and live) using MSDeploy. Building the packages together and using the same configuration, Release, ensures the binaries are the same.

<Target Name="BuildWebPackageTest">
     <MSBuild Projects="$(SolutionLocation)"
              Properties="Platform=Any Cpu;
              Configuration=Release;
              DeployOnBuild=True;
              DeployTarget=Package;
              DeployIisAppPath=$(DeployTestAppName);
              PackageLocation=$(PackagePath)\$(PackageName)$(TestConfiguration).zip;"/>
</Target>
<Target Name="BuildWebPackageLive">
     <MSBuild Projects="$(SolutionLocation)"
              Properties="Platform=Any Cpu;
              Configuration=Release;
              DeployOnBuild=True;
              DeployTarget=Package;
              DeployIisAppPath=$(DeployLiveAppName);
              PackageLocation=$(PackagePath)\$(PackageName)$(LiveConfiguration).zip;"/>
</Target>

The next targets do the following…
Firstly the offline page is deployed
Then the offline config which prevents the app pool restarting during deployment
Then the offline rules which contains the redirect
Then the actual deployment of the website (excluding the web.config)
Then ensure the correct web.config is deployed
Then the empty version of the offline rules is deployed which will traffic through again
Lastly restart the app pool

	<Target Name="DeployOfflinePage">
		<Exec Command="&quot;$(MSDeployPath)&quot; -source:filePath=&quot;$(OfflineWebFullPath)&quot; -dest:filePath=$(ServerApplicationRoot)\$(OfflineWebFile),computerName='$(DeployServerName)',username=$(ServerUsername),password=$(ServerPassword),authType=basic -allowUntrusted=true -verb:sync" />
	</Target>

	<Target Name="DeployOfflineConfig">
		<Exec Command="&quot;$(MSDeployPath)&quot; -source:filePath=&quot;$(OfflineConfigFullPath)&quot; -dest:filePath=$(ServerApplicationRoot)\Web.config,computerName='$(DeployServerName)',username=$(ServerUsername),password=$(ServerPassword),authType=basic -allowUntrusted=true -verb:sync" />
	</Target>

	<Target Name="DeployOfflineRulesConfig">
		<Exec Command="&quot;$(MSDeployPath)&quot; -source:filePath=&quot;$(OfflineConfigWithRulesFullPath)&quot; -dest:filePath=$(ServerApplicationRoot)\$(OfflineConfigWithRulesFile),computerName='$(DeployServerName)',username=$(ServerUsername),password=$(ServerPassword),authType=basic -allowUntrusted=true -verb:sync" />
	</Target>

	<Target Name="DeployToTest">
		<Exec Command="&quot;$(MSDeployPath)&quot; -source:package='$(PackagePath)\$(PackageName)$(TestConfiguration).zip' -dest:auto,computerName='$(DeployServerName)',username=$(ServerUsername),password=$(ServerPassword),authType=basic -allowUntrusted=true -skip:objectName=filePath,skipAction=Delete,absolutePath=$(OfflineConfigWithRulesFile) -skip:objectName=filePath,skipAction=Delete,absolutePath=$(OfflineWebFile) -skip:objectName=filePath,skipAction=Delete,absolutePath=Web.config -verb:sync -setParamFile:$(PackagePath)\$(packageName)$(TestConfiguration).SetParameters.xml" ContinueOnError="false" />
	</Target>

	<Target Name="DeployCorrectConfig">
		<Exec Command="&quot;$(MSDeployPath)&quot; -source:filePath=&quot;$(TransformTestVersionOutputFile)&quot; -dest:filePath=$(serverApplicationRoot)\Web.config,computerName='$(DeployServerName)',username=$(ServerUsername),password=$(ServerPassword),authType=basic -allowUntrusted=true -verb:sync" />
	</Target>

	<Target Name="DeployEmptyOfflineConfig">
		<Exec Command="&quot;$(MSDeployPath)&quot; -source:filePath=&quot;$(OfflineConfigNoRulesFullPath)&quot; -dest:filePath=$(serverApplicationRoot)\$(OfflineConfigWithRulesFile),computerName='$(DeployServerName)',username=$(ServerUsername),password=$(ServerPassword),authType=basic -allowUntrusted=true -verb:sync" />
	</Target>

	<Target Name="RestartAppPool">
		<Exec Command="&quot;$(MSDeployPath)&quot; -source:recycleApp -dest:recycleApp='$(DeployTestAppName)',computerName='$(DeployServerName)',username=$(ServerUsername),password=$(ServerPassword),authType=basic,recycleMode='RecycleAppPool' -allowUntrusted=true -verb:sync" />
	</Target>

Finally I setup a task that calls all the above tasks:

	<Target Name="AfterBuild">
		<CallTarget Targets="Transform" />
		<CallTarget Targets="BuildWebPackageTest" />
		<CallTarget Targets="BuildWebPackageLive" />
		<CallTarget Targets="DeployOfflinePage" />
		<CallTarget Targets="DeployOfflineRulesConfig" />
		<CallTarget Targets="DeployOfflineConfig" />
		<CallTarget Targets="DeployToTest" />
		<CallTarget Targets="DeployCorrectConfig" />
		<CallTarget Targets="DeployEmptyOfflineConfig" />
		<CallTarget Targets="RestartAppPool" />
	</Target>

This is the temporary web.config that prevents restarts (as explained here)

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.web>
        <httpRuntime waitChangeNotification="300" maxWaitChangeNotification="300"/>
    </system.web>
    <system.webServer>
		<validation validateIntegratedModeConfiguration="false" />
		<modules runAllManagedModulesForAllRequests="true" />
		<rewrite>
		  <rules configSource="Maintenance.config"></rules>
		</rewrite>
    </system.webServer>
</configuration>

This is the file (MaintenanceRules.config) that defines the HTTP redirect rules. Which basically says redirect everything but the IP specified. How this is done and configured is located here.

<rules>
  <rule name="ShowMaintenancePage" enabled="true" stopProcessing="true">
	<match url="Maintenance.htm" />
	<conditions logicalGrouping="MatchAll" trackAllCaptures="false">
	</conditions>
	<action type="None" />
  </rule>
	<rule name="BlockExternalExpectSelected" patternSyntax="Wildcard" stopProcessing="true">
		<match url="*" />
		<conditions>
			<add input="{REMOTE_ADDR}" pattern="123.123.123.123" negate="true" />
		</conditions>
	  <action type="Redirect" url="Maintenance.htm" />
	</rule>
</rules>

Lastly the empty rule file (MaintanceEmpty.config) is very simple and is as follows:

<rules>
</rules>

You might wonder why I choose not to the use the app_offline.htm provided by ASP.NET… well it has issues. I won’t repeat them all but you want to see why check out this blog by Kurt Schindler.
The 2nd configuration to push to live, you pretty much use the above but only call this targets and change a few properties e.g. use live package.

	<Target Name="AfterBuild">
		<CallTarget Targets="DeployOfflinePage" />
		<CallTarget Targets="DeployOfflineRulesConfig" />
		<CallTarget Targets="DeployOfflineConfig" />
		<CallTarget Targets="DeployToLive" />
		<CallTarget Targets="DeployCorrectConfig" />
	</Target>

Lastly the 3rd configuration is simply the target to re-enable the traffic through once your happy with your release.

	<Target Name="AfterBuild">
		<CallTarget Targets="DeployEmptyOfflineConfig" />
		<CallTarget Targets="RestartAppPool" />
	</Target>

Summary

I hope this blog post will help someone else out there when coming across a similar deployment issue like mine. I am sure its possible to reuse the targets better and maybe reduce the number of properties that I used but this solution as it stands works well for me.

MSBuild and MSDeploy being used together is not really well documated all in one place, so I had to piece this together from various sources. I have included a reference list to the best of my knowledge.

Any questions please comment and I will try my best to get back to you.

References

Leave a response: