Quantcast
Viewing latest article 11
Browse Latest Browse All 56

Creating Multi-Environment Windows Installers with Visual Studio 2012 and Wix 3.7: Part 2


In part 1 we looked at the structure of a WiX setup project, and created a simple installer. We looked at the staging paradigm which gave us complete flexibility in the way we harvested files and packaged them into a deployment structure. We also looked at heat.exe, which we used to harvest files dynamically, and we concluded by wiring everything up into a single installer Feature.

Today we're going to look at ways to configure our installer for different environments.

Per-environment configuration

People need to install software in different environments, and in those different environments the names of things change. Server names and ports are different; connection strings are no longer the same; websites are secured, or end-points are protected. All too often people fix things up by running their installer and then tweaking the settings by hand; that's because it's by no means obvious what the best solution is, and with all the different deployment mechanisms to choose from, one size doesn't fit all. Since this is an article about WiX, I'm going to explore some of the things you can do with an MSI to ensure your application gets configured correctly in each of your target environments.

Environments as their own configuration

The simplest solution -- which may not work for every application -- is to turn the whole idea of per-environment configuration on its head. Instead of saying that the installer should know something about each deployment environment, you essentially say that each environment needs to supply the specific names for the set of generic configuration resources to which the installer refers. Thus, for example, the installer may refer to %APPSERVER% or %DATABASESERVER%, but these system environment variables will return different values for different environments. You can initialise them via a one-off batch script at the time the environment is built; alternatively, you can run these scripts as part of the application deployment process.

This approach works well when you can guarantee that environment variables will be expanded, but there are many situations where that won't be the case (configuration in BizTalk bindings, for example). For these situations, you need another approach.

Environments as keys to their own configuration

A related approach is to create a Configuration Management database -- a central resource containing configuration information that is accessible from all environments. The servers within each environment can then query the management database for their own specific configurations. This approach works well when the configuration items in question are more complex than simple value types; it has however a number of disadvantages, foremost of which is that at least one server per environment needs access to the database in question. Deploying behind firewalls and across domains can therefore prove problematic. Typically the installer queries the management database at install time, collecting all the environment-specific configuration into an associative array. Different file resources are then marked up by token substitution using an appropriate regular expression.

Two-stage deployment (copy files, then everything else)

Within this scenario the installer becomes simply a payload for file resources, devolving the responsibility for configuring the environment upon a subsequent script. Typically the installer and the configuration script also represent different technologies -- for example an MSI followed by a batch file, VB script, NAnt or Powershell scripts.  Superficially this seems a reasonable way to prototype things, particularly when the application and its increasingly complex configuration is evolving faster than the installer that deploys it.  But it does nothing to leverage the extensive WiX API and the various out-the-box features that come with it like Restore point creation, rollback, reinstallation, patching, etc. One of the reasons Configuration analysts like MSIs is because they can open them up and see exactly what is inside them, and what they intend to do. Something of that transparency is lost when you start running arbitrary scripts on top.

Configuration by Managed Code Custom Actions

One way to keep all application configuration not only within the MSI but within the scope of its execution is to delegate it to user Custom actions written in managed code. At a high level, this typically involves embedding sets of configuration items for each environment within the MSI (perhaps as an XML file within the Binary table, or else by creating and populating a custom installer table). You then have a number of options:

  • You can embed an assembly within the Binary table with code to transform any files that need to be marked up after they have been laid out on the file system. You can use TARGETDIR to establish a transformation root
  • You can map the environment-specific value corresponding to each configuration item back to a environment-agnostic installer property. You can then reference these properties directly, marking up the files with WiX objects like XmlFile. The only disadvantage of this method is that it may be non-trivial to mark up resources that the WiX API cannot parse natively

Conditional resource deployment

In my experience the simplest way to implement environment-specific configuration with WiX is to retain within your Visual Studio solution a set of configuration files, one for each environment. You then swap in the appropriate configuration files at deployment time by conditioning the Components that contains these files on the installation environment appropriate to them.

