I am a firm believer that everything that can be easily automated should be.
Making a human responsible for repetitive IT work is not only inhumane, but also extremely inefficient and a waste of money.
Lately I have been working with automating deployment of new applications within our company. Deploying automatically to our customers is not within reach (yet).
The only thing that should be required for a developer in order to show his latest work to the rest of the company is to check in the code. The tests run and the application(s) should be deployed immediately after. A deployment package should also be created.
We are using TeamCity as our continuous build server, and it's doing a great job. The easiest way for us to build our code continuously, is to use the "sln2008" build runner. This means that TeamCity pulls the code from our code repository (Subversion) and compiles it. In addition we have set up the build runner to run our NUnit tests as well.
If we want to create an IIS application and deploy our code to it, we need to go for another option. Some common ones in .NET are NAnt, MSBuild, Rake and probably a lot more.
As a hardcore .NET guy my choice fell on MSBuild. I need to get started somewhere right?
An introduction to MSBuild
MSBuild is not that magical. Running MSBuild is as simple as running MsBuild.exe on the command line together with the path of your MSBuild script. The MSBuild script is an XML file. Actually the Visual Studio project files are MSBuild scripts. When you compile in Visual Studio, MSBuild is runs in the background.
The structure of an MSBuild script is pretty simple. It consists of a project. Inside a project you can have targets and inside each target you have one or more tasks:
<Project> <Target> <Task></Task> .. </Target> .. </Project>
A task is some action you want to perform, like creating a directory or deleting an IIS application.
A target is just a collection of tasks to be executed sequentially.
A project is just a wrapper for a set of targets.
The following build script will write a “hello world" message when you run it on the command line using "msbuild fooscript.msbuild"
<Project DefaultTargets="FooTarget"> <Target Name="FooTarget"> <Message Text="Hello world" /> </Target> </Project>
Notice that the project defines "DefaultTargets", which is a list of targets to run. Inside the FooTarget there is only a single task "Message" that will output "Hello world" to the console.
You can make multiple targets and let them run depending on the success of the previous target.
<Project DefaultTargets="FooTarget"> <Target Name="FooTarget" DependsOnTargets="FiiTarget"> <Message Text="Hello world" /> </Target> <Target Name="FiiTarget" > <Message Text="Hello world" /> </Target> </Project>
In the script above the default target is FooTarget, but since FooTarget depends on FiiTarget, FiiTarget will run before FooTarget. If FiiTarget succeeds, which it probably will, the FooTarget will run.
In MsBuild scripts you often have the need for defining variables that you can use later in the script. A variable is referred to using the $-sign. You can also define lists which are referred to using the @-sign. The following example demonstrates use of a variable:
<Project DefaultTargets="FooTarget">
<PropertyGroup>
<FooVariable>Hello world</FooVariable>
</PropertyGroup>
<Target Name="FooTarget">
<Message Text="$(FooVariable)" />
</Target>
</Project>
The above script will output "Hello World" (again) :)
Sometimes you don't want to put all of your targets in a single file. Maybe you want to use targets defined by third parties, like Microsoft or open source. I have used open source targets from tigris.org (http://msbuildtasks.tigris.org/). They have targets for copying files over FTP and editing XML-files. Both are valuable for automatic deployment.
To use targets defined in other files, you import them using the import statement:
<Project DefaultTargets="FooTarget"> <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" /> <Target Name="FooTarget"> <FtpUpload .... /> </Target> </Project>
Here I import the MSBuild file located at C:\Program Files (x86)\MSBuild\MSBuildCommunityTasks. MSBuild uses the built in variable $(MSBuildExtensionsPath) to find the MSBuild root directory. By doing this I make the task "FtpUpload" available to my script. The built-in Microsoft tasks defined here (http://msdn.microsoft.com/en-us/library/7z253716(v=VS.90).aspx) are always available.
If you are moving a script from one machine to another, you need to make sure that you also move the imported scripts. Usually you can find these in the MSBuild directory. In my case it is C:\Program Files (x86)\MSBuild.
Now we are ready to look at how I actually implemented our MSBuild script to automatically deploy our web applications.
Creating a continuous integration script using MSBuild.
To deploy a web site you basically need to build it, create an IIS application and deploy the application to the physical location of the web site. In addition you would run your tests using the NUnit task, but I will not go into that here.
To create the build script for my web site I installed “Web Deployment Projects” for Visual Studio. You can add this project to you Visual Studio solution the same way you add any other project. (http://www.microsoft.com/downloads/details.aspx?familyId=0AA30AE8-C73B-4BDD-BB1B-FE697256C459&displaylang=en)
The web deployment project creates a .wsproj file which is actually an MSBuild script. Surprise. This script is configured to build the web application. I have removed a lot of noise from the script:
<Project DefaultTargets="Build"> <PropertyGroup>...<PropertyGroup> <Import Project="$(MSBuildExtensionsPath)\Microsoft\WebDeployment\v9.0\Microsoft.WebDeployment.targets"/> <Target Name="BeforeBuild"> </Target> <Target Name="BeforeMerge"> </Target> <Target Name="AfterMerge"></Target> <Target Name="AfterBuild">...</ Target> </Project>
Notice that the default target is “Build”, but “Build” is not actually defined in this script. The “Build” target is actually defined in the imported script Microsoft.WebDeployment.targets. This script was put in the MSBuild folder when I installed the Web Deployment Project. It is the “Build” target that is responsible for building my web site. I will not go into the details of that script here. Suffice to say that Microsoft has done a good job. Notice however that when the “Build” task finishes, the final web site is put in the “./source” folder. In an MSBuild script it is common practice to operate with relative paths. This causes the least amount of hassle.
Next step is to deploy the web site to the IIS server. I did this by putting a set of tasks into the “AfterBuild” target in the .wsproj file. (Same as above, but with some details removed and some added)
<Project ...>
<PropertyGroup>
<Customer>$(FOO_CUSTOMER)</Customer>
<DeploymentDirectory>C:\ContinuousIntegration\$( Customer)</DeploymentDirectory>
<WebSite>Default Web Site</WebSite>
<AppName>$(Customer)</AppName>
<FullAppName>$(WebSite)/$(AppName)</FullAppName>
</PropertyGroup>
....
<Target Name="AfterBuild">
<ItemGroup>
<File Include=".\source\**\*.*" />
</ItemGroup>
<RemoveDir Directories="$(DeploymentDirectory)" />
<MakeDir Directories="$(DeploymentDirectory)"/>
<Copy
SourceFiles="@(File)"
DestinationFolder="$(DeploymentDirectory)\%(RecursiveDir)">
</Copy>
<Exec Command="appcmd delete app /app.name:"$(FullAppName)"" ContinueOnError="true"></Exec>
<Exec Command="appcmd add app /site.name:"$(WebSite)" /path:/$(AppName) /physicalPath:$(DeploymentDirectory)"></Exec>
<Exec Command="appcmd set app /app.name:"$(FullAppName)" /applicationPool:FooAppPool"></Exec>
</ Target>
</Project>
First I copy the compiled files over to the deployment directory, making sure I remove previous files. Then I use the Exec task to run the command line tool “AppCmd”. AppCmd is used to create an IIS 7 application pointing to the same physical location as the compiled files.
Well that’s it. I have compiled the web site, moved the web site to a deployment directory and created an IIS application pointing at the web site.
A side note
I have omitted talking about the problems I had figuring out the difference between compiling a Web Application and a Web Site. Not sure I have the big picture on this question yet. It seems like you easily can build a Web Application Project using only a MSBuild task (within MSBuild)
<MSBuild
Projects="$(ProjectFile)"
StopOnFirstFailure="true"
Targets="ResolveReferences;_CopyWebApplication;_BuiltWebOutputGroupOutput"
Properties="WarningLevel=0;DefineConstants=TRACE;Configuration=Debug;Platform=AnyCPU;Optimize=true;DebugSymbols=false;OverwriteReadOnlyFiles=true;WebProjectOutputDir=$(DeploymentDir)\;OutDir=$(DeploymentDir)\bin\;DocumentationFile=;"/>
That's it. For now. In a later post I guess I will talk about getting the script to run in TeamCity.