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:

@toolsnantNAnt.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.

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Jimmy Bogard

I'm a technical architect with Headspring in Austin, TX. I focus on DDD, distributed systems, and any other acronym-centric design/architecture/methodology. I created AutoMapper and am a co-author of the ASP.NET MVC in Action books.
This entry was posted in Tools. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://www.lostechies.com/blogs/sean_chambers/ Sean Chambers

    Im amazed by how much content you always have in your postings. Do you take a couple of days to write these postings or do you do them all at once?

    I was just trying to remember how to call targets from other targets tonight. thanks!

    xmlpoke is definately handy. I use it in almost every Nant script that I write.

  • http://grabbagoft.blogspot.com/ Jimmy Bogard

    @Sean

    I keep a running list of post ideas. If I have a conversation during the day that might make a good post, I write it down right then so I don’t forget. It really annoys the co-workers :) .

    Longer posts used to take a couple of days to hash out, but now I can knock them out in about an hour, but I’ve usually been thinking about them beforehand.

  • http://www.flux88.com Ben Scheirman

    Keep these NAnt tips coming!

  • Ujjaval

    when I fier this command “nant -D:build.defines=FAKE_AD_AUTH build” it will build the project success fully but how do I come to know the Parameter value from Program.cs file ?

    • jbogard

      Hmmm not sure. I actually haven’t used NAnt in a few years. You might want to ask on StackOverflow, I’m sure there’s some NAnt experts there!