This makes installation straightforward, but from a maintenance point of view, particularly if you have a large number of separate configuration items, you need to think carefully about how you keep these files in sync. The worst approach where your configuration remit is high is a set of static configuration files (web.DEV.config, web.UAT.config, web.PRD.config, etc.). People will change (and forget that they changed) little things within a single environment for which a good case was made at the time - a timeout in seconds, or the maximum size of a response packet, or the maximum number of worker threads in a thread pool. Over time, it becomes harder and harder to establish which environment is really "definitive", and therefore should be used as the production base. Web Config Transforms were designed to get around this problem, linking each configuration item, via a structural transformation, to its base. However it's time consuming keeping these sets of transformations up to date, and tempting simply to drop in an entire appsettings replacement section -- which brings you back to virtually the same place you were with static files. T4 templates are another option, making it a lot easier to keep the different environments in step. Additionally, they are powerful enough to transform configuration files programatically in ways beyond the scope of WCTs.

Those caveats aside, for this example I will use static configuration files. Let's say that I'm not really interested where these files came from, except in so far as I can trust the process that produced them. What we need to do is add them to our test project, get them into the MSI, then get them deployed appropriately.

  • Download and unzip the code from part 1 of this article here
  • In Solution Explorer, copy and paste MyApplication's existing App.config file to make two more copies. Call them App.UAT.config and App.PRD.config, then edit the appSettings key you added in the two new files, assigning a value of “UAT” and “Production” respectively
  • Add a new WiX Installer file to MyApplicationInstaller; call it MarkUpMyApplication.wxs
  • Create a ComponentGroup and within it two components to handle the conditional installation of the UAT and PRD App.config files. Do this by highlighting the contents of the WiX file and replacing it with the following:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="
http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
    <ComponentGroup Id="MarkUpMyApplication">
      <!-- Swap in an appropriate config file based on the deployment environment -->
      <Component Id="cmp3702FD087BAF4C74AFF0F5B8BE86B027" Directory="MYAPPLICATIONINSTALLFOLDER" Guid="{78D12327-30D1-4E4C-9F0D-51B81A68C895}">
        <File Name="MyApplication.exe.config" Id="appconfig_uat" Source="..\MyApplication\App.UAT.config"/>
        <Condition><![CDATA[APPENV="UAT"]]></Condition>
      </Component>
      <Component Id="cmp46FDF759ED014654844A1C72EF171BB6" Directory="MYAPPLICATIONINSTALLFOLDER" Guid="{0C9BD4EE-9799-448F-A4A8-D355B658329D}">
        <File Name="MyApplication.exe.config" Id="appconfig_prd" Source="..\MyApplication\App.PRD.config"/>
        <Condition><![CDATA[APPENV="PRD"]]></Condition>
      </Component>
    </ComponentGroup>
  </Fragment>
</Wix>

  • Edit Product.wxs and add a reference to the MarkUpMyApplication fragment we just created. The ApplicationCopyFiles feature should now read:

    <Feature Id="ApplicationCopyFiles" Title="MyApplication.Installer.CopyFiles" Level="1000">
      <ComponentGroupRef Id="MyApplicationFilesComponents"/>
      <ComponentGroupRef Id="MarkUpMyApplication"/>
    </Feature>

Finally compile the project. You'll get 4 ICE30 errors: "Installation of a conditionalized component would cause the target file 'jnquizaz.con|MyApplication.exe.config' to be installed in [TARGETDIR]\Application by two different components on an LFN system." What's going on? Open the newly recreated HarvestedMyApplication.wxs, and notice that we are already harvesting MyApplication.exe.config. As the compiler has noticed, we will always install the default MyApplication.exe.config file, meaning that it makes no sense to add further conditions to the installation of a file with this name. What we need to do is make sure we *don't* attempt to harvest any file that we subsequently plan to replace with some other one at install time. This simply means editing MyApplicationInstaller.wixproj, and editing our ItemGroup. Do so, removing the line:

     <MyApplicationFiles Include="$(MSBuildProjectDirectory)\..\MyApplication\bin\$(Configuration)\*.config" />
 
Then save your changes, reload the project, and recompile it. This time, in place of the errors you'll get two ICE30 warnings: "The target file 'opstrggf.con|MyApplication.exe.config' might be installed in '[TARGETDIR]\Application\' by two different conditionalized components on an LFN system." Now that's a bit more acceptable. Again, the compiler notes that it can't be sure the conditions you've placed on component installation are mutually exclusive, so it may be possible for more than one condition to turn out to be true. We know however that that won't be the case, so in this instance we can overlook the warning.
Now give it a go by opening a command prompt in the directory that you've built the MSI and entering:


