Wednesday, January 2, 2008

Targeting multiple environments through NAnt

One of the nice things about using a command-line local build is that I can easily target multiple environments.  Our configuration scheme is fairly straightforward, with all changes limited to one "web.config" file.

When I refer to multiple environments, I'm talking about many individual isolated deployment targets, such as production, integration, developer, local, etc.  Each environment has its own database, services, maybe even domain.  Sometimes I need to configure my local code to point to different environments, where maybe a defect shows up in production but not our integration environment.

A typical scenario might be that I have different database in each environment.  Different databases means different connection strings, and my connection strings are stored in my "web.config" file.  The problem is that the "web.config" file is stored in source control, and I don't want to always check-in and check-out the file each time I want to target a different environment.

Additionally, I don't want to have to remember the connection string when I switch to a different environment.  I want it all automated, and I want it to just work.

To point our local codebase at different environments, we apply a few tricks to NAnt to make it easy to switch back and forth between many environments.

The command-line build

The first item we have set up is a command-line build and local deployment.  Our environment is a too complex to have only solution compilation to be sufficient to actually run our app, so we use NAnt to build and run our software.  To do this, I have a very simple "go.bat" batch file that calls NAnt with the appropriate command-line arguments:

@tools\nant\NAnt.exe -buildfile:NBehave.build %*

When I call NAnt from the command-line, I can pass in multiple targets without needing to specify the build file or other arguments every time:

go clean test deploy

Now that I can easily call different targets in the build, I can use that mechanism to target different environments, doing something like this:

go PROD clean test deploy

Configuring NAnt

To get a NAnt target to change my configuration, I need a few elements in place:

  • File to hold configuration entries
  • Target to load configuration
  • Tasks to apply configuration

The basic idea is that the "PROD" or "SIT" or "DEV" target will load up specific configuration properties.  After compilation, these configuration properties will be inserted back into the web.config file.  I will have a set of configuration properties for each environment that have the same name, but different values.

Configuration settings file

I like to keep my configuration settings in a separate build file, so I created an "environmentSettings.build" file to hold all of the settings for each environment:

<?xml version="1.0" encoding="utf-8"?>
<project name="Environment Settings" xmlns="http://nant.sf.net/schemas/nant.xsd">

  <target name="config-settings-PROD">
    <property name="connection_string" value="Data Source=prddbsvr;Initial Catalog=AdventureWorks;Integrated Security=true" />
  </target>

  <target name="config-settings-SIT">
    <property name="connection_string" value="Data Source=sitdbsvr;Initial Catalog=AdventureWorks;Integrated Security=true" />
  </target>

  <target name="config-settings-DEV">
    <property name="connection_string" value="Data Source=(local);Initial Catalog=AdventureWorks;Integrated Security=true" />
  </target>

</project>

Two important items to note are:

  • Target names differ only by last part, the target environment
  • Targets all define the same property, namely "connection_string", but these values are different in each example

Selecting configuration

Now that my configuration settings file is finished, it's time to turn our attention back to the main build script file.  I need to add targets to handle "PROD", "SIT", etc.  Additionally, I want to define a property that has a default environment setting.

The targets that handle "PROD", etc. don't need to do much other than re-define the environment setting property and load the targets from the new file.  Here are those targets:

<property name="target-env" value="DEV" />

<target name="DEV">
  <property name="target-env" value="DEV" />
  <call target="load-config-settings" />
</target>

<target name="SIT">
  <property name="target-env" value="SIT" />
  <call target="load-config-settings" />
</target>

<target name="PROD">
  <property name="target-env" value="PROD" />
  <call target="load-config-settings" />
</target>

<target name="load-config-settings" unless="${target::has-executed('load-config-settings')}">
  <include buildfile="${env-settings.file}" />
</target>

The first thing to note here is the declaration of the "target-env" property at the top.  That will be useful later on when making decisions based on the target environment.

Next, I declare a set of targets named after my target environments, namely "DEV", "SIT" and "PROD".  These are also the same names as the postfixes in the target names in my "environmentSettings.build" file I created earlier.  In each of these targets, I override the "target-env" property with its new value, the target environment.  Remember that in my "go.bat" file, all command-line arguments are targets to be executed by NAnt, so I have to create a specific target for each target environment I want to support.

Finally, I call the "load-config-settings" target.  Its responsibility is simply to load the environment settings build file I created earlier, but not to call any of its targets.  The reason for the "unless" part is that NAnt does not allow you to declare the same targets twice, so I need to make sure that the "load-config-settings" target is only executed at most once.

Loading and applying configuration

Now that I have all of the targets loaded, I need to call the appropriate settings target and apply the configuration properties to the web.config file.  This step is usually done post-compilation, but I can apply the settings any time after they are loaded:

<target name="modify-web-config">
  
  <call target="config-settings-${target-env}" />

  <xmlpoke
    file="${deploy.dir}/Web.Config"
    xpath="/configuration/appSettings/add[@key='ConnectionString']/@value"
    value="${connection_string}"
   />

</target>

First, this target calls "config-settings-XXXXX", where the last part is filled in by the "target-env" property declared earlier.  If I chose "SIT", the "config-settings-SIT" target is called.  If I chose "PROD", the "config-settings-PROD" target is called.  Recall also that the "config-settings-XXXX" targets all declare the same properties, but with different values.

Finally, I use the xmlpoke task to modify the web.config file, giving it the new "connection_string" property value set up from the "config-settings-XXXX" target.

Now, if I want to target different environments, all I need to do is put in the environment name when calling the batch script, such as "go SIT deploy-local", and my local app now targets a different environment.  If there are more complex things I need to do based on the target environment, all I need to do is check the "target-env" property.

Wrapping it up

There are many different ways to target different environments, such as web deployment projects and solution configuration.  I found using NAnt integrated well with our command-line build and gave us a maintainable solution, as all build/deployment logic is hosted in one build script, instead of spread over many project or solution configurations.

5 comments:

Anonymous said...

Why don't you use MSBuild?

Jimmy Bogard said...

@SirMike

I use MSBuild when using Team Build, but I personally can get a NAnt build up and running faster than an MSBuild one.

Also, our builds are using specific features of NAnt, like functions, that don't exist in MSBuild.

We use MSBuild for all of our compilation tasks, just not build/deployment.

Unknown said...

hi jimmy brilliant article i am currently in a process of implementing continuous integration for an Web application (ASP.NET C# VSS) i was just wondering would it be possible to get an exaple of your nant build file. i can be emailed at min.shah@gmail.com
Thanks

Anonymous said...

Nice post, just found your blog. Think I'll have a browse through it this week.

We're doing something very similar to yourself at Huddle (www.huddle.net).

I've been researching into dev tree's etc... and came across Tree Surgeon (I've got the pdf from the original author before it became Tree Surgeon).

We've not got a build\lib\src etc... type dev tree and a go.bat

I've been working on CruiseControl.Net too and Nant.

I use CruiseControl as little as possible in terms of keeping its Config small and untouched when possible. Nant handles everything.

We have an entry Nant file which sets up directories, paths, variables, paths to apps like nunit etc...

So an entry point for dev build, test build etc... Then from there it calls another build file, which using all of those variables, makes a build.

Bil Simser said...

Good and interesting article to something I have a lot of passion over. I did find some issues the way you're calling things here (and maybe because the full build file isn't posted). I've made some enhancements to the process just because we need to do additonal things like publish ClickOnce apps, sign them, brand them with environment information, etc. that I think I'll follow up with a blog post. Would like to discuss this with you though if you have some time.