msiexec /i MyApplicationInstaller.msi ADDLOCAL="ApplicationCopyFiles" TARGETDIR="c:\temp" APPENV="UAT" /l*V log.txt

You can then run MyAPplication.exe in the c:\temp\Application folder and confirm the UAT settings have been deployed.

Hosting multiple environments on the same box

The term "environment" tends to imply a discrete set of configuration settings and application code sandboxed conceptually -- and also usually physically. But servers, particularly servers that provide specialist integration services (for example, those dedicated to Payment processing)  tend to be scarce resources, at which point the temptation arises to share such servers among multiple environments. In the past getting an MSI to support side-by-side installation of the same application configured for different environments was non-trivial. Typically you would create an Instance transform, modifying at a minimum the ProductCode and ProductName, so that the MSI would recognise the transform as unique, and so that you could identify the specific instance in the ARP. You would achieve this by opening up the installer database and using the installer API's Record, View and Database objects to update the relevant MSI tables, then inject the transforms one at a time back into the installer, ensuring you dispose of all the temporary objects in the correct sequence.  WiX makes this activity far simpler. We can simply insert an InstanceTransforms node, add an associated Instance identifier to map the transform property passed on the command line to our existing APPENV property -- and we're done.

Make these changes by opening Product.wxs and appending the following section of code as an immediate child of the "Product" node.

   <Property Id="APPENV" Value="UNDEFINED"/>
    <InstanceTransforms Property="APPENV">
      <Instance Id="UAT" ProductCode="{1E87B5D6-7BFB-420E-8679-794454556537}" ProductName="MyApplication (UAT)"/>
      <Instance Id="PRD" ProductCode="{0E29F802-F8E7-4408-ADF5-87C4FC539422}" ProductName="MyApplication (PRD)"/>
    </InstanceTransforms>

You can now test the installation (and install side-by-side instances of the product) with the following command lines:

msiexec /i MyApplicationInstaller.msi ADDLOCAL="ApplicationCopyFiles" TARGETDIR="c:\temp\UAT" MSINEWINSTANCE=1 TRANSFORMS=:UAT /l*V log.txt

msiexec /i MyApplicationInstaller.msi ADDLOCAL="ApplicationCopyFiles" TARGETDIR="c:\temp\PRD" MSINEWINSTANCE=1 TRANSFORMS=:PRD /l*V log.txt

There are two design decisions here. The first is the choice of installation directory. We could have parameterised this via a type 51 CA, determining it programatically from APPENV. I've chosen not to do this and instead left you to specify a TARGETDIR explicitly, partly because this gives me more control, but mostly because an environment-specific installation structure is not (or shouldn't be) a prerequisite for the proper functioning of the application -- which in essence is saying that an application instance is simply that -- a copy of the application, not a copy plus additional constraints.

The second decision is to put as little logic as possible into each transform, and as much logic as possible into "environment-agnostic" code, conditioning components where required. Because of the way we moved from a single environment MSI to a multi-environment MSI and finally a multi-instance MSI, this happened naturally. It's also meant that provisioning our MSIs for instance support added very little overhead, and very little ties us into it if we decide we no longer require it.

You'll notice of course that because of the way we swap in configuration files, we've introduced a tight coupling between our environment instances (in Product.wxs) and the file that configures them (MarkUpMyApplication.wxs). It would certainly be better if we could decouple these two, so that when we added a new environment we only needed to update it in one place. Really we want to write:

     <Component Id="cmp3702FD087BAF4C74AFF0F5B8BE86B027" Directory="MYAPPLICATIONINSTALLFOLDER" Guid="{78D12327-30D1-4E4C-9F0D-51B81A68C895}">
        <File Name="MyApplication.exe.config" Id="appconfig_uat" Source="..\MyApplication\App.[APPENV].config"/>
      </Component>

This removes the condition, generating the name of the file to install based on the target environment. But WiX doesn't support this, because it cannot verify at compile time that the file in question will actually exist at deployment time. In fact we can get the level of abstraction we want, but it's a bit more complicated. This is something we'll explore in part 3. In the meantime, you can download the code for part 2 here.


Viewing latest article 11
Browse Latest Browse All 56

Trending Articles